From bcfe80e468d1f1603cdf733eed1a2296fd0bdefb Mon Sep 17 00:00:00 2001 From: Krysl Date: Thu, 30 Apr 2026 12:59:12 +0800 Subject: [PATCH 01/28] dart3: init a empty dart3 project Signed-off-by: Krysl --- impls/dart3/.gitignore | 3 + impls/dart3/CHANGELOG.md | 3 + impls/dart3/README.md | 2 + impls/dart3/analysis_options.yaml | 30 +++ impls/dart3/bin/mal.dart | 5 + impls/dart3/lib/mal.dart | 3 + impls/dart3/pubspec.lock | 389 ++++++++++++++++++++++++++++++ impls/dart3/pubspec.yaml | 15 ++ impls/dart3/test/mal_test.dart | 8 + 9 files changed, 458 insertions(+) create mode 100644 impls/dart3/.gitignore create mode 100644 impls/dart3/CHANGELOG.md create mode 100644 impls/dart3/README.md create mode 100644 impls/dart3/analysis_options.yaml create mode 100644 impls/dart3/bin/mal.dart create mode 100644 impls/dart3/lib/mal.dart create mode 100644 impls/dart3/pubspec.lock create mode 100644 impls/dart3/pubspec.yaml create mode 100644 impls/dart3/test/mal_test.dart diff --git a/impls/dart3/.gitignore b/impls/dart3/.gitignore new file mode 100644 index 0000000000..3a85790408 --- /dev/null +++ b/impls/dart3/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/impls/dart3/CHANGELOG.md b/impls/dart3/CHANGELOG.md new file mode 100644 index 0000000000..effe43c82c --- /dev/null +++ b/impls/dart3/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/impls/dart3/README.md b/impls/dart3/README.md new file mode 100644 index 0000000000..3816eca3ad --- /dev/null +++ b/impls/dart3/README.md @@ -0,0 +1,2 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/impls/dart3/analysis_options.yaml b/impls/dart3/analysis_options.yaml new file mode 100644 index 0000000000..dee8927aaf --- /dev/null +++ b/impls/dart3/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/impls/dart3/bin/mal.dart b/impls/dart3/bin/mal.dart new file mode 100644 index 0000000000..51031b7c51 --- /dev/null +++ b/impls/dart3/bin/mal.dart @@ -0,0 +1,5 @@ +import 'package:mal/mal.dart' as mal; + +void main(List arguments) { + print('Hello world: ${mal.calculate()}!'); +} diff --git a/impls/dart3/lib/mal.dart b/impls/dart3/lib/mal.dart new file mode 100644 index 0000000000..f64ad72658 --- /dev/null +++ b/impls/dart3/lib/mal.dart @@ -0,0 +1,3 @@ +int calculate() { + return 6 * 7; +} diff --git a/impls/dart3/pubspec.lock b/impls/dart3/pubspec.lock new file mode 100644 index 0000000000..8c4d394821 --- /dev/null +++ b/impls/dart3/pubspec.lock @@ -0,0 +1,389 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b + url: "https://pub.flutter-io.cn" + source: hosted + version: "100.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" + url: "https://pub.flutter-io.cn" + source: hosted + version: "13.0.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.20" + meta: + dependency: transitive + description: + name: meta + sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.18.2" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.31.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.12" + test_core: + dependency: transitive + description: + name: test_core + sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.18" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.flutter-io.cn" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.4 <4.0.0" diff --git a/impls/dart3/pubspec.yaml b/impls/dart3/pubspec.yaml new file mode 100644 index 0000000000..49d19d4746 --- /dev/null +++ b/impls/dart3/pubspec.yaml @@ -0,0 +1,15 @@ +name: mal +description: A sample command-line application. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.11.4 + +# Add regular dependencies here. +dependencies: + path: ^1.9.0 + +dev_dependencies: + lints: ^6.0.0 + test: ^1.25.6 diff --git a/impls/dart3/test/mal_test.dart b/impls/dart3/test/mal_test.dart new file mode 100644 index 0000000000..0fd24309eb --- /dev/null +++ b/impls/dart3/test/mal_test.dart @@ -0,0 +1,8 @@ +import 'package:mal/mal.dart'; +import 'package:test/test.dart'; + +void main() { + test('calculate', () { + expect(calculate(), 42); + }); +} From 7a92b5046b1acff6711d2c513a982861f66124a7 Mon Sep 17 00:00:00 2001 From: Krysl Date: Thu, 30 Apr 2026 15:35:47 +0800 Subject: [PATCH 02/28] dart3: step0 --- Makefile.impls | 3 ++- impls/dart3/bin/step0_repl.dart | 22 ++++++++++++++++++++++ impls/dart3/run | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 impls/dart3/bin/step0_repl.dart create mode 100644 impls/dart3/run diff --git a/Makefile.impls b/Makefile.impls index 2c3517d7ff..2aa1c413ec 100644 --- a/Makefile.impls +++ b/Makefile.impls @@ -32,7 +32,7 @@ wasm_MODE = wasmtime # Implementation specific settings # -IMPLS = ada ada.2 awk bash basic bbc-basic c c.2 chuck clojure coffee common-lisp cpp crystal cs d dart \ +IMPLS = ada ada.2 awk bash basic bbc-basic c c.2 chuck clojure coffee common-lisp cpp crystal cs d dart dart3 \ elisp elixir elm erlang es6 factor fantom fennel forth fsharp go groovy gnu-smalltalk \ guile hare haskell haxe hy io janet java java-truffle js jq julia kotlin latex3 livescript logo lua make mal \ matlab miniMAL nasm nim objc objpascal ocaml perl perl6 php picolisp pike plpgsql \ @@ -61,6 +61,7 @@ dist_EXCLUDES += guile io julia matlab swift # Extra options to pass to runtest.py bbc-basic_TEST_OPTS = --test-timeout 60 +dart3_TEST_OPTS = --no-pty guile_TEST_OPTS = --test-timeout 120 io_TEST_OPTS = --test-timeout 120 java-truffle_TEST_OPTS = --start-timeout 30 diff --git a/impls/dart3/bin/step0_repl.dart b/impls/dart3/bin/step0_repl.dart new file mode 100644 index 0000000000..6a5a34af9d --- /dev/null +++ b/impls/dart3/bin/step0_repl.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'dart:core'; +import 'dart:core' as core show print; + +String read(String str) => str; + +String eval(String str) => str; + +String print(String str) => str; + +String rep(String str) => print(eval(read(str))); + +void main(List args) { + while (true) { + core.print('user> '); + final input = stdin.readLineSync(); + if (input == null) break; + final output = rep(input); + core.print('$output\n'); + } +} diff --git a/impls/dart3/run b/impls/dart3/run new file mode 100644 index 0000000000..c57af5ee26 --- /dev/null +++ b/impls/dart3/run @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec dart $(dirname $0)/bin/${STEP:-stepA_mal}.dart "${@}" From 7c11d55c95cda4741fc0423a2d5cf1df1f11446e Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 1 May 2026 13:26:45 +0800 Subject: [PATCH 03/28] dart3: (wip) step 1.1 --- impls/dart3/bin/mal.dart | 5 -- impls/dart3/bin/step1_read_print.dart | 24 ++++++++ impls/dart3/lib/mal.dart | 6 +- impls/dart3/lib/src/print.dart | 3 + impls/dart3/lib/src/reader.dart | 87 +++++++++++++++++++++++++++ impls/dart3/lib/src/types.dart | 37 ++++++++++++ impls/dart3/test/mal_test.dart | 8 --- 7 files changed, 154 insertions(+), 16 deletions(-) delete mode 100644 impls/dart3/bin/mal.dart create mode 100644 impls/dart3/bin/step1_read_print.dart create mode 100644 impls/dart3/lib/src/print.dart create mode 100644 impls/dart3/lib/src/reader.dart create mode 100644 impls/dart3/lib/src/types.dart delete mode 100644 impls/dart3/test/mal_test.dart diff --git a/impls/dart3/bin/mal.dart b/impls/dart3/bin/mal.dart deleted file mode 100644 index 51031b7c51..0000000000 --- a/impls/dart3/bin/mal.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:mal/mal.dart' as mal; - -void main(List arguments) { - print('Hello world: ${mal.calculate()}!'); -} diff --git a/impls/dart3/bin/step1_read_print.dart b/impls/dart3/bin/step1_read_print.dart new file mode 100644 index 0000000000..0e1b598e27 --- /dev/null +++ b/impls/dart3/bin/step1_read_print.dart @@ -0,0 +1,24 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); +MalType eval(MalType val) => val; +String print(MalType str) => prStr(str); +String rep(String str) => print(eval(read(str))); + +void main(List args) { + while (true) { + core.print('user> '); + final input = stdin.readLineSync(); + if (input == null) break; + try { + final output = rep(input); + core.print('$output\n'); + } on UnexpectedError catch (e) { + core.print(e.message); + } + } +} diff --git a/impls/dart3/lib/mal.dart b/impls/dart3/lib/mal.dart index f64ad72658..b052ca6fc4 100644 --- a/impls/dart3/lib/mal.dart +++ b/impls/dart3/lib/mal.dart @@ -1,3 +1,3 @@ -int calculate() { - return 6 * 7; -} +export 'src/types.dart'; +export 'src/reader.dart'; +export 'src/print.dart'; \ No newline at end of file diff --git a/impls/dart3/lib/src/print.dart b/impls/dart3/lib/src/print.dart new file mode 100644 index 0000000000..d1b8194d79 --- /dev/null +++ b/impls/dart3/lib/src/print.dart @@ -0,0 +1,3 @@ +import 'package:mal/src/types.dart'; + +String prStr(MalType val) => val.toStr(); diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart new file mode 100644 index 0000000000..31b7ad61d5 --- /dev/null +++ b/impls/dart3/lib/src/reader.dart @@ -0,0 +1,87 @@ +import 'package:mal/src/types.dart'; + +class Reader { + final List _tokens; + late final int _len; + Reader(this._tokens) { + _len = _tokens.length; + } + int _index = 0; + + String? next() { + var token = peek(); + _index++; + return token; + } + + String? peek() { + if (_index >= _len) return null; + return _tokens[_index]; + } +} + +final re = RegExp( + r'''[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)''', +); + +List tokenize(String str) { + return re + .allMatches(str) + .map((e) => e.group(1)!) + .where((e) => e.isNotEmpty) + .toList(); +} + +class UnexpectedError extends Error { + final String? message; + UnexpectedError(this.message); + @override + String toString() => 'UnexpectedError: $message'; +} + +final intRe = RegExp(r'^-?[0-9]+$'); +MalType readAtom(Reader reader) { + final token = reader.next(); + if (token == null) throw UnexpectedError('unexpecetd EOF'); + if (intRe.hasMatch(token)) { + final val = int.parse(token); + return MalInt(val); + } + return MalSymbol(token); +} + +MalList readList(Reader reader) { + assert(reader.peek() == '('); + reader.next(); + final list = MalList(); + + while (true) { + final peek = reader.peek(); + if (peek == null) throw UnexpectedError('unexpecetd EOF'); + if (peek == ')') { + reader.next(); + break; + } + list.add(readForm(reader)); + } + return list; +} + +MalType readForm(Reader reader) { + final token = reader.peek(); + switch (token) { + case '(': + return readList(reader); + default: + // throw UnimplementedError('unexpect token $token'); + return readAtom(reader); + } +} + +MalType readStr(String str) { + final token = tokenize(str); + + final reader = Reader(token); + + return readForm(reader); +} diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart new file mode 100644 index 0000000000..f5a55afff0 --- /dev/null +++ b/impls/dart3/lib/src/types.dart @@ -0,0 +1,37 @@ +import 'dart:collection'; + +sealed class MalType { + String toStr(); +} + +class MalInt implements MalType { + final int _val; + MalInt(this._val); + @override + String toStr() => _val.toString(); +} + +class MalList extends ListBase implements MalType { + final List _inner = []; + @override + int get length => _inner.length; + + @override + set length(int newLength) => _inner.length = newLength; + + @override + MalType operator [](int index) => _inner[index]!; + + @override + void operator []=(int index, MalType value) => _inner[index] = value; + + @override + String toStr() => '(${_inner.map((e) => e!.toStr()).join(' ')})'; +} + +class MalSymbol extends MalType { + final String name; + MalSymbol(this.name); + @override + String toStr() => name; +} diff --git a/impls/dart3/test/mal_test.dart b/impls/dart3/test/mal_test.dart deleted file mode 100644 index 0fd24309eb..0000000000 --- a/impls/dart3/test/mal_test.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:mal/mal.dart'; -import 'package:test/test.dart'; - -void main() { - test('calculate', () { - expect(calculate(), 42); - }); -} From 559bd8f8acf81a0b38d2461f20a1782e1743c994 Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 1 May 2026 16:33:49 +0800 Subject: [PATCH 04/28] dart3: step1 passed --- impls/dart3/bin/step1_read_print.dart | 8 +- impls/dart3/lib/mal.dart | 3 +- impls/dart3/lib/src/reader.dart | 92 ++++++++++++----- impls/dart3/lib/src/types.dart | 116 +++++++++++++++++++++- impls/dart3/lib/src/utils/str_escape.dart | 29 ++++++ 5 files changed, 217 insertions(+), 31 deletions(-) create mode 100644 impls/dart3/lib/src/utils/str_escape.dart diff --git a/impls/dart3/bin/step1_read_print.dart b/impls/dart3/bin/step1_read_print.dart index 0e1b598e27..5bee0f074a 100644 --- a/impls/dart3/bin/step1_read_print.dart +++ b/impls/dart3/bin/step1_read_print.dart @@ -11,14 +11,14 @@ String rep(String str) => print(eval(read(str))); void main(List args) { while (true) { - core.print('user> '); + stdout.write('user> '); final input = stdin.readLineSync(); if (input == null) break; try { final output = rep(input); - core.print('$output\n'); - } on UnexpectedError catch (e) { - core.print(e.message); + stdout.write('$output\n'); + } on ParserError catch (e) { + core.print(e.toString()); } } } diff --git a/impls/dart3/lib/mal.dart b/impls/dart3/lib/mal.dart index b052ca6fc4..2ad52c1295 100644 --- a/impls/dart3/lib/mal.dart +++ b/impls/dart3/lib/mal.dart @@ -1,3 +1,4 @@ export 'src/types.dart'; export 'src/reader.dart'; -export 'src/print.dart'; \ No newline at end of file +export 'src/print.dart'; +export 'src/utils/str_escape.dart'; \ No newline at end of file diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index 31b7ad61d5..96f79e5e42 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -1,4 +1,6 @@ -import 'package:mal/src/types.dart'; +import 'dart:collection'; + +import 'package:mal/mal.dart'; class Reader { final List _tokens; @@ -20,8 +22,13 @@ class Reader { } } +const specialDoubleChRe = r'''~@'''; +const specialSingleChRe = r'''[\[\]{}()'`~^@]'''; +const strRe = r'''"(?:\\.|[^\\"])*"?'''; +const commentRe = r''';.*'''; +const normalSeqRe = r'''[^\s\[\]{}('"`,;)]*'''; final re = RegExp( - r'''[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)''', + '[\\s,]*($specialDoubleChRe|$specialSingleChRe|$strRe|$commentRe|$normalSeqRe)', ); List tokenize(String str) { @@ -32,50 +39,89 @@ List tokenize(String str) { .toList(); } -class UnexpectedError extends Error { - final String? message; - UnexpectedError(this.message); - @override - String toString() => 'UnexpectedError: $message'; -} - final intRe = RegExp(r'^-?[0-9]+$'); +final strRe2 = RegExp(r'''"(?(?:\\.|[^\\"])*)"?'''); MalType readAtom(Reader reader) { final token = reader.next(); if (token == null) throw UnexpectedError('unexpecetd EOF'); if (intRe.hasMatch(token)) { final val = int.parse(token); return MalInt(val); + } else if (token[0] == '"') { + final str = strRe2.firstMatch(token)!.namedGroup('string')!; + if (str.length == token.length - 1) { + throw UnbalancedBracketsError('need `"`'); + } + return MalString(str.escape()); } return MalSymbol(token); } -MalList readList(Reader reader) { - assert(reader.peek() == '('); +MalType readList(Reader reader, ParenthesesType p) { + assert(reader.peek() == p.left); reader.next(); - final list = MalList(); - + final list = switch (p) { + .round => MalList(), + .square => MalVector(), + .curly => MalMap(), + _ => throw UnsupportedError(''), + }; + bool isKey = true; + String key = ''; while (true) { final peek = reader.peek(); if (peek == null) throw UnexpectedError('unexpecetd EOF'); - if (peek == ')') { + if (peek == p.right) { reader.next(); break; } - list.add(readForm(reader)); + switch (p) { + case .round: + case .square: + (list as ListBase).add(readForm(reader)); + break; + case .curly: + if (isKey) { + key = peek; + } else { + reader.next(); + (list as MalMap)[key] = readForm(reader); + } + break; + default: + throw UnsupportedError(''); + } + isKey = !isKey; } - return list; + return list as MalType; } +const macros = { + "'": 'quote', + '`': 'quasiquote', + '~': 'unquote', + '~@': 'splice-unquote', + '@': 'deref', + '^': 'with-meta', +}; MalType readForm(Reader reader) { - final token = reader.peek(); - switch (token) { - case '(': - return readList(reader); - default: - // throw UnimplementedError('unexpect token $token'); - return readAtom(reader); + var token = reader.peek(); + MalList readQuote(String token) { + reader.next(); + if (token == '^') { + final a = readForm(reader); + final b = readForm(reader); + return MalList([MalSymbol(macros[token]!), b, a]); + } else { + return MalList([MalSymbol(macros[token]!), readForm(reader)]); + } } + + return switch (token) { + '(' || '[' || '{' => readList(reader, ParenthesesType.fromLeft(token!)), + "'" || '`' || '~' || '~@' || '@' || '^' => readQuote(token!), + _ => readAtom(reader), + }; } MalType readStr(String str) { diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index f5a55afff0..246538a64c 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -1,5 +1,28 @@ import 'dart:collection'; +import 'package:mal/src/utils/str_escape.dart'; + +abstract class ParserError extends Error { + final String? message; + ParserError(this.message); + @override + String toString() => + (message != null) // + ? '$runtimeType: $message' + : '$runtimeType'; +} + +class UnexpectedError extends ParserError { + UnexpectedError(super.message); +} + +class UnbalancedBracketsError extends ParserError { + UnbalancedBracketsError([super.message]); + + @override + String get message => '(unbalanced) ${super.message}'; // make test happy +} + sealed class MalType { String toStr(); } @@ -11,8 +34,43 @@ class MalInt implements MalType { String toStr() => _val.toString(); } -class MalList extends ListBase implements MalType { - final List _inner = []; +class MalString implements MalType { + final String _val; + MalString(String str) : _val = str; + + @override + String toStr() => '"${_val.toPrintable()}"'; +} + +extension type const Parentheses._((String, String) p) { + const Parentheses(String l, String r) : this._((l, r)); + + String get left => p.$1; + String get right => p.$2; +} + +enum ParenthesesType { + round(Parentheses('(', ')')), + square(Parentheses('[', ']')), + curly(Parentheses('{', '}')); + + const ParenthesesType(this.p); + final Parentheses p; + factory ParenthesesType.fromLeft(String left) { + return switch (left) { + '(' => ParenthesesType.round, + '[' => ParenthesesType.square, + '{' => ParenthesesType.curly, + _ => throw UnsupportedError('unsupport $left'), + }; + } + String get left => p.left; + String get right => p.right; +} + +class MalList extends ListBase implements MalType { + MalList([List? list]) : _inner = list ?? []; + final List _inner; @override int get length => _inner.length; @@ -23,10 +81,62 @@ class MalList extends ListBase implements MalType { MalType operator [](int index) => _inner[index]!; @override - void operator []=(int index, MalType value) => _inner[index] = value; + void operator []=(int index, MalType? value) => _inner[index] = value; @override String toStr() => '(${_inner.map((e) => e!.toStr()).join(' ')})'; + + @override + Iterable map(T Function(MalType? e) f) => _inner.map(f); +} + +class MalVector extends ListBase implements MalType { + final List _inner = []; + @override + int get length => _inner.length; + + @override + set length(int newLength) => _inner.length = newLength; + + @override + MalType operator [](int index) => _inner[index]!; + + @override + void operator []=(int index, MalType? value) => _inner[index] = value; + + @override + String toStr() => '[${_inner.map((e) => e!.toStr()).join(' ')}]'; + + @override + Iterable map(T Function(MalType? e) f) => _inner.map(f); +} + +// extension type MalVector._(MalList list) { +// MalVector() : this._(MalList()); +// @override +// String toStr() => '(${list.map((e) => e!.toStr()).join(' ')})'; +// } + +class MalMap with MapMixin implements MalType { + final _innerMap = {}; + @override + operator [](Object? key) => _innerMap[key]; + + @override + void operator []=(key, value) => _innerMap[key] = value; + + @override + void clear() => _innerMap.clear(); + + @override + Iterable get keys => _innerMap.keys; + + @override + remove(Object? key) => _innerMap.remove(key); + + @override + String toStr() => + '{${_innerMap.entries.map((kv) => '${kv.key} ${kv.value.toStr()}').join(' ')}}'; } class MalSymbol extends MalType { diff --git a/impls/dart3/lib/src/utils/str_escape.dart b/impls/dart3/lib/src/utils/str_escape.dart new file mode 100644 index 0000000000..799a621a5e --- /dev/null +++ b/impls/dart3/lib/src/utils/str_escape.dart @@ -0,0 +1,29 @@ +const escapeMap = { + r'\\': r'\', + r'\n': '\n', + r'\t': '\t', + r'\r': '\r', + // r"\'": "'", + r'\"': '"', + r'\b': '\b', + r'\f': '\f', + r'\v': '\v', +}; + +extension Escape on String { + String escape() { + String output = this; + for (final MapEntry(:key, :value) in escapeMap.entries) { + output = output.replaceAll(key, value); + } + return output; + } + + String toPrintable() { + String output = this; + for (final MapEntry(:key, :value) in escapeMap.entries) { + output = output.replaceAll(value, key); + } + return output; + } +} From 945f254ebd1686ff400ad0de45b93c549f65d891 Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 1 May 2026 17:26:20 +0800 Subject: [PATCH 05/28] dart3: add nil/bool/:keyword support --- impls/dart3/lib/src/reader.dart | 9 +++++++++ impls/dart3/lib/src/types.dart | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index 96f79e5e42..82fdc3104c 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -44,6 +44,7 @@ final strRe2 = RegExp(r'''"(?(?:\\.|[^\\"])*)"?'''); MalType readAtom(Reader reader) { final token = reader.next(); if (token == null) throw UnexpectedError('unexpecetd EOF'); + if (intRe.hasMatch(token)) { final val = int.parse(token); return MalInt(val); @@ -53,6 +54,14 @@ MalType readAtom(Reader reader) { throw UnbalancedBracketsError('need `"`'); } return MalString(str.escape()); + } else if (token[0] == ':') { + return MalKeyword(token.substring(1)); + } else if (token == 'nil') { + return MalNil(); + } else if (token == 'true') { + return MalBool(true); + } else if (token == 'false') { + return MalBool(false); } return MalSymbol(token); } diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 246538a64c..25e556ab94 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -34,6 +34,25 @@ class MalInt implements MalType { String toStr() => _val.toString(); } +class MalNil implements MalType { + @override + String toStr() => 'nil'; +} + +class MalBool implements MalType { + final bool _val; + MalBool(this._val); + @override + String toStr() => _val ? 'true' : 'false'; +} + +class MalKeyword implements MalType { + final String _val; + MalKeyword(String val) : _val = '\u029E$val'; + @override + String toStr() => ':${_val.substring(1)}'; +} + class MalString implements MalType { final String _val; MalString(String str) : _val = str; From 4e0bfef07c459421018cdd68c9c1c3b9a9335838 Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 1 May 2026 19:12:56 +0800 Subject: [PATCH 06/28] dart3: step2 passed --- impls/dart3/bin/step2_eval.dart | 73 ++++++++++++++++++++++++++++++++ impls/dart3/lib/src/reader.dart | 3 -- impls/dart3/lib/src/types.dart | 74 +++++++++++++++++++++++++-------- 3 files changed, 130 insertions(+), 20 deletions(-) create mode 100644 impls/dart3/bin/step2_eval.dart diff --git a/impls/dart3/bin/step2_eval.dart b/impls/dart3/bin/step2_eval.dart new file mode 100644 index 0000000000..5d9b2dbbf0 --- /dev/null +++ b/impls/dart3/bin/step2_eval.dart @@ -0,0 +1,73 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); +MalType eval(MalType ast, ReplEnv env) { + final debugEval = env.get('DEBUG-EVAL'); + if (debugEval != null && + debugEval is! MalNil && + !(debugEval is MalBool && debugEval.val == false)) { + stdout.write('EVAL: ${prStr(ast)}'); + } + return switch (ast) { + MalSymbol(name: final name) => env.getSymbol(name), + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ), + final MalMap map => MalMap(map.map((k, v) => MapEntry(k, eval(v, env)))), + final MalList list => + list.isNotEmpty + ? (eval(list.first, env) as MalFunction).call( + list.args.map((e) => eval(e, env)).toList(), + ) + : ast, + _ => ast, + }; +} + +String print(MalType str) => prStr(str); + +class ReplEnv { + final Map replEnv = { + '+': MalFunction((int a, int b) => a + b), + '-': MalFunction((int a, int b) => a - b), + '*': MalFunction((int a, int b) => a * b), + '/': MalFunction((int a, int b) => (a / b).round()), + }; + + MalType getSymbol(String name) { + final ret = get(name); + if (ret == null) { + throw KeyNotFoundError("'{$name} not found"); + } + + return ret; + } + + MalType? get(String name) { + if (replEnv.containsKey(name)) { + return replEnv[name]; + } + return null; + } +} + +final replEnv = ReplEnv(); +String rep(String str) => print(eval(read(str), replEnv)); + +void main(List args) { + while (true) { + stdout.write('user> '); + final input = stdin.readLineSync(); + if (input == null) break; + try { + final output = rep(input); + stdout.write('$output\n'); + } on ParserError catch (e) { + core.print(e.toString()); + } + } +} diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index 82fdc3104c..1a299cda3e 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -73,7 +73,6 @@ MalType readList(Reader reader, ParenthesesType p) { .round => MalList(), .square => MalVector(), .curly => MalMap(), - _ => throw UnsupportedError(''), }; bool isKey = true; String key = ''; @@ -97,8 +96,6 @@ MalType readList(Reader reader, ParenthesesType p) { (list as MalMap)[key] = readForm(reader); } break; - default: - throw UnsupportedError(''); } isKey = !isKey; } diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 25e556ab94..78983f0869 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -23,6 +23,10 @@ class UnbalancedBracketsError extends ParserError { String get message => '(unbalanced) ${super.message}'; // make test happy } +class KeyNotFoundError extends ParserError { + KeyNotFoundError(super.message); +} + sealed class MalType { String toStr(); } @@ -32,6 +36,8 @@ class MalInt implements MalType { MalInt(this._val); @override String toStr() => _val.toString(); + + int get val => _val; } class MalNil implements MalType { @@ -40,10 +46,10 @@ class MalNil implements MalType { } class MalBool implements MalType { - final bool _val; - MalBool(this._val); + final bool val; + MalBool(this.val); @override - String toStr() => _val ? 'true' : 'false'; + String toStr() => val ? 'true' : 'false'; } class MalKeyword implements MalType { @@ -87,9 +93,9 @@ enum ParenthesesType { String get right => p.right; } -class MalList extends ListBase implements MalType { - MalList([List? list]) : _inner = list ?? []; - final List _inner; +class MalList extends ListMixin implements MalType { + MalList([List? list]) : _inner = list ?? []; + final List _inner; @override int get length => _inner.length; @@ -97,20 +103,31 @@ class MalList extends ListBase implements MalType { set length(int newLength) => _inner.length = newLength; @override - MalType operator [](int index) => _inner[index]!; + MalType operator [](int index) => _inner[index]; + + @override + void operator []=(int index, MalType value) => _inner[index] = value; + + @override + String toStr() => '(${_inner.map((e) => e.toStr()).join(' ')})'; @override - void operator []=(int index, MalType? value) => _inner[index] = value; + Iterable map(T Function(MalType e) f) => _inner.map(f); + + List get list => _inner; @override - String toStr() => '(${_inner.map((e) => e!.toStr()).join(' ')})'; + void add(MalType element) => _inner.add(element); @override - Iterable map(T Function(MalType? e) f) => _inner.map(f); + MalType get first => _inner.first; + + List get args => _inner.sublist(1); } -class MalVector extends ListBase implements MalType { - final List _inner = []; +class MalVector extends ListBase implements MalType { + MalVector([List? list]) : _inner = list ?? []; + final List _inner; @override int get length => _inner.length; @@ -118,16 +135,20 @@ class MalVector extends ListBase implements MalType { set length(int newLength) => _inner.length = newLength; @override - MalType operator [](int index) => _inner[index]!; + MalType operator [](int index) => _inner[index]; @override - void operator []=(int index, MalType? value) => _inner[index] = value; + void operator []=(int index, MalType value) => _inner[index] = value; @override - String toStr() => '[${_inner.map((e) => e!.toStr()).join(' ')}]'; + String toStr() => '[${_inner.map((e) => e.toStr()).join(' ')}]'; @override - Iterable map(T Function(MalType? e) f) => _inner.map(f); + void add(MalType element) => _inner.add(element); + @override + Iterable map(T Function(MalType e) f) => _inner.map(f); + + List get list => _inner; } // extension type MalVector._(MalList list) { @@ -137,7 +158,8 @@ class MalVector extends ListBase implements MalType { // } class MalMap with MapMixin implements MalType { - final _innerMap = {}; + MalMap([Map? map]) : _innerMap = map ?? {}; + final Map _innerMap; @override operator [](Object? key) => _innerMap[key]; @@ -164,3 +186,21 @@ class MalSymbol extends MalType { @override String toStr() => name; } + +class MalFunction extends MalType { + final Function fn; + MalFunction(this.fn); + @override + String toStr() => fn.toString(); + + MalType call(List args) { + if (fn is int Function(int, int) && args.length == 2) { + return MalInt( + Function.apply(fn, args.map((e) => (e as MalInt).val).toList()), + ); + } + throw UnimplementedError( + 'funcion type ${fn.runtimeType} is not implemented', + ); + } +} From 5652fdb1be3ac7e9cba4bb4ffb64274393635833 Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 1 May 2026 21:12:44 +0800 Subject: [PATCH 07/28] dart3: step3 passed --- impls/dart3/bin/step3_env.dart | 107 +++++++++++++++++++++++++++++++++ impls/dart3/lib/mal.dart | 3 +- impls/dart3/lib/src/env.dart | 20 ++++++ impls/dart3/lib/src/types.dart | 47 +++++++++++---- impls/dart3/pubspec.lock | 2 +- impls/dart3/pubspec.yaml | 1 + 6 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 impls/dart3/bin/step3_env.dart create mode 100644 impls/dart3/lib/src/env.dart diff --git a/impls/dart3/bin/step3_env.dart b/impls/dart3/bin/step3_env.dart new file mode 100644 index 0000000000..b28e8e05a1 --- /dev/null +++ b/impls/dart3/bin/step3_env.dart @@ -0,0 +1,107 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); +MalType eval(MalType ast, Env env) { + final debugEval = env['DEBUG-EVAL']; + if (debugEval is! MalSymbolNotFound && + debugEval is! MalNil && + !(debugEval is MalBool && debugEval.val == false)) { + stdout.writeln('EVAL: ${prStr(ast)}'); + } + return switch (ast) { + MalSymbol(name: final name) => env[name], + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ), + final MalMap map => MalMap(map.map((k, v) => MapEntry(k, eval(v, env)))), + final MalList list => call(list, env, ast), + _ => ast, + }; +} + +MalType call(MalList list, Env env, MalList ast) { + if (list.isNotEmpty) { + var fn = eval(list.first, env); + if (fn is MalFunction) { + return fn.call(list.args.map((e) => eval(e, env)).toList(), env); + } else if (fn is MalMacroFunction) { + return fn.call(list.args, env); + } else if (fn is MalSymbolNotFound) { + throw KeyNotFoundError('${fn.name} not found'); + } else { + throw UnimplementedError('unknow fn type ${fn.runtimeType}'); + } + } else { + return ast; + } +} + +String print(MalType str) => prStr(str); + +int evalToInt(MalType a, Env env) { + var val = eval(a, env); + if (val is MalSymbolNotFound) { + throw KeyNotFoundError('${val.name} is not found'); + } + return (val as MalInt).val; +} + +final replEnv = Env( + data: { + '+': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) + evalToInt(b, env), + ), + '-': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) - evalToInt(b, env), + ), + '*': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) * evalToInt(b, env), + ), + '/': MalFunction( + (Env env, MalType a, MalType b) => + (evalToInt(a, env) / evalToInt(b, env)).round(), + ), + 'def!': MalMacroFunction( + (List args, Env env) => + env[(args[0] as MalSymbol).name] = eval(args[1], env), + ), + 'let*': MalMacroFunction((List args, Env env) { + final newEnv = Env(outer: env); + List first; + if (args.first is MalList) { + first = (args.first as MalList); + } else if (args.first is MalVector) { + first = (args.first as MalVector); + } else { + throw UnsupportedError( + 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); + } + + for (final [key, val] in first.slices(2)) { + newEnv[(key as MalSymbol).name] = eval(val, newEnv); + } + return eval(args[1], newEnv); + }), + }, +); +String rep(String str) => print(eval(read(str), replEnv)); + +void main(List args) { + while (true) { + stdout.write('user> '); + final input = stdin.readLineSync(); + if (input == null) break; + try { + final output = rep(input); + stdout.write('$output\n'); + } on ParserError catch (e) { + core.print(e.toString()); + } + } +} diff --git a/impls/dart3/lib/mal.dart b/impls/dart3/lib/mal.dart index 2ad52c1295..5d01ebe1d5 100644 --- a/impls/dart3/lib/mal.dart +++ b/impls/dart3/lib/mal.dart @@ -1,4 +1,5 @@ export 'src/types.dart'; export 'src/reader.dart'; export 'src/print.dart'; -export 'src/utils/str_escape.dart'; \ No newline at end of file +export 'src/env.dart'; +export 'src/utils/str_escape.dart'; diff --git a/impls/dart3/lib/src/env.dart b/impls/dart3/lib/src/env.dart new file mode 100644 index 0000000000..dddbc0430b --- /dev/null +++ b/impls/dart3/lib/src/env.dart @@ -0,0 +1,20 @@ +import 'package:mal/mal.dart'; + +class Env { + final Env? outer; + final Map data; + Env({this.outer, Map? data}) : data = data ?? {}; + + void operator []=(String key, MalType val) => data[key] = val; + MalType operator [](String key) => + data[key] ?? outer?[key] ?? MalSymbolNotFound(key); + + MalType getSymbol(String name) { + final ret = this[name]; + if (ret is MalSymbolNotFound) { + throw KeyNotFoundError("'{$name} not found"); + } + + return ret; + } +} diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 78983f0869..7953115b6e 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:mal/src/utils/str_escape.dart'; +import 'package:mal/mal.dart'; abstract class ParserError extends Error { final String? message; @@ -60,11 +60,11 @@ class MalKeyword implements MalType { } class MalString implements MalType { - final String _val; - MalString(String str) : _val = str; + final String val; + MalString(String str) : val = str; @override - String toStr() => '"${_val.toPrintable()}"'; + String toStr() => '"${val.toPrintable()}"'; } extension type const Parentheses._((String, String) p) { @@ -119,9 +119,6 @@ class MalList extends ListMixin implements MalType { @override void add(MalType element) => _inner.add(element); - @override - MalType get first => _inner.first; - List get args => _inner.sublist(1); } @@ -187,17 +184,43 @@ class MalSymbol extends MalType { String toStr() => name; } +class MalSymbolNotFound extends MalType { + final String name; + MalSymbolNotFound(this.name); + @override + String toStr() => "'{$name} not found"; +} + class MalFunction extends MalType { final Function fn; MalFunction(this.fn); @override String toStr() => fn.toString(); - MalType call(List args) { - if (fn is int Function(int, int) && args.length == 2) { - return MalInt( - Function.apply(fn, args.map((e) => (e as MalInt).val).toList()), - ); + MalType call(List args, [Env? env]) { + if (fn is int Function(Env, MalType, MalType) && args.length == 2) { + try { + return MalInt(Function.apply(fn, [env, ...args])); + } catch (e) { + print("error when run fn `$fn`"); + rethrow; + } + } + throw UnimplementedError( + 'funcion type ${fn.runtimeType} is not implemented', + ); + } +} + +class MalMacroFunction extends MalType { + final Function fn; + MalMacroFunction(this.fn); + @override + String toStr() => fn.toString(); + + MalType call(List args, Env env) { + if (fn is MalType Function(List args, Env env)) { + return fn(args, env); } throw UnimplementedError( 'funcion type ${fn.runtimeType} is not implemented', diff --git a/impls/dart3/pubspec.lock b/impls/dart3/pubspec.lock index 8c4d394821..01118efbec 100644 --- a/impls/dart3/pubspec.lock +++ b/impls/dart3/pubspec.lock @@ -50,7 +50,7 @@ packages: source: hosted version: "0.2.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" diff --git a/impls/dart3/pubspec.yaml b/impls/dart3/pubspec.yaml index 49d19d4746..82bbf80db1 100644 --- a/impls/dart3/pubspec.yaml +++ b/impls/dart3/pubspec.yaml @@ -8,6 +8,7 @@ environment: # Add regular dependencies here. dependencies: + collection: ^1.19.1 path: ^1.9.0 dev_dependencies: From 599d7973460100ba604d792b490f51c9df69e4df Mon Sep 17 00:00:00 2001 From: Krysl Date: Sun, 3 May 2026 21:46:06 +0800 Subject: [PATCH 08/28] dart3: all test [0~4] passed --- impls/dart3/.gitignore | 1 + impls/dart3/analysis_options.yaml | 6 +- impls/dart3/bin/step0_repl.dart | 7 +- impls/dart3/bin/step1_read_print.dart | 10 +- impls/dart3/bin/step2_eval.dart | 86 ++-- impls/dart3/bin/step3_env.dart | 57 ++- impls/dart3/bin/step4_if_fn_do.dart | 136 ++++++ impls/dart3/justfile | 20 + impls/dart3/lib/mal.dart | 2 + impls/dart3/lib/src/core.dart | 69 +++ impls/dart3/lib/src/env.dart | 77 +++- impls/dart3/lib/src/print.dart | 34 +- impls/dart3/lib/src/reader.dart | 67 ++- impls/dart3/lib/src/types.dart | 181 ++++++-- impls/dart3/lib/src/utils/ansi_color.dart | 21 + impls/dart3/lib/src/utils/str_escape.dart | 60 ++- impls/dart3/pubspec.lock | 8 + impls/dart3/pubspec.yaml | 1 + impls/dart3/tool/generate_all_mal_tests.dart | 85 ++++ impls/dart3/tool/generate_mal_tests.dart | 452 +++++++++++++++++++ 20 files changed, 1207 insertions(+), 173 deletions(-) create mode 100644 impls/dart3/bin/step4_if_fn_do.dart create mode 100644 impls/dart3/justfile create mode 100644 impls/dart3/lib/src/core.dart create mode 100644 impls/dart3/lib/src/utils/ansi_color.dart create mode 100644 impls/dart3/tool/generate_all_mal_tests.dart create mode 100644 impls/dart3/tool/generate_mal_tests.dart diff --git a/impls/dart3/.gitignore b/impls/dart3/.gitignore index 3a85790408..64287c1135 100644 --- a/impls/dart3/.gitignore +++ b/impls/dart3/.gitignore @@ -1,3 +1,4 @@ # https://dart.dev/guides/libraries/private-files # Created by `dart pub` .dart_tool/ +test/generated/ diff --git a/impls/dart3/analysis_options.yaml b/impls/dart3/analysis_options.yaml index dee8927aaf..010473a090 100644 --- a/impls/dart3/analysis_options.yaml +++ b/impls/dart3/analysis_options.yaml @@ -15,9 +15,9 @@ include: package:lints/recommended.yaml # Uncomment the following section to specify additional rules. -# linter: -# rules: -# - camel_case_types +linter: + rules: + - avoid_print # analyzer: # exclude: diff --git a/impls/dart3/bin/step0_repl.dart b/impls/dart3/bin/step0_repl.dart index 6a5a34af9d..a6e420b684 100644 --- a/impls/dart3/bin/step0_repl.dart +++ b/impls/dart3/bin/step0_repl.dart @@ -1,8 +1,5 @@ import 'dart:io'; -import 'dart:core'; -import 'dart:core' as core show print; - String read(String str) => str; String eval(String str) => str; @@ -13,10 +10,10 @@ String rep(String str) => print(eval(read(str))); void main(List args) { while (true) { - core.print('user> '); + stdout.write('user> '); final input = stdin.readLineSync(); if (input == null) break; final output = rep(input); - core.print('$output\n'); + stdout.writeln(output); } } diff --git a/impls/dart3/bin/step1_read_print.dart b/impls/dart3/bin/step1_read_print.dart index 5bee0f074a..e74a8469ad 100644 --- a/impls/dart3/bin/step1_read_print.dart +++ b/impls/dart3/bin/step1_read_print.dart @@ -1,24 +1,22 @@ -import 'dart:core'; -import 'dart:core' as core show print; import 'dart:io'; import 'package:mal/mal.dart'; MalType read(String str) => readStr(str); MalType eval(MalType val) => val; -String print(MalType str) => prStr(str); +String print(MalType str) => prStr(str, true); String rep(String str) => print(eval(read(str))); void main(List args) { while (true) { - stdout.write('user> '); + stdout.write('user> '.toBlue); final input = stdin.readLineSync(); if (input == null) break; try { final output = rep(input); - stdout.write('$output\n'); + stdout.writeln(output); } on ParserError catch (e) { - core.print(e.toString()); + stdout.writeln(e.toString()); } } } diff --git a/impls/dart3/bin/step2_eval.dart b/impls/dart3/bin/step2_eval.dart index 5d9b2dbbf0..327b71e8a2 100644 --- a/impls/dart3/bin/step2_eval.dart +++ b/impls/dart3/bin/step2_eval.dart @@ -1,73 +1,75 @@ -import 'dart:core'; -import 'dart:core' as core show print; import 'dart:io'; import 'package:mal/mal.dart'; MalType read(String str) => readStr(str); -MalType eval(MalType ast, ReplEnv env) { - final debugEval = env.get('DEBUG-EVAL'); - if (debugEval != null && - debugEval is! MalNil && - !(debugEval is MalBool && debugEval.val == false)) { - stdout.write('EVAL: ${prStr(ast)}'); +MalType eval(MalType ast, Env env) { + if (env.debugEval) { + stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); } + + MalType listCall(MalList list, Env env, MalList ast) { + if (list.isNotEmpty) { + var fn = eval(list.first, env); + if (fn is MalFunction) { + return fn.call(list.args.map((e) => eval(e, env)).toList(), env); + } + throw NotCallableError('${fn.toStr()} is not callable'); + } else { + return ast; + } + } + return switch (ast) { - MalSymbol(name: final name) => env.getSymbol(name), + final MalSymbol symbol => env.getSymbolVal(symbol), MalVector(list: final list) => MalVector( list.map((e) => eval(e, env)).toList(), ), final MalMap map => MalMap(map.map((k, v) => MapEntry(k, eval(v, env)))), - final MalList list => - list.isNotEmpty - ? (eval(list.first, env) as MalFunction).call( - list.args.map((e) => eval(e, env)).toList(), - ) - : ast, + final MalList list => listCall(list, env, ast), _ => ast, }; } -String print(MalType str) => prStr(str); - -class ReplEnv { - final Map replEnv = { - '+': MalFunction((int a, int b) => a + b), - '-': MalFunction((int a, int b) => a - b), - '*': MalFunction((int a, int b) => a * b), - '/': MalFunction((int a, int b) => (a / b).round()), - }; - - MalType getSymbol(String name) { - final ret = get(name); - if (ret == null) { - throw KeyNotFoundError("'{$name} not found"); - } +String print(MalType str) => prStr(str, true); - return ret; - } - - MalType? get(String name) { - if (replEnv.containsKey(name)) { - return replEnv[name]; - } - return null; +int evalToInt(MalType a, Env env) { + var val = eval(a, env); + if (val is MalSymbolNotFound) { + throw val.makeError(); } + return (val as MalInt).val; } -final replEnv = ReplEnv(); +final replEnv = Env( + data: { + '+': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) + evalToInt(b, env), + ), + '-': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) - evalToInt(b, env), + ), + '*': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) * evalToInt(b, env), + ), + '/': MalFunction( + (Env env, MalType a, MalType b) => + (evalToInt(a, env) / evalToInt(b, env)).round(), + ), + }, +); String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { while (true) { - stdout.write('user> '); + stdout.write('user> '.toBlue); final input = stdin.readLineSync(); if (input == null) break; try { final output = rep(input); - stdout.write('$output\n'); + stdout.writeln(output); } on ParserError catch (e) { - core.print(e.toString()); + stdout.writeln(e.toString()); } } } diff --git a/impls/dart3/bin/step3_env.dart b/impls/dart3/bin/step3_env.dart index b28e8e05a1..f531168310 100644 --- a/impls/dart3/bin/step3_env.dart +++ b/impls/dart3/bin/step3_env.dart @@ -1,5 +1,3 @@ -import 'dart:core'; -import 'dart:core' as core show print; import 'dart:io'; import 'package:collection/collection.dart'; @@ -7,46 +5,43 @@ import 'package:mal/mal.dart'; MalType read(String str) => readStr(str); MalType eval(MalType ast, Env env) { - final debugEval = env['DEBUG-EVAL']; - if (debugEval is! MalSymbolNotFound && - debugEval is! MalNil && - !(debugEval is MalBool && debugEval.val == false)) { - stdout.writeln('EVAL: ${prStr(ast)}'); + if (env.debugEval) { + stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); } + + MalType listCall(MalList list, Env env, MalList ast) { + if (list.isNotEmpty) { + var fn = eval(list.first, env); + if (fn is MalFunction) { + return fn.call(list.args.map((e) => eval(e, env)).toList(), env); + } else if (fn is MalMacroFunction) { + return fn.call(list.args, env); + } else if (fn is MalSymbolNotFound) { + throw fn.makeError(); + } + throw NotCallableError('${fn.toStr()} is not callable'); + } else { + return ast; + } + } + return switch (ast) { - MalSymbol(name: final name) => env[name], + final MalSymbol symbol => env.getSymbolVal(symbol), MalVector(list: final list) => MalVector( list.map((e) => eval(e, env)).toList(), ), final MalMap map => MalMap(map.map((k, v) => MapEntry(k, eval(v, env)))), - final MalList list => call(list, env, ast), + final MalList list => listCall(list, env, ast), _ => ast, }; } -MalType call(MalList list, Env env, MalList ast) { - if (list.isNotEmpty) { - var fn = eval(list.first, env); - if (fn is MalFunction) { - return fn.call(list.args.map((e) => eval(e, env)).toList(), env); - } else if (fn is MalMacroFunction) { - return fn.call(list.args, env); - } else if (fn is MalSymbolNotFound) { - throw KeyNotFoundError('${fn.name} not found'); - } else { - throw UnimplementedError('unknow fn type ${fn.runtimeType}'); - } - } else { - return ast; - } -} - -String print(MalType str) => prStr(str); +String print(MalType str) => prStr(str, true); int evalToInt(MalType a, Env env) { var val = eval(a, env); if (val is MalSymbolNotFound) { - throw KeyNotFoundError('${val.name} is not found'); + throw val.makeError(); } return (val as MalInt).val; } @@ -94,14 +89,14 @@ String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { while (true) { - stdout.write('user> '); + stdout.write('user> '.toBlue); final input = stdin.readLineSync(); if (input == null) break; try { final output = rep(input); - stdout.write('$output\n'); + stdout.writeln(output); } on ParserError catch (e) { - core.print(e.toString()); + stdout.writeln(e.toString()); } } } diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart new file mode 100644 index 0000000000..4d88bbbb11 --- /dev/null +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -0,0 +1,136 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); +MalType eval(MalType ast, Env env) { + if (env.debugEval) { + stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); + } + + MalType listCall(ListLike list, Env env, T ast) { + if (list.isNotEmpty) { + var fn = eval(list.first, env); + if (fn is MalFunction) { + return fn.call(list.args.map((e) => eval(e, env)).toList(), env); + } else if (fn is MalMacroFunction) { + return fn.call(list.args, env); + } else if (fn is MalClosure) { + return fn.call(list.args.map((e) => eval(e, env)).toList()); + } else if (fn is MalSymbolNotFound) { + throw fn.makeError(); + } + throw NotCallableError('${fn.toStr()} is not callable'); + } else { + return ast.toMalType(); + } + } + + return switch (ast) { + final MalSymbol symbol => env.getSymbolVal(symbol), + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ), + final MalMap map => MalMap(map.map((k, v) => MapEntry(k, eval(v, env)))), + final MalList list => listCall(list, env, ast), + _ => ast, + }; +} + +String print(MalType str) => prStr(str, true); + +int evalToInt(MalType a, Env env) { + var val = eval(a, env); + if (val is MalSymbolNotFound) { + throw val.makeError(); + } + return (val as MalInt).val; +} + +final replEnv = globalEnv + ..addAll({ + '+': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) + evalToInt(b, env), + ), + '-': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) - evalToInt(b, env), + ), + '*': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) * evalToInt(b, env), + ), + '/': MalFunction( + (Env env, MalType a, MalType b) => + (evalToInt(a, env) / evalToInt(b, env)).round(), + ), + 'def!': MalMacroFunction( + (List args, Env env) => + env[(args[0] as MalSymbol).name] = eval(args[1], env), + ), + 'let*': MalMacroFunction((List args, Env env) { + final newEnv = Env(outer: env); + List first; + if (args.first is MalList) { + first = (args.first as MalList); + } else if (args.first is MalVector) { + first = (args.first as MalVector); + } else { + throw UnsupportedError( + 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); + } + + for (final [key, val] in first.slices(2)) { + newEnv[(key as MalSymbol).name] = eval(val, newEnv); + } + return eval(args[1], newEnv); + }), + 'do': MalMacroFunction((List args, Env env) { + return args.map((e) => eval(e, env)).toList().last; + }), + 'if': MalMacroFunction((List args, Env env) { + final br = eval(args.first, env); + if (br is! MalNil && !(br is MalBool && br.val == false)) { + return eval(args[1], env); + } else if (args.length > 2) { + return eval(args[2], env); + } else { + return MalNil(); + } + }), + 'fn*': MalMacroFunction((List args, Env env) { + final first = args.first; + final list = ((first is ListLike ? first : null) as ListLike?)?.list; + if (list == null) { + throw UnsupportedError( + 'fn* not support ${list.runtimeType}($list) as params', + ); + } + final params = List.from(list); + + return MalClosure( + params, + env, + (List fnArgs) => + eval(args.second, Env(outer: env, binds: params, exprs: fnArgs)), + ); + }), + ...ns, + }); +String rep(String str) => print(eval(read(str), replEnv)); + +void main(List args) { + while (true) { + stdout.write('user> '.toBlue); + final input = stdin.readLineSync(); + if (input == null) break; + try { + final output = rep(input); + stdout.writeln(output); + } on ParserError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/justfile b/impls/dart3/justfile new file mode 100644 index 0000000000..2fd00df850 --- /dev/null +++ b/impls/dart3/justfile @@ -0,0 +1,20 @@ +default: + @just --choose + +about: + @awk '/^#/ {print} !/^#/ {exit}' "{{justfile()}}" + @echo 'Summarises the purpose of this file.' + @echo 'This lists the comment lines of the file until the first line that does not start with a '#' character. Then it lists the targets of the file.' + @just --list + +alias c := clean-tests +alias g := gen-tests + +clean-tests: + cd test/generated && rm -f * +gen-tests: + @dart run tool/generate_all_mal_tests.dart + +test-generated FILE='test/generated/step4_if_fn_do_generated_test.dart' TAG='': + @if [ -n "{{TAG}}" ]; then dart test "{{FILE}}" -t "{{TAG}}"; else dart test "{{FILE}}"; fi + diff --git a/impls/dart3/lib/mal.dart b/impls/dart3/lib/mal.dart index 5d01ebe1d5..af002831a7 100644 --- a/impls/dart3/lib/mal.dart +++ b/impls/dart3/lib/mal.dart @@ -2,4 +2,6 @@ export 'src/types.dart'; export 'src/reader.dart'; export 'src/print.dart'; export 'src/env.dart'; +export 'src/core.dart'; export 'src/utils/str_escape.dart'; +export 'src/utils/ansi_color.dart'; diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart new file mode 100644 index 0000000000..c6d14ae540 --- /dev/null +++ b/impls/dart3/lib/src/core.dart @@ -0,0 +1,69 @@ +import 'package:mal/mal.dart'; + +final Map ns = { + 'prn': MalFunction((List args, Env env) { + println(args.isNotEmpty ? args.map((e) => prStr(e, true)).join(' ') : ''); + return MalNil(); + }), + 'println': MalFunction((List args, Env env) { + println(args.isNotEmpty ? args.map((e) => prStr(e, false)).join(' ') : ''); + return MalNil(); + }), + 'pr-str': MalFunction( + (List args, Env env) => + MalString(args.map((e) => prStr(e, true)).join(' ')), + ), + 'str': MalFunction( + (List args, Env env) => + MalString(args.map((e) => prStr(e, false)).join('')), + ), + 'list': MalFunction((List args, Env env) => MalList(args)), + 'list?': MalFunction( + (List args, Env env) => MalBool(args.first is MalList), + ), + 'empty?': MalFunction( + (List args, Env env) => MalBool((args.first as ListLike).isEmpty), + ), + 'count': MalFunction((List args, Env env) { + switch (args.first) { + case MalList(length: final len): + return MalInt(len); + case MalVector(length: final len): + return MalInt(len); + case MalNil(): + return MalInt(0); + default: + throw UnsupportedError( + 'count fn cannot count ${args.first.runtimeType}', + ); + } + }), + '=': MalFunction((List args, Env env) { + return MalBool((args.first, args.second).equals()); + }), + '>': MalFunction( + (List args, Env env) => + MalBool(args.first.asInt() > args.second.asInt()), + ), + '>=': MalFunction( + (List args, Env env) => + MalBool(args.first.asInt() >= args.second.asInt()), + ), + '<': MalFunction( + (List args, Env env) => + MalBool(args.first.asInt() < args.second.asInt()), + ), + '<=': MalFunction( + (List args, Env env) => + MalBool(args.first.asInt() <= args.second.asInt()), + ), + 'not': MalFunction( + (List args, Env env) => MalBool(switch (args.first) { + MalBool(val: final val) => !val, + MalNil() => true, + MalString() => false, + MalInt() => false, + _ => throw UnimplementedError(), + }), + ), +}; diff --git a/impls/dart3/lib/src/env.dart b/impls/dart3/lib/src/env.dart index dddbc0430b..814b8cc04b 100644 --- a/impls/dart3/lib/src/env.dart +++ b/impls/dart3/lib/src/env.dart @@ -1,20 +1,79 @@ +import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; +class Flags { + final Env _env; + Flags(this._env); + + bool operator [](String key) { + final debug = _env[key]; + if (debug != null && + debug is! MalNil && + !(debug is MalBool && debug.val == false)) { + return true; + } + return false; + } + + void operator []=(String key, bool? val) { + switch (val) { + case null: + _env.data.remove(key); + case true: + case false: + _env[key] = MalBool(val); + } + } +} + class Env { final Env? outer; final Map data; - Env({this.outer, Map? data}) : data = data ?? {}; + Env({ + this.outer, + Map? data, + List? binds, + List? exprs, + }) : data = data ?? {} { + if (binds == null) { + assert(exprs == null); + } else { + assert( + exprs != null && + (binds.length == exprs.length || + binds.firstWhereOrNull((e) => e.name == '&') != null), + ); + for (var i = 0; i < binds.length; i++) { + final name = binds[i].name; + if (name == '&') { + this[binds[i + 1].name] = MalList(exprs!.sublist(i)); + break; + } + this[name] = exprs![i]; + } + } + flags = Flags(this); + } void operator []=(String key, MalType val) => data[key] = val; - MalType operator [](String key) => - data[key] ?? outer?[key] ?? MalSymbolNotFound(key); + MalType? operator [](String key) => data[key] ?? outer?[key]; - MalType getSymbol(String name) { - final ret = this[name]; - if (ret is MalSymbolNotFound) { - throw KeyNotFoundError("'{$name} not found"); + void addAll(Map other) => data.addAll(other); + MalType getSymbolVal(MalSymbol symbol) { + if (symbol.isBuiltin) { + return symbol; + } else { + final key = symbol.name; + return data[key] ?? outer?[key] ?? MalSymbolNotFound(symbol.token!); } - - return ret; } + + late final Flags flags; + + bool get debugEval => flags['DEBUG-EVAL']; + set debugEval(bool val) => flags['DEBUG-EVAL'] = val; + bool get debugStr => flags['DEBUG-STR']; + set debugStr(bool val) => flags['DEBUG-STR'] = val; } + +final globalEnv = Env(); diff --git a/impls/dart3/lib/src/print.dart b/impls/dart3/lib/src/print.dart index d1b8194d79..c01405ff51 100644 --- a/impls/dart3/lib/src/print.dart +++ b/impls/dart3/lib/src/print.dart @@ -1,3 +1,33 @@ -import 'package:mal/src/types.dart'; +import 'dart:io'; +import 'dart:collection'; -String prStr(MalType val) => val.toStr(); +import 'package:mal/mal.dart'; + +typedef PrintFn = void Function(String output); +PrintFn println = (output) => stdout.writeln(output); +var debugPrint = stdout.writeln; + +final _testOutput = Queue(); +final crlf = RegExp(r'\r?\n'); +void setTestMock() { + println = (output) => _testOutput.addAll(output.split(crlf)); +} + +String readTestOutput() => _testOutput.removeFirst(); +List clearTestOutput() { + final list = _testOutput.toList(); + _testOutput.clear(); + return list; +} + +String prStr(MalType val, [bool printReadably = false]) { + final debugOn = globalEnv.debugStr; + if (debugOn) { + debugPrint( + '${'prStr'.toGreen}${printReadably ? 't' : 'f'}' + '${'<${val.runtimeType}>'.toYellow}' + '${val.toStr().toMagenta}', + ); + } + return val.toStr(printReadably); +} diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index 1a299cda3e..e22454ad98 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -3,20 +3,20 @@ import 'dart:collection'; import 'package:mal/mal.dart'; class Reader { - final List _tokens; + final List _tokens; late final int _len; Reader(this._tokens) { _len = _tokens.length; } int _index = 0; - String? next() { + Token? next() { var token = peek(); _index++; return token; } - String? peek() { + Token? peek() { if (_index >= _len) return null; return _tokens[_index]; } @@ -31,11 +31,32 @@ final re = RegExp( '[\\s,]*($specialDoubleChRe|$specialSingleChRe|$strRe|$commentRe|$normalSeqRe)', ); -List tokenize(String str) { +class Token { + Token({ + required this.str, + required this.input, + required this.start, + required this.end, + }); + String str; + String input; + final int start; + final int end; + + bool hasMatch(RegExp re) => re.hasMatch(str); + int get length => str.length; + + String get tokenIndicator => + start >= 0 ? ('$input\n${' ' * start}^${'~' * (end - start - 1)}') : ''; +} + +List tokenize(String str) { return re .allMatches(str) - .map((e) => e.group(1)!) - .where((e) => e.isNotEmpty) + .map( + (e) => Token(str: e.group(1)!, input: str, start: e.start, end: e.end), + ) + .where((e) => e.str.isNotEmpty) .toList(); } @@ -45,29 +66,29 @@ MalType readAtom(Reader reader) { final token = reader.next(); if (token == null) throw UnexpectedError('unexpecetd EOF'); - if (intRe.hasMatch(token)) { - final val = int.parse(token); + if (intRe.hasMatch(token.str)) { + final val = int.parse(token.str); return MalInt(val); - } else if (token[0] == '"') { - final str = strRe2.firstMatch(token)!.namedGroup('string')!; + } else if (token.str[0] == '"') { + final str = strRe2.firstMatch(token.str)!.namedGroup('string')!; if (str.length == token.length - 1) { throw UnbalancedBracketsError('need `"`'); } return MalString(str.escape()); - } else if (token[0] == ':') { - return MalKeyword(token.substring(1)); - } else if (token == 'nil') { + } else if (token.str[0] == ':') { + return MalKeyword(token.str.substring(1)); + } else if (token.str == 'nil') { return MalNil(); - } else if (token == 'true') { + } else if (token.str == 'true') { return MalBool(true); - } else if (token == 'false') { + } else if (token.str == 'false') { return MalBool(false); } return MalSymbol(token); } MalType readList(Reader reader, ParenthesesType p) { - assert(reader.peek() == p.left); + assert(reader.peek()!.str == p.left); reader.next(); final list = switch (p) { .round => MalList(), @@ -79,7 +100,7 @@ MalType readList(Reader reader, ParenthesesType p) { while (true) { final peek = reader.peek(); if (peek == null) throw UnexpectedError('unexpecetd EOF'); - if (peek == p.right) { + if (peek.str == p.right) { reader.next(); break; } @@ -90,7 +111,7 @@ MalType readList(Reader reader, ParenthesesType p) { break; case .curly: if (isKey) { - key = peek; + key = peek.str; } else { reader.next(); (list as MalMap)[key] = readForm(reader); @@ -117,15 +138,15 @@ MalType readForm(Reader reader) { if (token == '^') { final a = readForm(reader); final b = readForm(reader); - return MalList([MalSymbol(macros[token]!), b, a]); + return MalList([MalSymbol.builtin(macros[token]!), b, a]); } else { - return MalList([MalSymbol(macros[token]!), readForm(reader)]); + return MalList([MalSymbol.builtin(macros[token]!), readForm(reader)]); } } - return switch (token) { - '(' || '[' || '{' => readList(reader, ParenthesesType.fromLeft(token!)), - "'" || '`' || '~' || '~@' || '@' || '^' => readQuote(token!), + return switch (token?.str) { + '(' || '[' || '{' => readList(reader, ParenthesesType.fromLeft(token!.str)), + "'" || '`' || '~' || '~@' || '@' || '^' => readQuote(token!.str), _ => readAtom(reader), }; } diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 7953115b6e..6e597dab06 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -27,44 +27,106 @@ class KeyNotFoundError extends ParserError { KeyNotFoundError(super.message); } +class NotCallableError extends ParserError { + NotCallableError(super.message); +} + sealed class MalType { - String toStr(); + String toStr([bool printReadably = false]); +} + +extension MalTypeAs on MalType { + int asInt() => (this as MalInt).val; + List asList() => (this as ListLike).list; +} + +extension Second on List { + MalType get second => this[1]; +} + +extension Equals on (MalType left, MalType right) { + MalType get first => this.$1; + MalType get second => this.$2; + bool equals() { + if (this.$1.runtimeType != this.$2.runtimeType) { + if (!(this.$1 is ListLike && this.$2 is ListLike)) { + return false; + } + } + final eq = switch (first) { + MalInt() => sameTypeEquals((a) => (a as MalInt).val), + MalNil() => true, + MalBool() => sameTypeEquals((a) => (a as MalBool).val), + MalKeyword() => sameTypeEquals((a) => (a as MalKeyword).val), + MalString() => sameTypeEquals((a) => (a as MalString).val), + MalMap() => throw UnimplementedError(), + MalSymbol() => throw UnimplementedError(), + MalSymbolNotFound() => throw UnimplementedError(), + MalFunction() => throw UnimplementedError(), + MalMacroFunction() => throw UnimplementedError(), + MalClosure() => throw UnimplementedError(), + MalList() => listEqual(), + MalVector() => listEqual(), + }; + return eq; + } + + bool sameTypeEquals(T Function(MalType) getVal) => + getVal(first) == getVal(second); + bool listEqual() { + var a = first.asList(); + var b = second.asList(); + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + final eq = (a[i], b[i]).equals(); + if (!eq) return false; + } + return true; + } } -class MalInt implements MalType { +class MalInt extends MalType { final int _val; MalInt(this._val); @override - String toStr() => _val.toString(); + String toStr([bool printReadably = false]) => _val.toString(); int get val => _val; } -class MalNil implements MalType { +class MalNil extends MalType { @override - String toStr() => 'nil'; + String toStr([bool printReadably = false]) => 'nil'; } -class MalBool implements MalType { +class MalBool extends MalType { final bool val; MalBool(this.val); @override - String toStr() => val ? 'true' : 'false'; + String toStr([bool printReadably = false]) => val ? 'true' : 'false'; } -class MalKeyword implements MalType { - final String _val; - MalKeyword(String val) : _val = '\u029E$val'; +class MalKeyword extends MalType { + final String val; + MalKeyword(String val) : val = '\u029E$val'; @override - String toStr() => ':${_val.substring(1)}'; + String toStr([bool printReadably = false]) => ':${val.substring(1)}'; } -class MalString implements MalType { +class MalString extends MalType { final String val; - MalString(String str) : val = str; + MalString(String str) + : val = str, + assert(() { + debugPrint('make MalString($str)'); + return true; + }()); @override - String toStr() => '"${val.toPrintable()}"'; + String toStr([bool printReadably = false]) => + printReadably == true ? '"${val.toPrintable()}"' : val; } extension type const Parentheses._((String, String) p) { @@ -93,7 +155,20 @@ enum ParenthesesType { String get right => p.right; } -class MalList extends ListMixin implements MalType { +abstract interface class ListLike { + List get list; + bool get isEmpty => list.isEmpty; + bool get isNotEmpty => list.isNotEmpty; + MalType get first => list.first; + List get args => list.sublist(1); + MalType toMalType() => switch (this) { + MalList() => this as MalList, + MalVector() => this as MalList, + _ => throw UnimplementedError('$runtimeType to MalType'), + }; +} + +class MalList extends ListMixin implements MalType, ListLike { MalList([List? list]) : _inner = list ?? []; final List _inner; @override @@ -109,20 +184,26 @@ class MalList extends ListMixin implements MalType { void operator []=(int index, MalType value) => _inner[index] = value; @override - String toStr() => '(${_inner.map((e) => e.toStr()).join(' ')})'; + String toStr([bool printReadably = false]) => + '(${_inner.map((e) => e.toStr(printReadably)).join(' ')})'; @override Iterable map(T Function(MalType e) f) => _inner.map(f); + @override List get list => _inner; @override void add(MalType element) => _inner.add(element); + @override List get args => _inner.sublist(1); + + @override + MalType toMalType() => this; } -class MalVector extends ListBase implements MalType { +class MalVector extends ListBase implements MalType, ListLike { MalVector([List? list]) : _inner = list ?? []; final List _inner; @override @@ -138,21 +219,23 @@ class MalVector extends ListBase implements MalType { void operator []=(int index, MalType value) => _inner[index] = value; @override - String toStr() => '[${_inner.map((e) => e.toStr()).join(' ')}]'; + String toStr([bool printReadably = false]) => + '[${_inner.map((e) => e.toStr(printReadably)).join(' ')}]'; @override void add(MalType element) => _inner.add(element); @override Iterable map(T Function(MalType e) f) => _inner.map(f); + @override List get list => _inner; -} -// extension type MalVector._(MalList list) { -// MalVector() : this._(MalList()); -// @override -// String toStr() => '(${list.map((e) => e!.toStr()).join(' ')})'; -// } + @override + List get args => _inner.sublist(1); + + @override + MalType toMalType() => this; +} class MalMap with MapMixin implements MalType { MalMap([Map? map]) : _innerMap = map ?? {}; @@ -173,38 +256,46 @@ class MalMap with MapMixin implements MalType { remove(Object? key) => _innerMap.remove(key); @override - String toStr() => - '{${_innerMap.entries.map((kv) => '${kv.key} ${kv.value.toStr()}').join(' ')}}'; + String toStr([bool printReadably = false]) => + '{${_innerMap.entries.map((kv) => '${kv.key} ${kv.value.toStr(printReadably)}').join(' ')}}'; } class MalSymbol extends MalType { final String name; - MalSymbol(this.name); + final Token? token; + MalSymbol(this.token) : name = token!.str; + MalSymbol.builtin(this.name) : token = null; @override - String toStr() => name; + String toStr([bool printReadably = false]) => name; + bool get isBuiltin => token == null; } class MalSymbolNotFound extends MalType { - final String name; - MalSymbolNotFound(this.name); + final Token token; + MalSymbolNotFound(this.token); @override - String toStr() => "'{$name} not found"; + String toStr([bool printReadably = false]) => + "'$name not found\n${token.tokenIndicator}"; + String get name => token.str; + Error makeError() => KeyNotFoundError(toStr()); } class MalFunction extends MalType { final Function fn; MalFunction(this.fn); @override - String toStr() => fn.toString(); + String toStr([bool printReadably = false]) => fn.toString(); - MalType call(List args, [Env? env]) { + MalType call(List args, Env env) { if (fn is int Function(Env, MalType, MalType) && args.length == 2) { try { return MalInt(Function.apply(fn, [env, ...args])); } catch (e) { - print("error when run fn `$fn`"); + debugPrint("error when run fn `$fn`"); rethrow; } + } else if (fn is MalType Function(List args, Env env)) { + return fn(args, env); } throw UnimplementedError( 'funcion type ${fn.runtimeType} is not implemented', @@ -212,11 +303,12 @@ class MalFunction extends MalType { } } +/// without eval args class MalMacroFunction extends MalType { final Function fn; MalMacroFunction(this.fn); @override - String toStr() => fn.toString(); + String toStr([bool printReadably = false]) => fn.toString(); MalType call(List args, Env env) { if (fn is MalType Function(List args, Env env)) { @@ -227,3 +319,22 @@ class MalMacroFunction extends MalType { ); } } + +class MalClosure extends MalType { + final Function fn; + final List params; + final Env env; + MalClosure(this.params, this.env, this.fn); + + MalType call(List args) { + if (fn is MalType Function(List args)) { + return fn(args); + } + throw UnimplementedError( + 'funcion type ${fn.runtimeType} is not implemented', + ); + } + + @override + String toStr([bool printReadably = false]) => '#'; +} diff --git a/impls/dart3/lib/src/utils/ansi_color.dart b/impls/dart3/lib/src/utils/ansi_color.dart new file mode 100644 index 0000000000..55653eae85 --- /dev/null +++ b/impls/dart3/lib/src/utils/ansi_color.dart @@ -0,0 +1,21 @@ +import 'package:ansicolor/ansicolor.dart'; + +AnsiPen black = AnsiPen()..black(); +AnsiPen red = AnsiPen()..red(); +AnsiPen green = AnsiPen()..green(); +AnsiPen yellow = AnsiPen()..yellow(); +AnsiPen blue = AnsiPen()..blue(); +AnsiPen magenta = AnsiPen()..magenta(); +AnsiPen cyan = AnsiPen()..cyan(); +AnsiPen white = AnsiPen()..white(); + +extension ToColor on String { + String get toBlack => black(this); + String get toRed => red(this); + String get toGreen => green(this); + String get toYellow => yellow(this); + String get toBlue => blue(this); + String get toMagenta => magenta(this); + String get toCyan => cyan(this); + String get toWhite => white(this); +} diff --git a/impls/dart3/lib/src/utils/str_escape.dart b/impls/dart3/lib/src/utils/str_escape.dart index 799a621a5e..2903e33db7 100644 --- a/impls/dart3/lib/src/utils/str_escape.dart +++ b/impls/dart3/lib/src/utils/str_escape.dart @@ -1,29 +1,55 @@ const escapeMap = { - r'\\': r'\', - r'\n': '\n', - r'\t': '\t', - r'\r': '\r', - // r"\'": "'", - r'\"': '"', - r'\b': '\b', - r'\f': '\f', - r'\v': '\v', + r'\': r'\', + r'n': '\n', + r't': '\t', + r'r': '\r', + //r"'": "'", + r'"': '"', + r'b': '\b', + r'f': '\f', + r'v': '\v', }; +final needEscape = escapeMap.map((key, value) => MapEntry(value, key)) +// ..removeWhere((k, v) => k == '"') +; +const backslash = r'\'; extension Escape on String { String escape() { - String output = this; - for (final MapEntry(:key, :value) in escapeMap.entries) { - output = output.replaceAll(key, value); + if (length == 0) return ""; + final out = StringBuffer(); + + final iter = runes.map((e) => String.fromCharCode(e)).iterator; + final backslash = r'\'; + while (iter.moveNext()) { + final curr = iter.current; + if (curr == backslash) { + iter.moveNext(); + final escaped = escapeMap[iter.current]; + out.write(escaped); + } else { + out.write(curr); + } } - return output; + + return out.toString(); } String toPrintable() { - String output = this; - for (final MapEntry(:key, :value) in escapeMap.entries) { - output = output.replaceAll(value, key); + final out = StringBuffer(); + final iter = runes.map((e) => String.fromCharCode(e)).iterator; + + while (iter.moveNext()) { + final String curr = iter.current; + + if (needEscape.containsKey(curr)) { + final escaped = needEscape[iter.current]!; + out.write('\\$escaped'); + } else { + out.write(curr); + } } - return output; + + return out.toString(); } } diff --git a/impls/dart3/pubspec.lock b/impls/dart3/pubspec.lock index 01118efbec..14bd551fc0 100644 --- a/impls/dart3/pubspec.lock +++ b/impls/dart3/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "13.0.0" + ansicolor: + dependency: "direct main" + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" args: dependency: transitive description: diff --git a/impls/dart3/pubspec.yaml b/impls/dart3/pubspec.yaml index 82bbf80db1..c55882027e 100644 --- a/impls/dart3/pubspec.yaml +++ b/impls/dart3/pubspec.yaml @@ -8,6 +8,7 @@ environment: # Add regular dependencies here. dependencies: + ansicolor: ^2.0.3 collection: ^1.19.1 path: ^1.9.0 diff --git a/impls/dart3/tool/generate_all_mal_tests.dart b/impls/dart3/tool/generate_all_mal_tests.dart new file mode 100644 index 0000000000..4a973d5299 --- /dev/null +++ b/impls/dart3/tool/generate_all_mal_tests.dart @@ -0,0 +1,85 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +void main(List args) async { + if (args.contains('--help') || args.contains('-h')) { + _printUsageAndExit(0); + } + if (args.isNotEmpty) { + _printUsageAndExit(64); + } + + final scriptDir = p.dirname(p.fromUri(Platform.script)); + final dart3Root = p.normalize(p.join(scriptDir, '..')); + final repoRoot = p.normalize(p.join(dart3Root, '..', '..')); + final testsDir = Directory(p.join(repoRoot, 'tests')); + final outputDir = Directory(p.join(dart3Root, 'test', 'generated')); + + if (!testsDir.existsSync()) { + stderr.writeln('Tests directory not found: ${testsDir.path}'); + exit(1); + } + + outputDir.createSync(recursive: true); + + final stepFiles = + testsDir + .listSync() + .whereType() + .where((file) { + final name = p.basename(file.path); + return name.startsWith('step') && name.endsWith('.mal'); + }) + .map((file) => p.normalize(file.path)) + .toList() + ..sort(); + + if (stepFiles.isEmpty) { + stdout.writeln('No step*.mal files found in ${testsDir.path}'); + return; + } + + var generatedCount = 0; + for (final inputPath in stepFiles) { + final stepName = p.basenameWithoutExtension(inputPath); + final outputPath = p.join( + outputDir.path, + '${stepName}_generated_test.dart', + ); + final importFilePath = 'bin/$stepName.dart'; + if (!File(importFilePath).existsSync()) { + stdout.writeln( + 'import file($importFilePath) not found, skip the generation.', + ); + continue; + } + final result = await Process.run('dart', [ + 'run', + 'tool/generate_mal_tests.dart', + inputPath, + '--output', + outputPath, + '--bin-import', + '../../$importFilePath', + ], workingDirectory: dart3Root); + + stdout.write(result.stdout); + stderr.write(result.stderr); + + if (result.exitCode != 0) { + stderr.writeln('Failed generating tests for $inputPath'); + exit(result.exitCode); + } + + generatedCount++; + } + + stdout.writeln('Generated $generatedCount test files -> ${outputDir.path}'); +} + +Never _printUsageAndExit(int code) { + final sink = code == 0 ? stdout : stderr; + sink.writeln('Usage: dart run tool/generate_all_mal_tests.dart'); + exit(code); +} diff --git a/impls/dart3/tool/generate_mal_tests.dart b/impls/dart3/tool/generate_mal_tests.dart new file mode 100644 index 0000000000..5acbfdf8bb --- /dev/null +++ b/impls/dart3/tool/generate_mal_tests.dart @@ -0,0 +1,452 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +class TestCase { + const TestCase({ + required this.groupTitle, + required this.form, + required this.preRunForms, + required this.expectedStdout, + required this.expectedReturn, + required this.lineNumber, + required this.soft, + required this.deferrable, + required this.optional, + }); + + final String groupTitle; + final String form; + final List preRunForms; + final List expectedStdout; + final String expectedReturn; + final int lineNumber; + final bool soft; + final bool deferrable; + final bool optional; +} + +class TestGroup { + TestGroup(this.title); + + final String title; + final List tests = []; +} + +class ParseResult { + const ParseResult(this.groups); + + final List groups; +} + +void main(List args) { + if (args.contains('--help') || args.contains('-h') || args.isEmpty) { + _printUsageAndExit(args.isEmpty ? 64 : 0); + } + + final options = _ArgParser(args); + final inputPath = p.normalize(p.absolute(options.inputPath)); + final outputPath = p.normalize( + p.absolute(options.outputPath ?? _defaultOutputPath(inputPath)), + ); + final binImport = + options.binImport ?? '../bin/${p.basenameWithoutExtension(inputPath)}.dart'; + + final parseResult = parseMalTests(File(inputPath).readAsStringSync()); + + final content = renderTestFile( + parseResult, + sourceMalPath: inputPath, + outputTestPath: outputPath, + binImport: binImport, + ); + + File(outputPath) + ..createSync(recursive: true) + ..writeAsStringSync(content); + + _autoFixEvalCallForLegacySteps(outputPath); + + final testCount = parseResult.groups.fold( + 0, + (sum, group) => sum + group.tests.length, + ); + stdout.writeln('Generated $testCount tests -> $outputPath'); +} + +void _autoFixEvalCallForLegacySteps(String outputPath) { + final scriptDir = p.dirname(p.fromUri(Platform.script)); + final dart3Root = p.normalize(p.join(scriptDir, '..')); + final outputForAnalyze = p.relative(outputPath, from: dart3Root).replaceAll('\\', '/'); + + final analyzeResult = Process.runSync( + 'dart', + ['analyze', outputForAnalyze], + workingDirectory: dart3Root, + ); + final analyzeOutput = '${analyzeResult.stdout}\n${analyzeResult.stderr}'; + + final hasExtraPositional = analyzeOutput.contains( + 'Too many positional arguments: 1 expected, but 2 found.', + ); + final hasUndefinedReplEnv = analyzeOutput.contains( + "Undefined name 'replEnv'.", + ); + + if (!hasExtraPositional || !hasUndefinedReplEnv) { + return; + } + + final file = File(outputPath); + final before = file.readAsStringSync(); + final replacementPattern = RegExp(r'eval\(read\(f\),\s*replEnv\)'); + if (!replacementPattern.hasMatch(before)) { + return; + } + + final after = before.replaceAll(replacementPattern, 'eval(read(f))'); + file.writeAsStringSync(after); + + final verifyResult = Process.runSync( + 'dart', + ['analyze', outputForAnalyze], + workingDirectory: dart3Root, + ); + final verifyOutput = '${verifyResult.stdout}\n${verifyResult.stderr}'; + final stillHasExtraPositional = verifyOutput.contains( + 'Too many positional arguments: 1 expected, but 2 found.', + ); + final stillHasUndefinedReplEnv = verifyOutput.contains( + "Undefined name 'replEnv'.", + ); + final stillHasSameErrors = stillHasExtraPositional || stillHasUndefinedReplEnv; + + if (!stillHasSameErrors) { + stdout.writeln('Auto-fixed eval/replEnv call mismatch in $outputPath'); + } +} + +String _defaultOutputPath(String inputPath) { + final testsDir = p.dirname(inputPath); + final name = '${p.basenameWithoutExtension(inputPath)}_generated_test.dart'; + return p.join(testsDir, '..', 'test', name); +} + +ParseResult parseMalTests(String source) { + final lines = source.split('\n'); + final groups = []; + var currentGroup = TestGroup('Ungrouped'); + groups.add(currentGroup); + final pendingPreRun = []; + + var soft = false; + var deferrable = false; + var optional = false; + final pendingComments = []; + + for (var index = 0; index < lines.length; index++) { + final rawLine = lines[index]; + final line = rawLine.trimRight(); + final lineNumber = index + 1; + + if (line.trim().isEmpty) { + continue; + } + + if (line.startsWith(';;;')) { + continue; + } + + if (line.startsWith(';>>> ')) { + final settings = parseSettings(line.substring(5)); + if (settings.containsKey('soft')) { + soft = settings['soft']!; + } + if (settings.containsKey('deferrable')) { + deferrable = settings['deferrable']!; + } + if (settings.containsKey('optional')) { + optional = settings['optional']!; + } + continue; + } + + if (line.startsWith(';;')) { + pendingComments.add(line.length >= 3 ? line.substring(3) : ''); + continue; + } + + if (line.startsWith(';')) { + throw FormatException('Unexpected comment syntax at line $lineNumber: $line'); + } + + final groupTitle = normalizeGroupTitle(pendingComments); + pendingComments.clear(); + if (groupTitle != null) { + if (currentGroup.tests.isEmpty && currentGroup.title == 'Ungrouped') { + currentGroup = TestGroup(groupTitle); + groups[0] = currentGroup; + } else if (currentGroup.title != groupTitle) { + currentGroup = TestGroup(groupTitle); + groups.add(currentGroup); + } + } + + final stdoutLines = []; + var expectedReturn = ''; + var cursor = index + 1; + while (cursor < lines.length) { + final expectationLine = lines[cursor]; + if (expectationLine.startsWith(';/')) { + stdoutLines.add(expectationLine.substring(2)); + cursor++; + continue; + } + if (expectationLine.startsWith(';=>')) { + expectedReturn = expectationLine.substring(3); + cursor++; + } + break; + } + index = cursor - 1; + + final isNoCheck = stdoutLines.isEmpty && expectedReturn == ''; + + if (isNoCheck) { + pendingPreRun.add(line); + continue; + } + + final testCase = TestCase( + groupTitle: currentGroup.title, + form: line, + preRunForms: List.from(pendingPreRun), + expectedStdout: stdoutLines, + expectedReturn: expectedReturn, + lineNumber: lineNumber, + soft: soft, + deferrable: deferrable, + optional: optional, + ); + pendingPreRun.clear(); + currentGroup.tests.add(testCase); + } + + return ParseResult(groups.where((group) => group.tests.isNotEmpty).toList()); +} + +Map parseSettings(String source) { + final values = {}; + final matches = RegExp( + r'(soft|deferrable|optional)\s*=\s*(True|False|true|false)', + ).allMatches(source); + for (final match in matches) { + values[match.group(1)!] = match.group(2)!.toLowerCase() == 'true'; + } + return values; +} + +String? normalizeGroupTitle(List comments) { + final titles = comments + .map((comment) => _normalizeComment(comment)) + .whereType() + .toList(); + if (titles.isEmpty) { + return null; + } + return titles.join(' / '); +} + +String? _normalizeComment(String comment) { + final trimmed = comment.trim(); + if (trimmed.isEmpty) { + return null; + } + if (RegExp(r'^[-=]+$').hasMatch(trimmed)) { + return null; + } + final stripped = trimmed.replaceAll(RegExp(r'^[-=\s]+|[-=\s]+$'), '').trim(); + return stripped.isEmpty ? null : stripped; +} + +String renderTestFile( + ParseResult result, { + required String sourceMalPath, + required String outputTestPath, + required String binImport, +}) { + final sourceRelative = p.posix.normalize( + p.relative(sourceMalPath, from: p.dirname(outputTestPath)).replaceAll('\\', '/'), + ); + + final buffer = StringBuffer() + ..writeln("import 'package:mal/mal.dart';") + ..writeln("import 'package:test/test.dart';") + ..writeln("import '$binImport';") + ..writeln() + ..writeln('// Generated from $sourceRelative.') + ..writeln() + ..writeln('class _RunResult {') + ..writeln(' const _RunResult({required this.stdoutLines, required this.returnValue});') + ..writeln() + ..writeln(' final List stdoutLines;') + ..writeln(' final String returnValue;') + ..writeln('}') + ..writeln() + ..writeln('Future<_RunResult> _runCase(List forms) async {') + ..writeln(' clearTestOutput();') + ..writeln(' String returnValue = \'nil\';') + ..writeln(' final throws = [];') + ..writeln(' for (final f in forms) {') + ..writeln(' try {') + ..writeln(' returnValue = print(eval(read(f), replEnv));') + ..writeln(' } catch (e) {') + ..writeln(' throws.add(e.toString().replaceAll(\'\\n\', \'\\\\n\'));') + ..writeln(' }') + ..writeln(' }') + ..writeln(' return _RunResult(') + ..writeln(' stdoutLines: [...clearTestOutput(), ...throws],') + ..writeln(' returnValue: returnValue,') + ..writeln(' );') + ..writeln('}') + ..writeln() + ..writeln('void main() {') + ..writeln(' setUpAll(setTestMock);'); + + for (final group in result.groups) { + buffer.writeln(' group(${_dartString(group.title)}, () {'); + for (final testCase in group.tests) { + final tags = []; + if (testCase.soft) { + tags.add('soft'); + } + if (testCase.deferrable) { + tags.add('deferrable'); + } + if (testCase.optional) { + tags.add('optional'); + } + if (testCase.soft || testCase.deferrable || testCase.optional) { + buffer.writeln( + ' // line ${testCase.lineNumber}: soft=${testCase.soft}, deferrable=${testCase.deferrable}, optional=${testCase.optional}', + ); + } + buffer.writeln(' test(${_dartString(testCase.form)}, () async {'); + final allForms = [...testCase.preRunForms, testCase.form]; + final formsList = '[${allForms.map(_dartString).join(', ')}]'; + buffer.writeln(' final result = await _runCase($formsList);'); + if (testCase.expectedStdout.isNotEmpty) { + if (testCase.expectedReturn.isEmpty && testCase.expectedStdout.length == 1) { + final regex = '^${testCase.expectedStdout[0]}\u0000'.replaceAll( + '\u0000', + r'$', + ); + final regexContainsNewline = testCase.expectedStdout[0].contains(r'\n'); + buffer.writeln(' if (result.stdoutLines.isEmpty) {'); + if (!regexContainsNewline) { + buffer.writeln( + ' expect(result.returnValue, matches(RegExp(${_dartString(regex)})));', + ); + } + buffer.writeln(' } else {'); + buffer.writeln(' expect(result.stdoutLines, hasLength(1));'); + buffer.writeln( + ' expect(result.stdoutLines[0], matches(RegExp(${_dartString(regex)})));', + ); + buffer.writeln(' expect(result.returnValue, equals(${_dartString('nil')}));'); + buffer.writeln(' }'); + } else { + buffer.writeln( + ' expect(result.stdoutLines, hasLength(${testCase.expectedStdout.length}));', + ); + for (var i = 0; i < testCase.expectedStdout.length; i++) { + final regex = '^${testCase.expectedStdout[i]}\u0000'.replaceAll( + '\u0000', + r'$', + ); + buffer.writeln( + ' expect(result.stdoutLines[$i], matches(RegExp(${_dartString(regex)})));', + ); + } + } + } + if (testCase.expectedReturn.isNotEmpty) { + buffer.writeln( + ' expect(result.returnValue, equals(${_dartString(testCase.expectedReturn)}));', + ); + } + final tagsArg = tags.isEmpty ? '' : ', tags: ${_dartStringList(tags)}'; + buffer.writeln(' }$tagsArg);'); + } + buffer.writeln(' });'); + } + + buffer.writeln('}'); + return buffer.toString(); +} + +String _dartStringList(List values) { + if (values.isEmpty) { + return 'const []'; + } + return '[${values.map(_dartString).join(', ')}]'; +} + +String _dartString(String value) { + if (!value.contains("'") && !value.contains(r'$')) { + return "r'''$value'''"; + } + + final escaped = value + .replaceAll(r'\', r'\\') + .replaceAll("'", r"\'") + .replaceAll(r'$', r'\$') + .replaceAll('\r', r'\r') + .replaceAll('\n', r'\n'); + return "'$escaped'"; +} + +class _ArgParser { + _ArgParser(List args) + : outputPath = _readFlagValue(args, '--output'), + binImport = _readFlagValue(args, '--bin-import'), + inputPath = _readInputPath(args); + + final String? outputPath; + final String? binImport; + final String inputPath; + + static String _readInputPath(List args) { + for (var i = 0; i < args.length; i++) { + final arg = args[i]; + if (arg == '--output' || arg == '--bin-import') { + i++; + continue; + } + if (!arg.startsWith('--')) { + return arg; + } + } + _printUsageAndExit(64); + } + + static String? _readFlagValue(List args, String flag) { + final index = args.indexOf(flag); + if (index == -1) { + return null; + } + if (index + 1 >= args.length) { + stderr.writeln('Missing value for $flag'); + exit(64); + } + return args[index + 1]; + } +} + +Never _printUsageAndExit(int code) { + final sink = code == 0 ? stdout : stderr; + sink.writeln( + 'Usage: dart run tool/generate_mal_tests.dart [--output ] [--bin-import ]', + ); + exit(code); +} \ No newline at end of file From dc6b30b23b5a5bf213e1dab77d7ab541a6a2051f Mon Sep 17 00:00:00 2001 From: Krysl Date: Sun, 3 May 2026 23:33:06 +0800 Subject: [PATCH 09/28] dart3: refactor --- impls/dart3/bin/step4_if_fn_do.dart | 6 +- impls/dart3/lib/src/core.dart | 4 +- impls/dart3/lib/src/reader.dart | 11 + impls/dart3/lib/src/types.dart | 309 +++++++++++++++++----------- impls/dart3/pubspec.lock | 2 +- impls/dart3/pubspec.yaml | 1 + 6 files changed, 212 insertions(+), 121 deletions(-) diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart index 4d88bbbb11..0dc2b68136 100644 --- a/impls/dart3/bin/step4_if_fn_do.dart +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -11,7 +11,7 @@ MalType eval(MalType ast, Env env) { stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); } - MalType listCall(ListLike list, Env env, T ast) { + MalType listCall(MalListBase list, Env env, MalListBase ast) { if (list.isNotEmpty) { var fn = eval(list.first, env); if (fn is MalFunction) { @@ -25,7 +25,7 @@ MalType eval(MalType ast, Env env) { } throw NotCallableError('${fn.toStr()} is not callable'); } else { - return ast.toMalType(); + return ast; } } @@ -102,7 +102,7 @@ final replEnv = globalEnv }), 'fn*': MalMacroFunction((List args, Env env) { final first = args.first; - final list = ((first is ListLike ? first : null) as ListLike?)?.list; + final list = ((first is MalListBase ? first : null))?.list; if (list == null) { throw UnsupportedError( 'fn* not support ${list.runtimeType}($list) as params', diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index c6d14ae540..b89bed3f01 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -22,7 +22,7 @@ final Map ns = { (List args, Env env) => MalBool(args.first is MalList), ), 'empty?': MalFunction( - (List args, Env env) => MalBool((args.first as ListLike).isEmpty), + (List args, Env env) => MalBool((args.first as MalListBase).isEmpty), ), 'count': MalFunction((List args, Env env) { switch (args.first) { @@ -39,7 +39,7 @@ final Map ns = { } }), '=': MalFunction((List args, Env env) { - return MalBool((args.first, args.second).equals()); + return MalBool(args.first == args.second); }), '>': MalFunction( (List args, Env env) => diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index e22454ad98..31c49cb585 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -48,6 +48,17 @@ class Token { String get tokenIndicator => start >= 0 ? ('$input\n${' ' * start}^${'~' * (end - start - 1)}') : ''; + + @override + bool operator ==(covariant Token other) { + return str == other.str && + input == other.input && + start == other.start && + end == other.end; + } + + @override + int get hashCode => Object.hashAll([str, input, start, end]); } List tokenize(String str) { diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 6e597dab06..b862ba716c 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'package:mal/mal.dart'; +import 'package:meta/meta.dart'; abstract class ParserError extends Error { final String? message; @@ -31,102 +32,115 @@ class NotCallableError extends ParserError { NotCallableError(super.message); } -sealed class MalType { +sealed class MalType { + final T val; + MalType(this.val); String toStr([bool printReadably = false]); + @override + @mustBeOverridden + bool operator ==(covariant MalType other); + + @override + @mustBeOverridden + int get hashCode; } extension MalTypeAs on MalType { int asInt() => (this as MalInt).val; - List asList() => (this as ListLike).list; } extension Second on List { MalType get second => this[1]; } -extension Equals on (MalType left, MalType right) { - MalType get first => this.$1; - MalType get second => this.$2; - bool equals() { - if (this.$1.runtimeType != this.$2.runtimeType) { - if (!(this.$1 is ListLike && this.$2 is ListLike)) { - return false; - } - } - final eq = switch (first) { - MalInt() => sameTypeEquals((a) => (a as MalInt).val), - MalNil() => true, - MalBool() => sameTypeEquals((a) => (a as MalBool).val), - MalKeyword() => sameTypeEquals((a) => (a as MalKeyword).val), - MalString() => sameTypeEquals((a) => (a as MalString).val), - MalMap() => throw UnimplementedError(), - MalSymbol() => throw UnimplementedError(), - MalSymbolNotFound() => throw UnimplementedError(), - MalFunction() => throw UnimplementedError(), - MalMacroFunction() => throw UnimplementedError(), - MalClosure() => throw UnimplementedError(), - MalList() => listEqual(), - MalVector() => listEqual(), - }; - return eq; - } +class MalInt extends MalType { + MalInt(super.val); + @override + String toStr([bool printReadably = false]) => val.toString(); - bool sameTypeEquals(T Function(MalType) getVal) => - getVal(first) == getVal(second); - bool listEqual() { - var a = first.asList(); - var b = second.asList(); - if (a.length != b.length) { + @override + bool operator ==(covariant MalType other) { + if (other is! MalInt) { return false; } - for (var i = 0; i < a.length; i++) { - final eq = (a[i], b[i]).equals(); - if (!eq) return false; - } - return true; + return val == other.val; } -} -class MalInt extends MalType { - final int _val; - MalInt(this._val); @override - String toStr([bool printReadably = false]) => _val.toString(); - - int get val => _val; + int get hashCode => val.hashCode; } -class MalNil extends MalType { +class MalNil extends MalType { + MalNil() : super(null); @override String toStr([bool printReadably = false]) => 'nil'; + @override + bool operator ==(covariant MalType other) { + if (other.runtimeType != MalNil) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; } -class MalBool extends MalType { - final bool val; - MalBool(this.val); +class MalBool extends MalType { + MalBool(super.val); @override String toStr([bool printReadably = false]) => val ? 'true' : 'false'; + + @override + bool operator ==(covariant MalType other) { + if (other is! MalBool) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; } -class MalKeyword extends MalType { - final String val; - MalKeyword(String val) : val = '\u029E$val'; +class MalKeyword extends MalType { + MalKeyword(String val) : super('\u029E$val'); @override String toStr([bool printReadably = false]) => ':${val.substring(1)}'; + + @override + bool operator ==(covariant MalType other) { + if (other is! MalKeyword) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; } -class MalString extends MalType { - final String val; - MalString(String str) - : val = str, - assert(() { - debugPrint('make MalString($str)'); +class MalString extends MalType { + MalString(super.val) + : assert(() { + debugPrint('make MalString($val)'); return true; }()); @override String toStr([bool printReadably = false]) => printReadably == true ? '"${val.toPrintable()}"' : val; + + @override + bool operator ==(covariant MalType other) { + if (other is! MalString) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; } extension type const Parentheses._((String, String) p) { @@ -155,21 +169,18 @@ enum ParenthesesType { String get right => p.right; } -abstract interface class ListLike { - List get list; - bool get isEmpty => list.isEmpty; - bool get isNotEmpty => list.isNotEmpty; - MalType get first => list.first; - List get args => list.sublist(1); - MalType toMalType() => switch (this) { - MalList() => this as MalList, - MalVector() => this as MalList, - _ => throw UnimplementedError('$runtimeType to MalType'), - }; +bool _listCompare(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (!(a[i] == b[i])) return false; + } + return true; } -class MalList extends ListMixin implements MalType, ListLike { - MalList([List? list]) : _inner = list ?? []; +abstract class MalListBase extends ListMixin implements MalType { + MalListBase([List? list]) : _inner = list ?? []; final List _inner; @override int get length => _inner.length; @@ -190,54 +201,52 @@ class MalList extends ListMixin implements MalType, ListLike { @override Iterable map(T Function(MalType e) f) => _inner.map(f); - @override List get list => _inner; @override void add(MalType element) => _inner.add(element); - @override List get args => _inner.sublist(1); @override - MalType toMalType() => this; -} + List get val => _inner; -class MalVector extends ListBase implements MalType, ListLike { - MalVector([List? list]) : _inner = list ?? []; - final List _inner; @override - int get length => _inner.length; + bool operator ==(covariant MalType other) => listCompare(this, other); @override - set length(int newLength) => _inner.length = newLength; + int get hashCode => val.hashCode; - @override - MalType operator [](int index) => _inner[index]; - - @override - void operator []=(int index, MalType value) => _inner[index] = value; - - @override - String toStr([bool printReadably = false]) => - '[${_inner.map((e) => e.toStr(printReadably)).join(' ')}]'; + bool listCompare(ListBase a, MalType other) { + if (other is ListBase) { + return _listCompare(a, other as ListBase); + } else { + return false; + } + } +} +class MalList extends MalListBase { + MalList([super.list]); @override - void add(MalType element) => _inner.add(element); - @override - Iterable map(T Function(MalType e) f) => _inner.map(f); + bool operator ==(covariant MalType other) => listCompare(this, other); @override - List get list => _inner; + int get hashCode => throw UnimplementedError(); +} +class MalVector extends MalListBase { + MalVector([super.list]); @override - List get args => _inner.sublist(1); + bool operator ==(covariant MalType other) => listCompare(this, other); @override - MalType toMalType() => this; + int get hashCode => throw UnimplementedError(); } -class MalMap with MapMixin implements MalType { +class MalMap + with MapMixin + implements MalType> { MalMap([Map? map]) : _innerMap = map ?? {}; final Map _innerMap; @override @@ -258,31 +267,66 @@ class MalMap with MapMixin implements MalType { @override String toStr([bool printReadably = false]) => '{${_innerMap.entries.map((kv) => '${kv.key} ${kv.value.toStr(printReadably)}').join(' ')}}'; + + @override + Map get val => _innerMap; + + @override + bool operator ==(covariant MalType other) { + if (other is! MalMap) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => throw UnimplementedError(); } -class MalSymbol extends MalType { - final String name; +class MalSymbol extends MalType { + String get name => super.val; final Token? token; - MalSymbol(this.token) : name = token!.str; - MalSymbol.builtin(this.name) : token = null; + MalSymbol(this.token) : super(token!.str); + MalSymbol.builtin(super.val) : token = null; @override String toStr([bool printReadably = false]) => name; bool get isBuiltin => token == null; + + @override + bool operator ==(covariant MalType other) { + if (other is! MalSymbol) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => Object.hashAll([MalSymbol, val]); } -class MalSymbolNotFound extends MalType { - final Token token; - MalSymbolNotFound(this.token); +class MalSymbolNotFound extends MalType { + MalSymbolNotFound(super.val); @override String toStr([bool printReadably = false]) => - "'$name not found\n${token.tokenIndicator}"; - String get name => token.str; + "'$name not found\n${super.val.tokenIndicator}"; + String get name => super.val.str; Error makeError() => KeyNotFoundError(toStr()); + + @override + bool operator ==(covariant MalType other) { + if (other is! MalSymbolNotFound) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; } -class MalFunction extends MalType { - final Function fn; - MalFunction(this.fn); +class MalFunction extends MalType { + Function get fn => super.val; + MalFunction(super.val); @override String toStr([bool printReadably = false]) => fn.toString(); @@ -301,12 +345,23 @@ class MalFunction extends MalType { 'funcion type ${fn.runtimeType} is not implemented', ); } + + @override + bool operator ==(covariant MalType other) { + if (other is! MalFunction) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; } /// without eval args -class MalMacroFunction extends MalType { - final Function fn; - MalMacroFunction(this.fn); +class MalMacroFunction extends MalType { + Function get fn => super.val; + MalMacroFunction(super.val); @override String toStr([bool printReadably = false]) => fn.toString(); @@ -318,13 +373,24 @@ class MalMacroFunction extends MalType { 'funcion type ${fn.runtimeType} is not implemented', ); } + + @override + bool operator ==(covariant MalType other) { + if (other is! MalMacroFunction) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; } -class MalClosure extends MalType { - final Function fn; +class MalClosure extends MalType { + Function get fn => super.val; final List params; final Env env; - MalClosure(this.params, this.env, this.fn); + MalClosure(this.params, this.env, super.val); MalType call(List args) { if (fn is MalType Function(List args)) { @@ -337,4 +403,17 @@ class MalClosure extends MalType { @override String toStr([bool printReadably = false]) => '#'; + + @override + bool operator ==(covariant MalType other) { + if (other is! MalClosure) { + return false; + } + return val == other.val && + env == other.env && + _listCompare(params, other.params); + } + + @override + int get hashCode => val.hashCode; } diff --git a/impls/dart3/pubspec.lock b/impls/dart3/pubspec.lock index 14bd551fc0..0ad5be80a3 100644 --- a/impls/dart3/pubspec.lock +++ b/impls/dart3/pubspec.lock @@ -162,7 +162,7 @@ packages: source: hosted version: "0.12.20" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739 diff --git a/impls/dart3/pubspec.yaml b/impls/dart3/pubspec.yaml index c55882027e..8590b143bd 100644 --- a/impls/dart3/pubspec.yaml +++ b/impls/dart3/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: ansicolor: ^2.0.3 collection: ^1.19.1 + meta: ^1.18.2 path: ^1.9.0 dev_dependencies: From 420132d48df39b0ff296590f6dd00713c6bfdd57 Mon Sep 17 00:00:00 2001 From: Krysl Date: Mon, 4 May 2026 15:27:48 +0800 Subject: [PATCH 10/28] dart3: step5 stash before refactor --- impls/dart3/bin/step5_tco.dart | 159 +++++++++++++++++++++++++++++++++ impls/dart3/lib/src/env.dart | 12 +++ impls/dart3/lib/src/types.dart | 25 +++++- 3 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 impls/dart3/bin/step5_tco.dart diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart new file mode 100644 index 0000000000..da3e55eb26 --- /dev/null +++ b/impls/dart3/bin/step5_tco.dart @@ -0,0 +1,159 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); +MalType eval(MalType ast, Env env) { + while (true) { + if (env.debugEval) { + stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); + } + + TCO listCall(MalListBase list, Env env, MalListBase ast) { + if (list.isNotEmpty) { + var fn = eval(list.first, env); + if (fn is MalFunction) { + return fn + .call(list.args.map((e) => eval(e, env)).toList(), env) + .toTCO(); + } else if (fn is MalMacroFunction) { + if (fn.isTCO) { + return fn.callTCO(list.args, env); + } else { + return fn.call(list.args, env).toTCO(); + } + } else if (fn is MalClosure) { + return fn.call(list.args.map((e) => eval(e, env)).toList()).toTCO(); + } else if (fn is MalSymbolNotFound) { + throw fn.makeError(); + } + throw NotCallableError('${fn.toStr()} is not callable'); + } else { + return (ast, null, false); + } + } + + final (maltype, newEnv, conti) = switch (ast) { + final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(), + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ).toTCO(), + final MalMap map => MalMap( + map.map((k, v) => MapEntry(k, eval(v, env))), + ).toTCO(), + final MalList list => listCall(list, env, ast), + _ => ast.toTCO(), + }; + if (newEnv != null) env = newEnv; + if (conti) { + ast = maltype; + continue; + } + if (env.debugEval) { + stdout.writeln('${'EVAL:=>'.toCyan} ${prStr(maltype, true)}'); + } + return maltype; + } +} + +String print(MalType str) => prStr(str, true); + +int evalToInt(MalType a, Env env) { + var val = eval(a, env); + if (val is MalSymbolNotFound) { + throw val.makeError(); + } + return (val as MalInt).val; +} + +final replEnv = globalEnv + ..addAll({ + '+': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) + evalToInt(b, env), + ), + '-': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) - evalToInt(b, env), + ), + '*': MalFunction( + (Env env, MalType a, MalType b) => evalToInt(a, env) * evalToInt(b, env), + ), + '/': MalFunction( + (Env env, MalType a, MalType b) => + (evalToInt(a, env) / evalToInt(b, env)).round(), + ), + 'def!': MalMacroFunction( + 'def!', + (List args, Env env) => + env[(args[0] as MalSymbol).name] = eval(args[1], env), + ), + 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + final newEnv = Env(outer: env); + List first; + if (args.first is MalList) { + first = (args.first as MalList); + } else if (args.first is MalVector) { + first = (args.first as MalVector); + } else { + throw UnsupportedError( + 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); + } + + for (final [key, val] in first.slices(2)) { + newEnv[(key as MalSymbol).name] = eval(val, newEnv); + } + // return eval(args[1], newEnv); + return (args.second, newEnv, true); + }), + 'do': MalMacroFunction.tco('do', (List args, Env env) { + args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList().last; + return (args.last, null, true); + }), + 'if': MalMacroFunction.tco('if', (List args, Env env) { + final br = eval(args.first, env); + if (br is! MalNil && !(br is MalBool && br.val == false)) { + return args[1].toTCO(null, true); + } else if (args.length > 2) { + return args[2].toTCO(null, true); + } else { + return MalNil().toTCO(null, true); + } + }), + 'fn*': MalMacroFunction.tco('fn*', (List args, Env env) { + final first = args.first; + final list = ((first is MalListBase ? first : null))?.list; + if (list == null) { + throw UnsupportedError( + 'fn* not support ${list.runtimeType}($list) as params', + ); + } + final params = List.from(list); + + return MalClosure( + params, + env, + (List fnArgs) => + eval(args.second, Env(outer: env, binds: params, exprs: fnArgs)), + args.second, + ).toTCO(); + }), + ...ns, + }); +String rep(String str) => print(eval(read(str), replEnv)); + +void main(List args) { + while (true) { + stdout.write('user> '.toBlue); + final input = stdin.readLineSync(); + if (input == null) break; + try { + final output = rep(input); + stdout.writeln(output); + } on ParserError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/lib/src/env.dart b/impls/dart3/lib/src/env.dart index 814b8cc04b..f47aa48658 100644 --- a/impls/dart3/lib/src/env.dart +++ b/impls/dart3/lib/src/env.dart @@ -74,6 +74,18 @@ class Env { set debugEval(bool val) => flags['DEBUG-EVAL'] = val; bool get debugStr => flags['DEBUG-STR']; set debugStr(bool val) => flags['DEBUG-STR'] = val; + + int get depth { + int d = 0; + var p = outer; + while (p != null) { + p = p.outer; + d++; + } + return d; + } + @override + String toString() => 'Env($depth)${data.toString()}'; } final globalEnv = Env(); diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index b862ba716c..82641740c8 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -358,12 +358,21 @@ class MalFunction extends MalType { int get hashCode => val.hashCode; } +typedef TCO = (MalType ast, Env? env, bool conti); + +extension ToTCO on MalType { + TCO toTCO([Env? env, bool cont = false]) => (this, env, cont); +} + /// without eval args class MalMacroFunction extends MalType { + final String debugName; Function get fn => super.val; - MalMacroFunction(super.val); + final bool isTCO; + MalMacroFunction(this.debugName, super.val, {bool tco = false}) : isTCO = tco; + MalMacroFunction.tco(this.debugName, super.val) : isTCO = true; @override - String toStr([bool printReadably = false]) => fn.toString(); + String toStr([bool printReadably = false]) => '$debugName ${fn.toString()}'; MalType call(List args, Env env) { if (fn is MalType Function(List args, Env env)) { @@ -374,6 +383,15 @@ class MalMacroFunction extends MalType { ); } + TCO callTCO(List args, Env env) { + if (fn is TCO Function(List args, Env env)) { + return fn(args, env); + } + throw UnimplementedError( + 'funcion "$debugName" type ${fn.runtimeType} is not implemented for tco', + ); + } + @override bool operator ==(covariant MalType other) { if (other is! MalMacroFunction) { @@ -390,7 +408,8 @@ class MalClosure extends MalType { Function get fn => super.val; final List params; final Env env; - MalClosure(this.params, this.env, super.val); + final MalType? ast; + MalClosure(this.params, this.env, Function fn, [this.ast]) : super(fn); MalType call(List args) { if (fn is MalType Function(List args)) { From 40f1d4614edcf04f12db7732ad5dee749492073a Mon Sep 17 00:00:00 2001 From: Krysl Date: Tue, 5 May 2026 07:34:08 +0800 Subject: [PATCH 11/28] dart3: step5 passed --- Makefile.impls | 2 +- impls/dart3/bin/step3_env.dart | 3 +- impls/dart3/bin/step4_if_fn_do.dart | 9 +- impls/dart3/bin/step5_tco.dart | 125 ++++++++++++++++------------ impls/dart3/lib/src/core.dart | 3 +- impls/dart3/lib/src/types.dart | 2 + 6 files changed, 83 insertions(+), 61 deletions(-) diff --git a/Makefile.impls b/Makefile.impls index 2aa1c413ec..fc518b1327 100644 --- a/Makefile.impls +++ b/Makefile.impls @@ -61,7 +61,7 @@ dist_EXCLUDES += guile io julia matlab swift # Extra options to pass to runtest.py bbc-basic_TEST_OPTS = --test-timeout 60 -dart3_TEST_OPTS = --no-pty +dart3_TEST_OPTS = --no-pty --start-timeout 60 --test-timeout 120 guile_TEST_OPTS = --test-timeout 120 io_TEST_OPTS = --test-timeout 120 java-truffle_TEST_OPTS = --start-timeout 30 diff --git a/impls/dart3/bin/step3_env.dart b/impls/dart3/bin/step3_env.dart index f531168310..ea94a37c24 100644 --- a/impls/dart3/bin/step3_env.dart +++ b/impls/dart3/bin/step3_env.dart @@ -62,10 +62,11 @@ final replEnv = Env( (evalToInt(a, env) / evalToInt(b, env)).round(), ), 'def!': MalMacroFunction( + 'def!', (List args, Env env) => env[(args[0] as MalSymbol).name] = eval(args[1], env), ), - 'let*': MalMacroFunction((List args, Env env) { + 'let*': MalMacroFunction('let*', (List args, Env env) { final newEnv = Env(outer: env); List first; if (args.first is MalList) { diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart index 0dc2b68136..3a5640f7cd 100644 --- a/impls/dart3/bin/step4_if_fn_do.dart +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -66,10 +66,11 @@ final replEnv = globalEnv (evalToInt(a, env) / evalToInt(b, env)).round(), ), 'def!': MalMacroFunction( + 'def!', (List args, Env env) => env[(args[0] as MalSymbol).name] = eval(args[1], env), ), - 'let*': MalMacroFunction((List args, Env env) { + 'let*': MalMacroFunction('let*', (List args, Env env) { final newEnv = Env(outer: env); List first; if (args.first is MalList) { @@ -87,10 +88,10 @@ final replEnv = globalEnv } return eval(args[1], newEnv); }), - 'do': MalMacroFunction((List args, Env env) { + 'do': MalMacroFunction('do', (List args, Env env) { return args.map((e) => eval(e, env)).toList().last; }), - 'if': MalMacroFunction((List args, Env env) { + 'if': MalMacroFunction('if', (List args, Env env) { final br = eval(args.first, env); if (br is! MalNil && !(br is MalBool && br.val == false)) { return eval(args[1], env); @@ -100,7 +101,7 @@ final replEnv = globalEnv return MalNil(); } }), - 'fn*': MalMacroFunction((List args, Env env) { + 'fn*': MalMacroFunction('fn*', (List args, Env env) { final first = args.first; final list = ((first is MalListBase ? first : null))?.list; if (list == null) { diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index da3e55eb26..b4bbf1514c 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -7,33 +7,13 @@ import 'package:mal/mal.dart'; MalType read(String str) => readStr(str); MalType eval(MalType ast, Env env) { + int loop = 0; while (true) { + loop++; if (env.debugEval) { - stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); - } - - TCO listCall(MalListBase list, Env env, MalListBase ast) { - if (list.isNotEmpty) { - var fn = eval(list.first, env); - if (fn is MalFunction) { - return fn - .call(list.args.map((e) => eval(e, env)).toList(), env) - .toTCO(); - } else if (fn is MalMacroFunction) { - if (fn.isTCO) { - return fn.callTCO(list.args, env); - } else { - return fn.call(list.args, env).toTCO(); - } - } else if (fn is MalClosure) { - return fn.call(list.args.map((e) => eval(e, env)).toList()).toTCO(); - } else if (fn is MalSymbolNotFound) { - throw fn.makeError(); - } - throw NotCallableError('${fn.toStr()} is not callable'); - } else { - return (ast, null, false); - } + stdout.writeln( + '${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}', + ); } final (maltype, newEnv, conti) = switch (ast) { @@ -44,7 +24,62 @@ MalType eval(MalType ast, Env env) { final MalMap map => MalMap( map.map((k, v) => MapEntry(k, eval(v, env))), ).toTCO(), - final MalList list => listCall(list, env, ast), + final MalList list => + (list.isNotEmpty) + ? (switch (list.first) { + MalSymbol(name: 'if') => switch (eval(list.second, env)) { + MalNil() || MalBool(val: true) => (list.third, null, true), + _ => + list.length > 3 + ? (list.fourth, null, true) + : (MalNil(), null, true), + }, + MalSymbol(name: 'fn*') => + list.second is MalListBase + ? ((params) => MalClosure( + params, + env, + (List fnArgs) => eval( + list.third, + Env(outer: env, binds: params, exprs: fnArgs), + ), + list.third, + ).toTCO())( + List.from( + (list.second as MalListBase).list, + ), + ) + : throw UnsupportedError( + 'fn* not support ${list.runtimeType}($list) as params', + ), + _ => switch (eval(list.first, env)) { + final MalFunction fn => + fn + .call(list.args.map((e) => eval(e, env)).toList(), env) + .toTCO(), + final MalMacroFunction fn => + (fn.isTCO) + ? fn.callTCO(list.args, env) + : fn.call(list.args, env).toTCO(), + final MalClosure fn => + // fn + // .call(list.args.map((e) => eval(e, env)).toList()) + // .toTCO(), + () { + final args = list.args.map((e) => eval(e, env)).toList(); + return ( + fn.ast!, + Env(outer: env, binds: fn.params, exprs: args), + true, + ); + }(), + final MalSymbolNotFound fn => throw fn.makeError(), + final fn => throw NotCallableError( + '${fn.toStr()} is not callable', + ), + }, + }) + : (ast, null, false), _ => ast.toTCO(), }; if (newEnv != null) env = newEnv; @@ -53,7 +88,9 @@ MalType eval(MalType ast, Env env) { continue; } if (env.debugEval) { - stdout.writeln('${'EVAL:=>'.toCyan} ${prStr(maltype, true)}'); + stdout.writeln( + '${loop == 1 ? 'EVAL=>'.toCyan : 'EVAL=>'} ${prStr(maltype, true)}', + ); } return maltype; } @@ -105,40 +142,20 @@ final replEnv = globalEnv for (final [key, val] in first.slices(2)) { newEnv[(key as MalSymbol).name] = eval(val, newEnv); } - // return eval(args[1], newEnv); return (args.second, newEnv, true); }), 'do': MalMacroFunction.tco('do', (List args, Env env) { args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList().last; return (args.last, null, true); }), - 'if': MalMacroFunction.tco('if', (List args, Env env) { - final br = eval(args.first, env); - if (br is! MalNil && !(br is MalBool && br.val == false)) { - return args[1].toTCO(null, true); - } else if (args.length > 2) { - return args[2].toTCO(null, true); - } else { - return MalNil().toTCO(null, true); - } - }), - 'fn*': MalMacroFunction.tco('fn*', (List args, Env env) { - final first = args.first; - final list = ((first is MalListBase ? first : null))?.list; - if (list == null) { - throw UnsupportedError( - 'fn* not support ${list.runtimeType}($list) as params', - ); - } - final params = List.from(list); + 'time': MalMacroFunction('time',(List args, Env env) { + final stopwatch = Stopwatch()..start(); + final ret = eval(args.first, env); + assert(stopwatch.isRunning); + stopwatch.stop(); - return MalClosure( - params, - env, - (List fnArgs) => - eval(args.second, Env(outer: env, binds: params, exprs: fnArgs)), - args.second, - ).toTCO(); + println('time: ${stopwatch.elapsed}'); + return ret; }), ...ns, }); diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index b89bed3f01..d38749291d 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -22,7 +22,8 @@ final Map ns = { (List args, Env env) => MalBool(args.first is MalList), ), 'empty?': MalFunction( - (List args, Env env) => MalBool((args.first as MalListBase).isEmpty), + (List args, Env env) => + MalBool((args.first as MalListBase).isEmpty), ), 'count': MalFunction((List args, Env env) { switch (args.first) { diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 82641740c8..e89d06c34c 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -51,6 +51,8 @@ extension MalTypeAs on MalType { extension Second on List { MalType get second => this[1]; + MalType get third => this[2]; + MalType get fourth => this[3]; } class MalInt extends MalType { From 2d73bab481f429b32b82ac37667a22bd89fbf2ca Mon Sep 17 00:00:00 2001 From: Krysl Date: Tue, 5 May 2026 08:48:44 +0800 Subject: [PATCH 12/28] dart3: all step[0~5] passed --- impls/dart3/lib/src/types.dart | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index e89d06c34c..d91fd2ef2b 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -183,6 +183,7 @@ bool _listCompare(List a, List b) { abstract class MalListBase extends ListMixin implements MalType { MalListBase([List? list]) : _inner = list ?? []; + abstract final ParenthesesType type; final List _inner; @override int get length => _inner.length; @@ -198,7 +199,7 @@ abstract class MalListBase extends ListMixin implements MalType { @override String toStr([bool printReadably = false]) => - '(${_inner.map((e) => e.toStr(printReadably)).join(' ')})'; + '${type.left}${_inner.map((e) => e.toStr(printReadably)).join(' ')}${type.right}'; @override Iterable map(T Function(MalType e) f) => _inner.map(f); @@ -235,6 +236,9 @@ class MalList extends MalListBase { @override int get hashCode => throw UnimplementedError(); + + @override + ParenthesesType get type => .round; } class MalVector extends MalListBase { @@ -244,6 +248,9 @@ class MalVector extends MalListBase { @override int get hashCode => throw UnimplementedError(); + + @override + ParenthesesType get type => .square; } class MalMap @@ -406,13 +413,15 @@ class MalMacroFunction extends MalType { int get hashCode => val.hashCode; } -class MalClosure extends MalType { - Function get fn => super.val; +class MalClosure extends MalType { + @Deprecated('only for step4') + Function get fn => super.val!; final List params; final Env env; final MalType? ast; - MalClosure(this.params, this.env, Function fn, [this.ast]) : super(fn); + MalClosure(this.params, this.env, Function? fn, [this.ast]) : super(fn); + @Deprecated('only for step4') MalType call(List args) { if (fn is MalType Function(List args)) { return fn(args); From 932445bd232d6b8d0823527eefba6ea1a28d3aaa Mon Sep 17 00:00:00 2001 From: Krysl Date: Tue, 5 May 2026 08:54:23 +0800 Subject: [PATCH 13/28] dart3: clean code --- impls/dart3/bin/step5_tco.dart | 36 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index b4bbf1514c..07358d92b1 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -36,15 +36,8 @@ MalType eval(MalType ast, Env env) { }, MalSymbol(name: 'fn*') => list.second is MalListBase - ? ((params) => MalClosure( - params, - env, - (List fnArgs) => eval( - list.third, - Env(outer: env, binds: params, exprs: fnArgs), - ), - list.third, - ).toTCO())( + ? ((params) => + MalClosure(params, env, null, list.third).toTCO())( List.from( (list.second as MalListBase).list, ), @@ -61,18 +54,15 @@ MalType eval(MalType ast, Env env) { (fn.isTCO) ? fn.callTCO(list.args, env) : fn.call(list.args, env).toTCO(), - final MalClosure fn => - // fn - // .call(list.args.map((e) => eval(e, env)).toList()) - // .toTCO(), - () { - final args = list.args.map((e) => eval(e, env)).toList(); - return ( - fn.ast!, - Env(outer: env, binds: fn.params, exprs: args), - true, - ); - }(), + final MalClosure fn => ( + fn.ast!, + Env( + outer: env, + binds: fn.params, + exprs: list.args.map((e) => eval(e, env)).toList(), + ), + true, + ), final MalSymbolNotFound fn => throw fn.makeError(), final fn => throw NotCallableError( '${fn.toStr()} is not callable', @@ -148,12 +138,10 @@ final replEnv = globalEnv args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList().last; return (args.last, null, true); }), - 'time': MalMacroFunction('time',(List args, Env env) { + 'time': MalMacroFunction('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); - assert(stopwatch.isRunning); stopwatch.stop(); - println('time: ${stopwatch.elapsed}'); return ret; }), From 192f10221fb2e4bc2b1d938320b3851a1343d16c Mon Sep 17 00:00:00 2001 From: Krysl Date: Tue, 5 May 2026 09:10:30 +0800 Subject: [PATCH 14/28] dart3: Define 'not' function using mal itself --- impls/dart3/bin/step4_if_fn_do.dart | 1 + impls/dart3/bin/step5_tco.dart | 1 + impls/dart3/lib/src/core.dart | 12 +++--------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart index 3a5640f7cd..bbdd5f3eb9 100644 --- a/impls/dart3/bin/step4_if_fn_do.dart +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -123,6 +123,7 @@ final replEnv = globalEnv String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { + preloading.forEach(rep); while (true) { stdout.write('user> '.toBlue); final input = stdin.readLineSync(); diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index 07358d92b1..b228f9ec27 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -150,6 +150,7 @@ final replEnv = globalEnv String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { + preloading.forEach(rep); while (true) { stdout.write('user> '.toBlue); final input = stdin.readLineSync(); diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index d38749291d..138536ece9 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -58,13 +58,7 @@ final Map ns = { (List args, Env env) => MalBool(args.first.asInt() <= args.second.asInt()), ), - 'not': MalFunction( - (List args, Env env) => MalBool(switch (args.first) { - MalBool(val: final val) => !val, - MalNil() => true, - MalString() => false, - MalInt() => false, - _ => throw UnimplementedError(), - }), - ), }; +const preloading = [ + r'''(def! not (fn* (a) (if a false true)))''', // +]; From ff147d5a475680d499a5a12d9ff913b0fcdb58de Mon Sep 17 00:00:00 2001 From: Krysl Date: Tue, 5 May 2026 10:11:28 +0800 Subject: [PATCH 15/28] dart3: strict closure function type --- impls/dart3/bin/step2_eval.dart | 17 ++++++---- impls/dart3/bin/step3_env.dart | 21 +++++++----- impls/dart3/bin/step4_if_fn_do.dart | 27 ++++++++------- impls/dart3/bin/step5_tco.dart | 21 +++++++----- impls/dart3/lib/src/types.dart | 51 +++++++++++++++-------------- 5 files changed, 76 insertions(+), 61 deletions(-) diff --git a/impls/dart3/bin/step2_eval.dart b/impls/dart3/bin/step2_eval.dart index 327b71e8a2..67c17bd89d 100644 --- a/impls/dart3/bin/step2_eval.dart +++ b/impls/dart3/bin/step2_eval.dart @@ -33,28 +33,31 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -int evalToInt(MalType a, Env env) { +MalInt evalToInt(MalType a, Env env) { var val = eval(a, env); if (val is MalSymbolNotFound) { throw val.makeError(); } - return (val as MalInt).val; + return (val as MalInt); } final replEnv = Env( data: { '+': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) + evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) + evalToInt(args.second, env), ), '-': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) - evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) - evalToInt(args.second, env), ), '*': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) * evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) * evalToInt(args.second, env), ), '/': MalFunction( - (Env env, MalType a, MalType b) => - (evalToInt(a, env) / evalToInt(b, env)).round(), + (List args, Env env) => + evalToInt(args.first, env) / evalToInt(args.second, env), ), }, ); diff --git a/impls/dart3/bin/step3_env.dart b/impls/dart3/bin/step3_env.dart index ea94a37c24..d953a89542 100644 --- a/impls/dart3/bin/step3_env.dart +++ b/impls/dart3/bin/step3_env.dart @@ -38,35 +38,38 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -int evalToInt(MalType a, Env env) { +MalInt evalToInt(MalType a, Env env) { var val = eval(a, env); if (val is MalSymbolNotFound) { throw val.makeError(); } - return (val as MalInt).val; + return (val as MalInt); } final replEnv = Env( data: { '+': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) + evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) + evalToInt(args.second, env), ), '-': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) - evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) - evalToInt(args.second, env), ), '*': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) * evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) * evalToInt(args.second, env), ), '/': MalFunction( - (Env env, MalType a, MalType b) => - (evalToInt(a, env) / evalToInt(b, env)).round(), + (List args, Env env) => + evalToInt(args.first, env) / evalToInt(args.second, env), ), - 'def!': MalMacroFunction( + 'def!': MalMacroFunction.normal( 'def!', (List args, Env env) => env[(args[0] as MalSymbol).name] = eval(args[1], env), ), - 'let*': MalMacroFunction('let*', (List args, Env env) { + 'let*': MalMacroFunction.normal('let*', (List args, Env env) { final newEnv = Env(outer: env); List first; if (args.first is MalList) { diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart index bbdd5f3eb9..cafca2ab1b 100644 --- a/impls/dart3/bin/step4_if_fn_do.dart +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -42,35 +42,38 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -int evalToInt(MalType a, Env env) { +MalInt evalToInt(MalType a, Env env) { var val = eval(a, env); if (val is MalSymbolNotFound) { throw val.makeError(); } - return (val as MalInt).val; + return (val as MalInt); } final replEnv = globalEnv ..addAll({ '+': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) + evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) + evalToInt(args.second, env), ), '-': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) - evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) - evalToInt(args.second, env), ), '*': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) * evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) * evalToInt(args.second, env), ), '/': MalFunction( - (Env env, MalType a, MalType b) => - (evalToInt(a, env) / evalToInt(b, env)).round(), + (List args, Env env) => + evalToInt(args.first, env) / evalToInt(args.second, env), ), - 'def!': MalMacroFunction( + 'def!': MalMacroFunction.normal( 'def!', (List args, Env env) => env[(args[0] as MalSymbol).name] = eval(args[1], env), ), - 'let*': MalMacroFunction('let*', (List args, Env env) { + 'let*': MalMacroFunction.normal('let*', (List args, Env env) { final newEnv = Env(outer: env); List first; if (args.first is MalList) { @@ -88,10 +91,10 @@ final replEnv = globalEnv } return eval(args[1], newEnv); }), - 'do': MalMacroFunction('do', (List args, Env env) { + 'do': MalMacroFunction.normal('do', (List args, Env env) { return args.map((e) => eval(e, env)).toList().last; }), - 'if': MalMacroFunction('if', (List args, Env env) { + 'if': MalMacroFunction.normal('if', (List args, Env env) { final br = eval(args.first, env); if (br is! MalNil && !(br is MalBool && br.val == false)) { return eval(args[1], env); @@ -101,7 +104,7 @@ final replEnv = globalEnv return MalNil(); } }), - 'fn*': MalMacroFunction('fn*', (List args, Env env) { + 'fn*': MalMacroFunction.normal('fn*', (List args, Env env) { final first = args.first; final list = ((first is MalListBase ? first : null))?.list; if (list == null) { diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index b228f9ec27..be93aee02a 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -88,30 +88,33 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -int evalToInt(MalType a, Env env) { +MalInt evalToInt(MalType a, Env env) { var val = eval(a, env); if (val is MalSymbolNotFound) { throw val.makeError(); } - return (val as MalInt).val; + return (val as MalInt); } final replEnv = globalEnv ..addAll({ '+': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) + evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) + evalToInt(args.second, env), ), '-': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) - evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) - evalToInt(args.second, env), ), '*': MalFunction( - (Env env, MalType a, MalType b) => evalToInt(a, env) * evalToInt(b, env), + (List args, Env env) => + evalToInt(args.first, env) * evalToInt(args.second, env), ), '/': MalFunction( - (Env env, MalType a, MalType b) => - (evalToInt(a, env) / evalToInt(b, env)).round(), + (List args, Env env) => + evalToInt(args.first, env) / evalToInt(args.second, env), ), - 'def!': MalMacroFunction( + 'def!': MalMacroFunction.normal( 'def!', (List args, Env env) => env[(args[0] as MalSymbol).name] = eval(args[1], env), @@ -138,7 +141,7 @@ final replEnv = globalEnv args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList().last; return (args.last, null, true); }), - 'time': MalMacroFunction('time', (List args, Env env) { + 'time': MalMacroFunction.normal('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); stopwatch.stop(); diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index d91fd2ef2b..795e37654c 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -60,6 +60,10 @@ class MalInt extends MalType { @override String toStr([bool printReadably = false]) => val.toString(); + MalInt operator +(MalInt other) => MalInt(val + other.val); + MalInt operator -(MalInt other) => MalInt(val - other.val); + MalInt operator *(MalInt other) => MalInt(val * other.val); + MalInt operator /(MalInt other) => MalInt(val ~/ other.val); @override bool operator ==(covariant MalType other) { if (other is! MalInt) { @@ -333,26 +337,18 @@ class MalSymbolNotFound extends MalType { int get hashCode => val.hashCode; } -class MalFunction extends MalType { - Function get fn => super.val; +typedef MalFn = T Function(List args, Env env); +typedef Fn = MalFn; +typedef FnTCO = MalFn; + +class MalFunction extends MalType { + Fn get fn => super.val; MalFunction(super.val); @override String toStr([bool printReadably = false]) => fn.toString(); MalType call(List args, Env env) { - if (fn is int Function(Env, MalType, MalType) && args.length == 2) { - try { - return MalInt(Function.apply(fn, [env, ...args])); - } catch (e) { - debugPrint("error when run fn `$fn`"); - rethrow; - } - } else if (fn is MalType Function(List args, Env env)) { - return fn(args, env); - } - throw UnimplementedError( - 'funcion type ${fn.runtimeType} is not implemented', - ); + return fn(args, env); } @override @@ -374,18 +370,25 @@ extension ToTCO on MalType { } /// without eval args -class MalMacroFunction extends MalType { +class MalMacroFunction extends MalType> { final String debugName; - Function get fn => super.val; + MalFn get fn => super.val; final bool isTCO; - MalMacroFunction(this.debugName, super.val, {bool tco = false}) : isTCO = tco; - MalMacroFunction.tco(this.debugName, super.val) : isTCO = true; + MalMacroFunction._(this.debugName, super.val, {bool tco = false}) + : isTCO = tco; + static MalMacroFunction normal( + String debugName, + Fn val, { + bool tco = false, + }) => MalMacroFunction._(debugName, val); + static MalMacroFunction tco(String debugName, FnTCO val) => + ._(debugName, val, tco: true); @override String toStr([bool printReadably = false]) => '$debugName ${fn.toString()}'; - MalType call(List args, Env env) { - if (fn is MalType Function(List args, Env env)) { - return fn(args, env); + R call(List args, Env env) { + if (fn is Fn) { + return fn(args, env) as R; } throw UnimplementedError( 'funcion type ${fn.runtimeType} is not implemented', @@ -393,8 +396,8 @@ class MalMacroFunction extends MalType { } TCO callTCO(List args, Env env) { - if (fn is TCO Function(List args, Env env)) { - return fn(args, env); + if (fn is FnTCO) { + return fn(args, env) as TCO; } throw UnimplementedError( 'funcion "$debugName" type ${fn.runtimeType} is not implemented for tco', From 48a927e229019413ba9c8b97283e06492da346aa Mon Sep 17 00:00:00 2001 From: Krysl Date: Wed, 6 May 2026 09:42:34 +0800 Subject: [PATCH 16/28] dart3: fix problems when run `make REGRESS=1 "test^dart3^step5"` --- Makefile.impls | 2 +- impls/dart3/bin/step5_tco.dart | 33 ++++++++++++----- impls/dart3/lib/mal.dart | 2 ++ impls/dart3/lib/src/core.dart | 42 ++++++++++++++++++++++ impls/dart3/lib/src/env.dart | 23 ++++++++++-- impls/dart3/lib/src/error.dart | 52 +++++++++++++++++++++++++++ impls/dart3/lib/src/types.dart | 41 +++++---------------- impls/dart3/lib/src/utils/logger.dart | 23 ++++++++++++ impls/dart3/pubspec.lock | 16 +++++++++ impls/dart3/pubspec.yaml | 3 +- 10 files changed, 191 insertions(+), 46 deletions(-) create mode 100644 impls/dart3/lib/src/error.dart create mode 100644 impls/dart3/lib/src/utils/logger.dart diff --git a/Makefile.impls b/Makefile.impls index fc518b1327..2aa1c413ec 100644 --- a/Makefile.impls +++ b/Makefile.impls @@ -61,7 +61,7 @@ dist_EXCLUDES += guile io julia matlab swift # Extra options to pass to runtest.py bbc-basic_TEST_OPTS = --test-timeout 60 -dart3_TEST_OPTS = --no-pty --start-timeout 60 --test-timeout 120 +dart3_TEST_OPTS = --no-pty guile_TEST_OPTS = --test-timeout 120 io_TEST_OPTS = --test-timeout 120 java-truffle_TEST_OPTS = --start-timeout 30 diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index be93aee02a..a1eda6aed0 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -6,18 +6,21 @@ import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; MalType read(String str) => readStr(str); +int depth = 0; MalType eval(MalType ast, Env env) { + depth++; int loop = 0; while (true) { loop++; if (env.debugEval) { stdout.writeln( - '${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}', + '${' ' * depth}${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}\n' + '${' ' * (depth + 1) + ' ' * 40}${env.showVars('\n${' ' * (depth + 2) + ' ' * 40}')}', ); } final (maltype, newEnv, conti) = switch (ast) { - final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(), + final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(null, false), MalVector(list: final list) => MalVector( list.map((e) => eval(e, env)).toList(), ).toTCO(), @@ -28,16 +31,24 @@ MalType eval(MalType ast, Env env) { (list.isNotEmpty) ? (switch (list.first) { MalSymbol(name: 'if') => switch (eval(list.second, env)) { - MalNil() || MalBool(val: true) => (list.third, null, true), - _ => + MalBool(val: true) || + MalInt() || + MalString() || + MalList() || + MalVector() => (list.third, null, true), + MalNil() || _ => list.length > 3 ? (list.fourth, null, true) : (MalNil(), null, true), }, MalSymbol(name: 'fn*') => list.second is MalListBase - ? ((params) => - MalClosure(params, env, null, list.third).toTCO())( + ? ((params) => MalClosure( + params, + env, + null, + list.third, + ).toTCO(null, true))( List.from( (list.second as MalListBase).list, ), @@ -57,10 +68,11 @@ MalType eval(MalType ast, Env env) { final MalClosure fn => ( fn.ast!, Env( - outer: env, + outer: fn.env, binds: fn.params, exprs: list.args.map((e) => eval(e, env)).toList(), ), + // fn.env, true, ), final MalSymbolNotFound fn => throw fn.makeError(), @@ -79,9 +91,10 @@ MalType eval(MalType ast, Env env) { } if (env.debugEval) { stdout.writeln( - '${loop == 1 ? 'EVAL=>'.toCyan : 'EVAL=>'} ${prStr(maltype, true)}', + '${' ' * depth}${loop == 1 ? 'EVAL=>'.toCyan : 'EVAL=>'} ${prStr(maltype, true)}', ); } + depth--; return maltype; } } @@ -138,7 +151,9 @@ final replEnv = globalEnv return (args.second, newEnv, true); }), 'do': MalMacroFunction.tco('do', (List args, Env env) { - args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList().last; + if (args.length > 1) { + args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList().last; + } return (args.last, null, true); }), 'time': MalMacroFunction.normal('time', (List args, Env env) { diff --git a/impls/dart3/lib/mal.dart b/impls/dart3/lib/mal.dart index af002831a7..da486dfc16 100644 --- a/impls/dart3/lib/mal.dart +++ b/impls/dart3/lib/mal.dart @@ -3,5 +3,7 @@ export 'src/reader.dart'; export 'src/print.dart'; export 'src/env.dart'; export 'src/core.dart'; +export 'src/error.dart'; export 'src/utils/str_escape.dart'; export 'src/utils/ansi_color.dart'; +export 'src/utils/logger.dart'; diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index 138536ece9..aa8b05078c 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -1,6 +1,34 @@ +import 'dart:io'; + import 'package:mal/mal.dart'; +import 'package:path/path.dart' as p; + +String getName(MalType v) { + switch (v) { + case MalString(val: final str): + case MalSymbolNotFound(name: final str): + return (str); + default: + throw ArgumentInvalidError( + '<${v.runtimeType}>$v is not valid Type, "String" or "invalid symbol" is needed.', + ); + } +} final Map ns = { + 'loglevel': MalFunction((List args, Env env) { + if (args.isEmpty) { + return MalString(currentLogLevel()); + } + + setLogLevel(getName(args.first)); + + return MalNil(); + }), + 'log': MalFunction((List args, Env env) { + logger.log(getLogLevel(getName(args.first)), args.map((e) => e.toStr())); + return MalNil(); + }), 'prn': MalFunction((List args, Env env) { println(args.isNotEmpty ? args.map((e) => prStr(e, true)).join(' ') : ''); return MalNil(); @@ -58,7 +86,21 @@ final Map ns = { (List args, Env env) => MalBool(args.first.asInt() <= args.second.asInt()), ), + 'pwd': MalFunction( + (List args, Env env) => MalString(Directory.current.path), + ), + 'read-string': MalFunction( + (List args, Env env) => readStr((args.first as MalString).val), + ), + 'slurp': MalFunction((List args, Env env) { + var file = File((args.first as MalString).val); + if (!file.existsSync()) { + throw FileNotFoundError(p.normalize(file.absolute.path)); + } + return MalString(file.readAsStringSync()); + }), }; const preloading = [ r'''(def! not (fn* (a) (if a false true)))''', // + r'''(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))''', ]; diff --git a/impls/dart3/lib/src/env.dart b/impls/dart3/lib/src/env.dart index f47aa48658..f5cde974cc 100644 --- a/impls/dart3/lib/src/env.dart +++ b/impls/dart3/lib/src/env.dart @@ -43,13 +43,18 @@ class Env { (binds.length == exprs.length || binds.firstWhereOrNull((e) => e.name == '&') != null), ); + final dep = depth; for (var i = 0; i < binds.length; i++) { final name = binds[i].name; if (name == '&') { this[binds[i + 1].name] = MalList(exprs!.sublist(i)); break; } - this[name] = exprs![i]; + final val = exprs![i]; + this[name] = val; + logger.d( + '${' ' * (dep + 1)}bind $name on <${val.runtimeType}>${val.toStr()}', + ); } } flags = Flags(this); @@ -58,7 +63,12 @@ class Env { void operator []=(String key, MalType val) => data[key] = val; MalType? operator [](String key) => data[key] ?? outer?[key]; - void addAll(Map other) => data.addAll(other); + final List _builtinKeys = ['DEBUG-EVAL', 'not', 'load-file']; + void addAll(Map other) { + _builtinKeys.addAll(other.keys); + data.addAll(other); + } + MalType getSymbolVal(MalSymbol symbol) { if (symbol.isBuiltin) { return symbol; @@ -84,8 +94,17 @@ class Env { } return d; } + @override String toString() => 'Env($depth)${data.toString()}'; + String showVars(String join) { + final joinStr = join; + var varMap = data.entries + .whereNot((kv) => _builtinKeys.contains(kv.key)) + .map((kv) => '${kv.key}:${kv.value}') + .join(joinStr); + return 'Env($depth)${varMap.isNotEmpty ? joinStr : ''}$varMap'; + } } final globalEnv = Env(); diff --git a/impls/dart3/lib/src/error.dart b/impls/dart3/lib/src/error.dart new file mode 100644 index 0000000000..d1f4b91dd9 --- /dev/null +++ b/impls/dart3/lib/src/error.dart @@ -0,0 +1,52 @@ +abstract class MalError extends Error { + final String? message; + MalError(this.message); + @override + String toString() => + (message != null) // + ? '$runtimeType: $message' + : '$runtimeType'; +} + +/* ParserError */ +abstract class ParserError extends MalError { + ParserError(super.message); +} + +class UnexpectedError extends ParserError { + UnexpectedError(super.message); +} + +class UnbalancedBracketsError extends ParserError { + UnbalancedBracketsError([super.message]); + + @override + String get message => '(unbalanced) ${super.message}'; // make test happy +} + +class KeyNotFoundError extends ParserError { + KeyNotFoundError(super.message); +} + +class NotCallableError extends ParserError { + NotCallableError(super.message); +} + +/* IOError */ +abstract class IOError extends MalError { + IOError(super.message); +} + +class FileNotFoundError extends IOError { + final String path; + FileNotFoundError(this.path) : super('File $path is not found'); +} + +/* RuntimeError */ +abstract class RuntimeError extends MalError { + RuntimeError(super.message); +} + +class ArgumentInvalidError extends RuntimeError { + ArgumentInvalidError(super.message); +} diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 795e37654c..28f3a9c492 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -3,35 +3,6 @@ import 'dart:collection'; import 'package:mal/mal.dart'; import 'package:meta/meta.dart'; -abstract class ParserError extends Error { - final String? message; - ParserError(this.message); - @override - String toString() => - (message != null) // - ? '$runtimeType: $message' - : '$runtimeType'; -} - -class UnexpectedError extends ParserError { - UnexpectedError(super.message); -} - -class UnbalancedBracketsError extends ParserError { - UnbalancedBracketsError([super.message]); - - @override - String get message => '(unbalanced) ${super.message}'; // make test happy -} - -class KeyNotFoundError extends ParserError { - KeyNotFoundError(super.message); -} - -class NotCallableError extends ParserError { - NotCallableError(super.message); -} - sealed class MalType { final T val; MalType(this.val); @@ -43,6 +14,9 @@ sealed class MalType { @override @mustBeOverridden int get hashCode; + + @override + String toString() => toStr(); } extension MalTypeAs on MalType { @@ -129,7 +103,7 @@ class MalKeyword extends MalType { class MalString extends MalType { MalString(super.val) : assert(() { - debugPrint('make MalString($val)'); + logger.t('make MalString($val)'); return true; }()); @@ -320,8 +294,9 @@ class MalSymbol extends MalType { class MalSymbolNotFound extends MalType { MalSymbolNotFound(super.val); @override - String toStr([bool printReadably = false]) => - "'$name not found\n${super.val.tokenIndicator}"; + String toStr([bool printReadably = false]) => printReadably + ? "'$name not found\n${super.val.tokenIndicator}" + : "'$name not found"; String get name => super.val.str; Error makeError() => KeyNotFoundError(toStr()); @@ -435,7 +410,7 @@ class MalClosure extends MalType { } @override - String toStr([bool printReadably = false]) => '#'; + String toStr([bool printReadably = false]) => '# ${ast?.toStr()}'; @override bool operator ==(covariant MalType other) { diff --git a/impls/dart3/lib/src/utils/logger.dart b/impls/dart3/lib/src/utils/logger.dart new file mode 100644 index 0000000000..957d481ce4 --- /dev/null +++ b/impls/dart3/lib/src/utils/logger.dart @@ -0,0 +1,23 @@ +import 'package:logger/logger.dart'; + +import '../error.dart'; + +final logger = Logger(); + +final levelNames = Map.fromEntries( + Level.values.map((e) => MapEntry(e.name, e)), +); + +void setLogLevel(String lv) => Logger.level = getLogLevel(lv); + +Level getLogLevel(String lv) { + if (levelNames.containsKey(lv)) { + return levelNames[lv]!; + } else { + throw ArgumentInvalidError( + '$lv is not valid, acceptable parameters are ${levelNames.keys}.', + ); + } +} + +String currentLogLevel() => Logger.level.name; diff --git a/impls/dart3/pubspec.lock b/impls/dart3/pubspec.lock index 0ad5be80a3..5e488a386b 100644 --- a/impls/dart3/pubspec.lock +++ b/impls/dart3/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" collection: dependency: "direct main" description: @@ -145,6 +153,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" logging: dependency: transitive description: diff --git a/impls/dart3/pubspec.yaml b/impls/dart3/pubspec.yaml index 8590b143bd..aa671bc0bb 100644 --- a/impls/dart3/pubspec.yaml +++ b/impls/dart3/pubspec.yaml @@ -10,8 +10,9 @@ environment: dependencies: ansicolor: ^2.0.3 collection: ^1.19.1 + logger: ^2.7.0 meta: ^1.18.2 - path: ^1.9.0 + path: ^1.9.1 dev_dependencies: lints: ^6.0.0 From e7d47000ad6a0614f69839fe13226a47f99340e1 Mon Sep 17 00:00:00 2001 From: Krysl Date: Wed, 6 May 2026 14:39:50 +0800 Subject: [PATCH 17/28] dart3: step6 passed --- impls/dart3/bin/step5_tco.dart | 4 +- impls/dart3/bin/step6_file.dart | 205 +++++++++++++++++++++++ impls/dart3/lib/src/core.dart | 66 +++++++- impls/dart3/lib/src/env.dart | 8 +- impls/dart3/lib/src/error.dart | 5 +- impls/dart3/lib/src/reader.dart | 4 + impls/dart3/lib/src/types.dart | 80 +++++++-- impls/dart3/lib/src/utils/logger.dart | 17 +- impls/dart3/tool/generate_mal_tests.dart | 131 +++++++++------ 9 files changed, 438 insertions(+), 82 deletions(-) create mode 100644 impls/dart3/bin/step6_file.dart diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index a1eda6aed0..42669e9be6 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -20,7 +20,7 @@ MalType eval(MalType ast, Env env) { } final (maltype, newEnv, conti) = switch (ast) { - final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(null, false), + final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(), MalVector(list: final list) => MalVector( list.map((e) => eval(e, env)).toList(), ).toTCO(), @@ -176,7 +176,7 @@ void main(List args) { try { final output = rep(input); stdout.writeln(output); - } on ParserError catch (e) { + } on MalError catch (e) { stdout.writeln(e.toString()); } } diff --git a/impls/dart3/bin/step6_file.dart b/impls/dart3/bin/step6_file.dart new file mode 100644 index 0000000000..0592c05f1c --- /dev/null +++ b/impls/dart3/bin/step6_file.dart @@ -0,0 +1,205 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); +int depth = 0; +MalType eval(MalType ast, Env env) { + depth++; + int loop = 0; + while (true) { + loop++; + if (env.debugEval) { + stdout.writeln( + '${' ' * depth}${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}\n' + '${' ' * (depth + 1) + ' ' * 40}${env.showVars('\n${' ' * (depth + 2) + ' ' * 40}')}', + ); + } + + final (maltype, newEnv, conti) = switch (ast) { + final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(), + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ).toTCO(), + final MalMap map => MalMap( + map.map((k, v) => MapEntry(k, eval(v, env))), + ).toTCO(), + final MalList list => + (list.isNotEmpty) + ? (switch (list.first) { + MalSymbol(name: 'if') => switch (eval(list.second, env)) { + MalBool(val: true) || + MalInt() || + MalString() || + MalList() || + MalVector() => (list.third, null, true), + MalNil() || _ => + list.length > 3 + ? (list.fourth, null, true) + : (MalNil(), null, true), + }, + MalSymbol(name: 'fn*') => + list.second is MalListBase + ? ((params) => MalClosure( + params, + env, + (List fnArgs) => eval( + list.third, + Env(outer: env, binds: params, exprs: fnArgs), + ), + list.third, + ).toTCO(null, true))( + List.from( + (list.second as MalListBase).list, + ), + ) + : throw UnsupportedError( + 'fn* not support ${list.runtimeType}($list) as params', + ), + MalSymbol(name: 'do') => () { + list + .sublist(1, list.length - 1) + .map((e) => eval(e, env)) + .toList(); + return (list.last, null, true); + }(), + _ => switch (eval(list.first, env)) { + final MalFunction fn => + fn + .call(list.args.map((e) => eval(e, env)).toList(), env) + .toTCO(), + final MalMacroFunction fn => + (fn.isTCO) + ? fn.callTCO(list.args, env) + : fn.call(list.args, env).toTCO(), + final MalClosure fn => ( + fn.ast!, + Env( + outer: fn.env, + binds: fn.params, + exprs: list.args.map((e) => eval(e, env)).toList(), + ), + true, + ), + final MalSymbolNotFound fn => throw fn.makeError(), + final fn => throw NotCallableError( + '${fn.toStr()} is not callable', + ), + }, + }) + : (ast, null, false), + _ => ast.toTCO(), + }; + if (newEnv != null) env = newEnv; + if (conti) { + ast = maltype; + continue; + } + if (env.debugEval) { + stdout.writeln( + '${' ' * depth}${loop == 1 ? 'EVAL=>'.toCyan : 'EVAL=>'} ${prStr(ast, true).toYellow}=>${prStr(maltype, true)}', + ); + } + depth--; + return maltype; + } +} + +String print(MalType str) => prStr(str, true); + +MalInt evalToInt(MalType a, Env env) { + var val = eval(a, env); + if (val is MalSymbolNotFound) { + throw val.makeError(); + } + return (val as MalInt); +} + +final replEnv = globalEnv + ..addAll({ + '+': MalFunction( + (List args, Env env) => + evalToInt(args.first, env) + evalToInt(args.second, env), + ), + '-': MalFunction( + (List args, Env env) => + evalToInt(args.first, env) - evalToInt(args.second, env), + ), + '*': MalFunction( + (List args, Env env) => + evalToInt(args.first, env) * evalToInt(args.second, env), + ), + '/': MalFunction( + (List args, Env env) => + evalToInt(args.first, env) / evalToInt(args.second, env), + ), + 'def!': MalMacroFunction.normal( + 'def!', + (List args, Env env) => + env[(args[0] as MalSymbol).name] = eval(args[1], env), + ), + 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + final newEnv = Env(outer: env); + List first; + if (args.first is MalList) { + first = (args.first as MalList); + } else if (args.first is MalVector) { + first = (args.first as MalVector); + } else { + throw UnsupportedError( + 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); + } + + for (final [key, val] in first.slices(2)) { + newEnv[(key as MalSymbol).name] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + // 'do': MalMacroFunction.tco('do', (List args, Env env) { + // args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList(); + // return (args.last, null, true); + // }), + 'time': MalMacroFunction.normal('time', (List args, Env env) { + final stopwatch = Stopwatch()..start(); + final ret = eval(args.first, env); + stopwatch.stop(); + println('time: ${stopwatch.elapsed}'); + return ret; + }), + ...ns, + 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), + }); +String rep(String str) => print(eval(read(str), replEnv)); + +void main(List args) { + preloading.forEach(rep); + if (args.isNotEmpty) { + final filePath = args.first; + if (args.length > 1) { + replEnv['*ARGV*'] = MalList( + args.sublist(1).map((e) => MalString(e)).toList(), + ); + } + var file = File(filePath); + if (file.existsSync()) { + rep('(load-file "$filePath")'); + } + return; + } + while (true) { + stdout.write('user> '.toBlue); + final input = stdin.readLineSync()?.trim(); + if (input == null) break; + if (input.isEmpty) continue; + try { + final output = rep(input); + stdout.writeln(output); + } on MalError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index aa8b05078c..b4f57a5d9e 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -15,10 +15,21 @@ String getName(MalType v) { } } +int getInt(MalType v) { + switch (v) { + case MalInt(val: final val): + return val; + default: + throw ArgumentInvalidError( + '<${v.runtimeType}>$v is not valid Type, "Int" is needed.', + ); + } +} + final Map ns = { 'loglevel': MalFunction((List args, Env env) { if (args.isEmpty) { - return MalString(currentLogLevel()); + return MalString(Logger.level.name); } setLogLevel(getName(args.first)); @@ -26,9 +37,26 @@ final Map ns = { return MalNil(); }), 'log': MalFunction((List args, Env env) { - logger.log(getLogLevel(getName(args.first)), args.map((e) => e.toStr())); + logger.log( + getLogLevelFromName(getName(args.first)), + args.map((e) => e.toStr()), + ); return MalNil(); }), + 'env': MalFunction((List args, Env env) { + int dep = args.isEmpty ? 0 : getInt(args.first); + Env p = env; + while (dep > 0 && p.outer != null) { + p = p.outer!; + } + final ret = p.data; + + return MalMap(ret); + }), + 'type': MalFunction( + (List args, Env env) => + MalString(args.first.runtimeType.toString()), + ), 'prn': MalFunction((List args, Env env) { println(args.isNotEmpty ? args.map((e) => prStr(e, true)).join(' ') : ''); return MalNil(); @@ -93,14 +121,46 @@ final Map ns = { (List args, Env env) => readStr((args.first as MalString).val), ), 'slurp': MalFunction((List args, Env env) { + if (args.first is! MalString) { + throw UnsupportedError('<${args.first.runtimeType}>${args.first.toStr(true)}'); + } var file = File((args.first as MalString).val); if (!file.existsSync()) { throw FileNotFoundError(p.normalize(file.absolute.path)); } return MalString(file.readAsStringSync()); }), + 'atom': MalMacroFunction.normal( + 'atom', + (List args, Env env) => MalAtom(args.first), + ), + 'atom?': MalFunction( + (List args, Env env) => MalBool(args.first is MalAtom), + ), + 'deref': MalFunction( + (List args, Env env) => (args.first as MalAtom).val.ref, + ), + 'reset!': MalFunction((List args, Env env) { + return (args.first as MalAtom).ref = args.second; + }), + 'swap!': MalFunction((List args, Env env) { + var atom = (args.first as MalAtom); + final fn = args.second; + var args2 = [atom.ref, ...args.sublist(2)]; + final result = switch (fn) { + MalClosure() => fn.call(args2), + MalFunction() => fn.call(args2, env), + _ => throw UnimplementedError( + 'fn type ${fn.runtimeType} is not implemented.', + ), + }; + atom.ref = result; + + return result; + }), }; const preloading = [ - r'''(def! not (fn* (a) (if a false true)))''', // + r'''(def! not (fn* (a) (if a false true)))''', r'''(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))''', + r'''(def! *ARGV* (list))''', ]; diff --git a/impls/dart3/lib/src/env.dart b/impls/dart3/lib/src/env.dart index f5cde974cc..3cca2fbcf0 100644 --- a/impls/dart3/lib/src/env.dart +++ b/impls/dart3/lib/src/env.dart @@ -70,12 +70,8 @@ class Env { } MalType getSymbolVal(MalSymbol symbol) { - if (symbol.isBuiltin) { - return symbol; - } else { - final key = symbol.name; - return data[key] ?? outer?[key] ?? MalSymbolNotFound(symbol.token!); - } + final key = symbol.name; + return data[key] ?? outer?[key] ?? MalSymbolNotFound(symbol.token!); } late final Flags flags; diff --git a/impls/dart3/lib/src/error.dart b/impls/dart3/lib/src/error.dart index d1f4b91dd9..73e8ee3bfd 100644 --- a/impls/dart3/lib/src/error.dart +++ b/impls/dart3/lib/src/error.dart @@ -1,3 +1,5 @@ +import 'package:path/path.dart' as p; + abstract class MalError extends Error { final String? message; MalError(this.message); @@ -39,7 +41,8 @@ abstract class IOError extends MalError { class FileNotFoundError extends IOError { final String path; - FileNotFoundError(this.path) : super('File $path is not found'); + FileNotFoundError(this.path) + : super('File $path is not found (cwd:${p.current})'); } /* RuntimeError */ diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index 31c49cb585..66938d8b8a 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -158,6 +158,10 @@ MalType readForm(Reader reader) { return switch (token?.str) { '(' || '[' || '{' => readList(reader, ParenthesesType.fromLeft(token!.str)), "'" || '`' || '~' || '~@' || '@' || '^' => readQuote(token!.str), + String s when s.startsWith(';') => () { + reader.next(); + return readForm(reader); + }(), _ => readAtom(reader), }; } diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 28f3a9c492..e77d378945 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; import 'package:meta/meta.dart'; @@ -66,6 +67,32 @@ class MalNil extends MalType { int get hashCode => val.hashCode; } +class _MalTypeRef { + _MalTypeRef(this.ref); + MalType ref; +} + +class MalAtom extends MalType<_MalTypeRef> { + MalType get ref => val.ref; + set ref(MalType newVal) => val.ref = newVal; + + MalAtom(MalType val) : super(_MalTypeRef(val)); + @override + bool operator ==(covariant MalType other) { + if (other.runtimeType != MalNil) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => throw UnimplementedError(); + + @override + String toStr([bool printReadably = false]) => + '(atom ${val.ref.toStr(printReadably)})'; +} + class MalBool extends MalType { MalBool(super.val); @override @@ -123,6 +150,29 @@ class MalString extends MalType { int get hashCode => val.hashCode; } +class MalComment extends MalType { + MalComment(super.val) + : assert(() { + logger.t('make MalComment($val)'); + return true; + }()); + + @override + String toStr([bool printReadably = false]) => + printReadably == true ? ';${val.toPrintable()}' : val; + + @override + bool operator ==(covariant MalType other) { + if (other is! MalComment) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; +} + extension type const Parentheses._((String, String) p) { const Parentheses(String l, String r) : this._((l, r)); @@ -159,7 +209,8 @@ bool _listCompare(List a, List b) { return true; } -abstract class MalListBase extends ListMixin implements MalType { +abstract class MalListBase extends ListMixin + implements MalType> { MalListBase([List? list]) : _inner = list ?? []; abstract final ParenthesesType type; final List _inner; @@ -190,7 +241,7 @@ abstract class MalListBase extends ListMixin implements MalType { List get args => _inner.sublist(1); @override - List get val => _inner; + List get val => _inner; @override bool operator ==(covariant MalType other) => listCompare(this, other); @@ -233,7 +284,7 @@ class MalVector extends MalListBase { class MalMap with MapMixin - implements MalType> { + implements MalType> { MalMap([Map? map]) : _innerMap = map ?? {}; final Map _innerMap; @override @@ -252,11 +303,17 @@ class MalMap remove(Object? key) => _innerMap.remove(key); @override - String toStr([bool printReadably = false]) => - '{${_innerMap.entries.map((kv) => '${kv.key} ${kv.value.toStr(printReadably)}').join(' ')}}'; + String toStr([bool printReadably = false]) { + if (getLogLevel() <= Level.debug) { + final maxKeyLength = _innerMap.keys.map((e) => e.length).max; + return '{\n\t${_innerMap.entries.map((kv) => '${kv.key}${' ' * (maxKeyLength - kv.key.length)}: ${kv.value.toStr(printReadably)}').join('\n\t')}\n}'; + } else { + return '{${_innerMap.entries.map((kv) => '${kv.key} ${kv.value.toStr(printReadably)}').join(' ')}}'; + } + } @override - Map get val => _innerMap; + Map get val => _innerMap; @override bool operator ==(covariant MalType other) { @@ -277,7 +334,6 @@ class MalSymbol extends MalType { MalSymbol.builtin(super.val) : token = null; @override String toStr([bool printReadably = false]) => name; - bool get isBuiltin => token == null; @override bool operator ==(covariant MalType other) { @@ -393,7 +449,7 @@ class MalMacroFunction extends MalType> { class MalClosure extends MalType { @Deprecated('only for step4') - Function get fn => super.val!; + Function? get fn => super.val; final List params; final Env env; final MalType? ast; @@ -401,8 +457,11 @@ class MalClosure extends MalType { @Deprecated('only for step4') MalType call(List args) { + // if (fn != null) { + + // } else if (fn is MalType Function(List args)) { - return fn(args); + return fn!(args); } throw UnimplementedError( 'funcion type ${fn.runtimeType} is not implemented', @@ -410,7 +469,8 @@ class MalClosure extends MalType { } @override - String toStr([bool printReadably = false]) => '# ${ast?.toStr()}'; + String toStr([bool printReadably = false]) => + '# ${ast?.toStr(printReadably)}'; @override bool operator ==(covariant MalType other) { diff --git a/impls/dart3/lib/src/utils/logger.dart b/impls/dart3/lib/src/utils/logger.dart index 957d481ce4..49f2df1655 100644 --- a/impls/dart3/lib/src/utils/logger.dart +++ b/impls/dart3/lib/src/utils/logger.dart @@ -1,16 +1,25 @@ +import 'dart:io'; + import 'package:logger/logger.dart'; +export 'package:logger/logger.dart'; import '../error.dart'; -final logger = Logger(); +bool get isTest => + Platform.environment.containsKey('FLUTTER_TEST') || + Platform.environment.containsKey('PUB_ALLOW_ANALYTICS'); + +final filter = DevelopmentFilter()..level = Level.error; +final logger = Logger(printer: SimplePrinter(), filter: filter); final levelNames = Map.fromEntries( Level.values.map((e) => MapEntry(e.name, e)), ); -void setLogLevel(String lv) => Logger.level = getLogLevel(lv); +void setLogLevel(String lv) => filter.level = getLogLevelFromName(lv); +Level getLogLevel() => filter.level!; -Level getLogLevel(String lv) { +Level getLogLevelFromName(String lv) { if (levelNames.containsKey(lv)) { return levelNames[lv]!; } else { @@ -19,5 +28,3 @@ Level getLogLevel(String lv) { ); } } - -String currentLogLevel() => Logger.level.name; diff --git a/impls/dart3/tool/generate_mal_tests.dart b/impls/dart3/tool/generate_mal_tests.dart index 5acbfdf8bb..aa417180df 100644 --- a/impls/dart3/tool/generate_mal_tests.dart +++ b/impls/dart3/tool/generate_mal_tests.dart @@ -50,7 +50,8 @@ void main(List args) { p.absolute(options.outputPath ?? _defaultOutputPath(inputPath)), ); final binImport = - options.binImport ?? '../bin/${p.basenameWithoutExtension(inputPath)}.dart'; + options.binImport ?? + '../bin/${p.basenameWithoutExtension(inputPath)}.dart'; final parseResult = parseMalTests(File(inputPath).readAsStringSync()); @@ -77,13 +78,14 @@ void main(List args) { void _autoFixEvalCallForLegacySteps(String outputPath) { final scriptDir = p.dirname(p.fromUri(Platform.script)); final dart3Root = p.normalize(p.join(scriptDir, '..')); - final outputForAnalyze = p.relative(outputPath, from: dart3Root).replaceAll('\\', '/'); - - final analyzeResult = Process.runSync( - 'dart', - ['analyze', outputForAnalyze], - workingDirectory: dart3Root, - ); + final outputForAnalyze = p + .relative(outputPath, from: dart3Root) + .replaceAll('\\', '/'); + + final analyzeResult = Process.runSync('dart', [ + 'analyze', + outputForAnalyze, + ], workingDirectory: dart3Root); final analyzeOutput = '${analyzeResult.stdout}\n${analyzeResult.stderr}'; final hasExtraPositional = analyzeOutput.contains( @@ -107,11 +109,10 @@ void _autoFixEvalCallForLegacySteps(String outputPath) { final after = before.replaceAll(replacementPattern, 'eval(read(f))'); file.writeAsStringSync(after); - final verifyResult = Process.runSync( - 'dart', - ['analyze', outputForAnalyze], - workingDirectory: dart3Root, - ); + final verifyResult = Process.runSync('dart', [ + 'analyze', + outputForAnalyze, + ], workingDirectory: dart3Root); final verifyOutput = '${verifyResult.stdout}\n${verifyResult.stderr}'; final stillHasExtraPositional = verifyOutput.contains( 'Too many positional arguments: 1 expected, but 2 found.', @@ -119,7 +120,8 @@ void _autoFixEvalCallForLegacySteps(String outputPath) { final stillHasUndefinedReplEnv = verifyOutput.contains( "Undefined name 'replEnv'.", ); - final stillHasSameErrors = stillHasExtraPositional || stillHasUndefinedReplEnv; + final stillHasSameErrors = + stillHasExtraPositional || stillHasUndefinedReplEnv; if (!stillHasSameErrors) { stdout.writeln('Auto-fixed eval/replEnv call mismatch in $outputPath'); @@ -177,7 +179,9 @@ ParseResult parseMalTests(String source) { } if (line.startsWith(';')) { - throw FormatException('Unexpected comment syntax at line $lineNumber: $line'); + throw FormatException( + 'Unexpected comment syntax at line $lineNumber: $line', + ); } final groupTitle = normalizeGroupTitle(pendingComments); @@ -276,43 +280,55 @@ String renderTestFile( required String binImport, }) { final sourceRelative = p.posix.normalize( - p.relative(sourceMalPath, from: p.dirname(outputTestPath)).replaceAll('\\', '/'), + p + .relative(sourceMalPath, from: p.dirname(outputTestPath)) + .replaceAll('\\', '/'), ); final buffer = StringBuffer() - ..writeln("import 'package:mal/mal.dart';") - ..writeln("import 'package:test/test.dart';") - ..writeln("import '$binImport';") - ..writeln() - ..writeln('// Generated from $sourceRelative.') - ..writeln() - ..writeln('class _RunResult {') - ..writeln(' const _RunResult({required this.stdoutLines, required this.returnValue});') - ..writeln() - ..writeln(' final List stdoutLines;') - ..writeln(' final String returnValue;') - ..writeln('}') - ..writeln() - ..writeln('Future<_RunResult> _runCase(List forms) async {') - ..writeln(' clearTestOutput();') - ..writeln(' String returnValue = \'nil\';') - ..writeln(' final throws = [];') - ..writeln(' for (final f in forms) {') - ..writeln(' try {') - ..writeln(' returnValue = print(eval(read(f), replEnv));') - ..writeln(' } catch (e) {') - ..writeln(' throws.add(e.toString().replaceAll(\'\\n\', \'\\\\n\'));') - ..writeln(' }') - ..writeln(' }') - ..writeln(' return _RunResult(') - ..writeln(' stdoutLines: [...clearTestOutput(), ...throws],') - ..writeln(' returnValue: returnValue,') - ..writeln(' );') - ..writeln('}') - ..writeln() - ..writeln('void main() {') - ..writeln(' setUpAll(setTestMock);'); + ..writeln(''' +import 'dart:io'; + +import 'package:mal/mal.dart'; +import 'package:test/test.dart'; +import '$binImport'; + +// Generated from $sourceRelative. + +class _RunResult { + const _RunResult({required this.stdoutLines, required this.returnValue}); + + final List stdoutLines; + final String returnValue; +} + +Future<_RunResult> _runCase(List forms) async { + clearTestOutput(); + String returnValue = 'nil'; + final throws = []; + for (final f in forms) { + try { + returnValue = print(eval(read(f), replEnv)); + } catch (e) { + throws.add(e.toString().replaceAll('\\n', '\\\\n')); + } + } + return _RunResult( + stdoutLines: [...clearTestOutput(), ...throws], + returnValue: returnValue, + ); +} +void main() { + setUpAll(() { + try { + preloading.forEach(rep); + } catch (e) { + stderr.writeln(e); + } + setTestMock(); + }); +'''); for (final group in result.groups) { buffer.writeln(' group(${_dartString(group.title)}, () {'); for (final testCase in group.tests) { @@ -336,12 +352,15 @@ String renderTestFile( final formsList = '[${allForms.map(_dartString).join(', ')}]'; buffer.writeln(' final result = await _runCase($formsList);'); if (testCase.expectedStdout.isNotEmpty) { - if (testCase.expectedReturn.isEmpty && testCase.expectedStdout.length == 1) { + if (testCase.expectedReturn.isEmpty && + testCase.expectedStdout.length == 1) { final regex = '^${testCase.expectedStdout[0]}\u0000'.replaceAll( '\u0000', r'$', ); - final regexContainsNewline = testCase.expectedStdout[0].contains(r'\n'); + final regexContainsNewline = testCase.expectedStdout[0].contains( + r'\n', + ); buffer.writeln(' if (result.stdoutLines.isEmpty) {'); if (!regexContainsNewline) { buffer.writeln( @@ -353,7 +372,9 @@ String renderTestFile( buffer.writeln( ' expect(result.stdoutLines[0], matches(RegExp(${_dartString(regex)})));', ); - buffer.writeln(' expect(result.returnValue, equals(${_dartString('nil')}));'); + buffer.writeln( + ' expect(result.returnValue, equals(${_dartString('nil')}));', + ); buffer.writeln(' }'); } else { buffer.writeln( @@ -408,9 +429,9 @@ String _dartString(String value) { class _ArgParser { _ArgParser(List args) - : outputPath = _readFlagValue(args, '--output'), - binImport = _readFlagValue(args, '--bin-import'), - inputPath = _readInputPath(args); + : outputPath = _readFlagValue(args, '--output'), + binImport = _readFlagValue(args, '--bin-import'), + inputPath = _readInputPath(args); final String? outputPath; final String? binImport; @@ -449,4 +470,4 @@ Never _printUsageAndExit(int code) { 'Usage: dart run tool/generate_mal_tests.dart [--output ] [--bin-import ]', ); exit(code); -} \ No newline at end of file +} From dc3ab4c57781fbd20256cb219b447d112bbfc3aa Mon Sep 17 00:00:00 2001 From: Krysl Date: Wed, 6 May 2026 14:59:23 +0800 Subject: [PATCH 18/28] dart3: clean code --- impls/dart3/bin/step2_eval.dart | 29 +---------------------------- impls/dart3/bin/step3_env.dart | 25 +------------------------ impls/dart3/bin/step4_if_fn_do.dart | 24 ------------------------ impls/dart3/bin/step5_tco.dart | 24 ------------------------ impls/dart3/bin/step6_file.dart | 28 ---------------------------- impls/dart3/lib/src/core.dart | 20 +++++++++++++++++++- impls/dart3/lib/src/types.dart | 1 + 7 files changed, 22 insertions(+), 129 deletions(-) diff --git a/impls/dart3/bin/step2_eval.dart b/impls/dart3/bin/step2_eval.dart index 67c17bd89d..e96253e607 100644 --- a/impls/dart3/bin/step2_eval.dart +++ b/impls/dart3/bin/step2_eval.dart @@ -33,34 +33,7 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -MalInt evalToInt(MalType a, Env env) { - var val = eval(a, env); - if (val is MalSymbolNotFound) { - throw val.makeError(); - } - return (val as MalInt); -} - -final replEnv = Env( - data: { - '+': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) + evalToInt(args.second, env), - ), - '-': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) - evalToInt(args.second, env), - ), - '*': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) * evalToInt(args.second, env), - ), - '/': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) / evalToInt(args.second, env), - ), - }, -); +final replEnv = Env(data: ns); String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { diff --git a/impls/dart3/bin/step3_env.dart b/impls/dart3/bin/step3_env.dart index d953a89542..d0acfef16b 100644 --- a/impls/dart3/bin/step3_env.dart +++ b/impls/dart3/bin/step3_env.dart @@ -38,32 +38,8 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -MalInt evalToInt(MalType a, Env env) { - var val = eval(a, env); - if (val is MalSymbolNotFound) { - throw val.makeError(); - } - return (val as MalInt); -} - final replEnv = Env( data: { - '+': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) + evalToInt(args.second, env), - ), - '-': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) - evalToInt(args.second, env), - ), - '*': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) * evalToInt(args.second, env), - ), - '/': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) / evalToInt(args.second, env), - ), 'def!': MalMacroFunction.normal( 'def!', (List args, Env env) => @@ -87,6 +63,7 @@ final replEnv = Env( } return eval(args[1], newEnv); }), + ...ns, }, ); String rep(String str) => print(eval(read(str), replEnv)); diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart index cafca2ab1b..3cdbc605ff 100644 --- a/impls/dart3/bin/step4_if_fn_do.dart +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -42,32 +42,8 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -MalInt evalToInt(MalType a, Env env) { - var val = eval(a, env); - if (val is MalSymbolNotFound) { - throw val.makeError(); - } - return (val as MalInt); -} - final replEnv = globalEnv ..addAll({ - '+': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) + evalToInt(args.second, env), - ), - '-': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) - evalToInt(args.second, env), - ), - '*': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) * evalToInt(args.second, env), - ), - '/': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) / evalToInt(args.second, env), - ), 'def!': MalMacroFunction.normal( 'def!', (List args, Env env) => diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index 42669e9be6..cda714c242 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -101,32 +101,8 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -MalInt evalToInt(MalType a, Env env) { - var val = eval(a, env); - if (val is MalSymbolNotFound) { - throw val.makeError(); - } - return (val as MalInt); -} - final replEnv = globalEnv ..addAll({ - '+': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) + evalToInt(args.second, env), - ), - '-': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) - evalToInt(args.second, env), - ), - '*': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) * evalToInt(args.second, env), - ), - '/': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) / evalToInt(args.second, env), - ), 'def!': MalMacroFunction.normal( 'def!', (List args, Env env) => diff --git a/impls/dart3/bin/step6_file.dart b/impls/dart3/bin/step6_file.dart index 0592c05f1c..e0d61f65ce 100644 --- a/impls/dart3/bin/step6_file.dart +++ b/impls/dart3/bin/step6_file.dart @@ -110,32 +110,8 @@ MalType eval(MalType ast, Env env) { String print(MalType str) => prStr(str, true); -MalInt evalToInt(MalType a, Env env) { - var val = eval(a, env); - if (val is MalSymbolNotFound) { - throw val.makeError(); - } - return (val as MalInt); -} - final replEnv = globalEnv ..addAll({ - '+': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) + evalToInt(args.second, env), - ), - '-': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) - evalToInt(args.second, env), - ), - '*': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) * evalToInt(args.second, env), - ), - '/': MalFunction( - (List args, Env env) => - evalToInt(args.first, env) / evalToInt(args.second, env), - ), 'def!': MalMacroFunction.normal( 'def!', (List args, Env env) => @@ -159,10 +135,6 @@ final replEnv = globalEnv } return (args.second, newEnv, true); }), - // 'do': MalMacroFunction.tco('do', (List args, Env env) { - // args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList(); - // return (args.last, null, true); - // }), 'time': MalMacroFunction.normal('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index b4f57a5d9e..7349beff44 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -73,6 +73,22 @@ final Map ns = { (List args, Env env) => MalString(args.map((e) => prStr(e, false)).join('')), ), + '+': MalFunction( + (List args, Env env) => + args.first.asMalInt() + args.second.asMalInt(), + ), + '-': MalFunction( + (List args, Env env) => + args.first.asMalInt() - args.second.asMalInt(), + ), + '*': MalFunction( + (List args, Env env) => + args.first.asMalInt() * args.second.asMalInt(), + ), + '/': MalFunction( + (List args, Env env) => + args.first.asMalInt() / args.second.asMalInt(), + ), 'list': MalFunction((List args, Env env) => MalList(args)), 'list?': MalFunction( (List args, Env env) => MalBool(args.first is MalList), @@ -122,7 +138,9 @@ final Map ns = { ), 'slurp': MalFunction((List args, Env env) { if (args.first is! MalString) { - throw UnsupportedError('<${args.first.runtimeType}>${args.first.toStr(true)}'); + throw UnsupportedError( + '<${args.first.runtimeType}>${args.first.toStr(true)}', + ); } var file = File((args.first as MalString).val); if (!file.existsSync()) { diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index e77d378945..c37fddf2ae 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -22,6 +22,7 @@ sealed class MalType { extension MalTypeAs on MalType { int asInt() => (this as MalInt).val; + MalInt asMalInt() => this as MalInt; } extension Second on List { From 97bfb558c04bb5c760c5cbd7e505625cf9581d05 Mon Sep 17 00:00:00 2001 From: Krysl Date: Wed, 6 May 2026 18:16:25 +0800 Subject: [PATCH 19/28] dart3: step7 passed --- impls/dart3/bin/step7_quote.dart | 219 ++++++++++++++++++++++++++ impls/dart3/lib/src/core.dart | 19 +++ impls/dart3/lib/src/reader.dart | 6 +- impls/dart3/lib/src/types.dart | 19 ++- impls/dart3/lib/src/utils/logger.dart | 1 + 5 files changed, 257 insertions(+), 7 deletions(-) create mode 100644 impls/dart3/bin/step7_quote.dart diff --git a/impls/dart3/bin/step7_quote.dart b/impls/dart3/bin/step7_quote.dart new file mode 100644 index 0000000000..fe1b03a659 --- /dev/null +++ b/impls/dart3/bin/step7_quote.dart @@ -0,0 +1,219 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); + +MalList qqLoop(MalListBase list) => MalList( + list.isEmpty + ? [] + : [ + MalList(), + ...list.reversed.map((elt) { + return switch (elt) { + final MalList l when l.length == 2 && l.first == spliceUnquote => + MalList([concat, elt.second]), + _ => MalList([cons, quasiquote(elt)]), + }; + }), + ].reduce((combined, curr) => curr..list.add(combined)), +); + +MalType quasiquote(MalType ast) { + final ret = switch (ast) { + final MalList list when list.length == 2 && list.first == unquote => + list.second, + final MalList list => qqLoop(list), + final MalVector v => MalList([ + vec, + // if (v.isNotEmpty) ...(v.first != unquote ? v : v.sublist(1)), + qqLoop(v), + ]), + MalMap() || MalSymbol() => MalList([quote, ast]), + _ => ast, + }; + return ret; +} + +int depth = 0; +MalType eval(MalType ast, Env env) { + depth++; + int loop = 0; + while (true) { + loop++; + if (env.debugEval) { + final a = shouldLog; + // if (loop == 1 || a) + stdout.writeln( + '${a ? ' ' * depth : ''}${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}', + ); + logger.d( + '${' ' * (depth + 1) + ' ' * 40}${env.showVars('\n${' ' * (depth + 2) + ' ' * 40}')}', + ); + } + + final (maltype, newEnv, conti) = switch (ast) { + final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(), + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ).toTCO(), + final MalMap map => MalMap( + map.map((k, v) => MapEntry(k, eval(v, env))), + ).toTCO(), + final MalListBase list => + (list.isNotEmpty) + ? (switch (list.first) { + MalSymbol(name: 'if') => switch (eval(list.second, env)) { + MalBool(val: true) || + MalInt() || + MalString() || + MalList() || + MalVector() => (list.third, null, true), + MalNil() || _ => + list.length > 3 + ? (list.fourth, null, true) + : (MalNil(), null, true), + }, + MalSymbol(name: 'fn*') => + list.second is MalListBase + ? ((params) => MalClosure( + params, + env, + (List fnArgs) => eval( + list.third, + Env(outer: env, binds: params, exprs: fnArgs), + ), + list.third, + ).toTCO(null, true))( + List.from( + (list.second as MalListBase).list, + ), + ) + : throw UnsupportedError( + 'fn* not support ${list.runtimeType}($list) as params', + ), + MalSymbol(name: 'do') => () { + list + .sublist(1, list.length - 1) + .map((e) => eval(e, env)) + .toList(); + return (list.last, null, true); + }(), + MalSymbol(name: 'quote') => () { + return (list.second, null, false); + }(), + MalSymbol(name: 'quasiquote') => () { + final ret = quasiquote(list.second); + logger.d('eval ${ret.toStr(true)}'); + return (ret, null, true); + }(), + _ => switch (eval(list.first, env)) { + final MalFunction fn => + fn + .call(list.args.map((e) => eval(e, env)).toList(), env) + .toTCO(), + final MalMacroFunction fn => + (fn.isTCO) + ? fn.callTCO(list.args, env) + : fn.call(list.args, env).toTCO(), + final MalClosure fn => ( + fn.ast!, + Env( + outer: fn.env, + binds: fn.params, + exprs: list.args.map((e) => eval(e, env)).toList(), + ), + true, + ), + final MalSymbolNotFound fn => throw fn.makeError(), + final fn => throw NotCallableError( + '${fn.toStr()} is not callable', + ), + }, + }) + : (ast, null, false), + _ => ast.toTCO(), + }; + if (newEnv != null) env = newEnv; + if (conti) { + ast = maltype; + continue; + } + logger.d( + '${' ' * depth}${loop == 1 ? 'EVAL=>'.toCyan : 'EVAL=>'} ${prStr(ast, true).toYellow}=>${prStr(maltype, true)}', + ); + depth--; + return maltype; + } +} + +String print(MalType str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction.normal( + 'def!', + (List args, Env env) => + env[(args[0] as MalSymbol).name] = eval(args[1], env), + ), + 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + final newEnv = Env(outer: env); + List first; + if (args.first is MalList) { + first = (args.first as MalList); + } else if (args.first is MalVector) { + first = (args.first as MalVector); + } else { + throw UnsupportedError( + 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); + } + + for (final [key, val] in first.slices(2)) { + newEnv[(key as MalSymbol).name] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + 'time': MalMacroFunction.normal('time', (List args, Env env) { + final stopwatch = Stopwatch()..start(); + final ret = eval(args.first, env); + stopwatch.stop(); + println('time: ${stopwatch.elapsed}'); + return ret; + }), + ...ns, + 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), + }); +String rep(String str) => print(eval(read(str), replEnv)); + +void main(List args) { + preloading.forEach(rep); + if (args.isNotEmpty) { + final filePath = args.first; + if (args.length > 1) { + replEnv['*ARGV*'] = MalList( + args.sublist(1).map((e) => MalString(e)).toList(), + ); + } + var file = File(filePath); + if (file.existsSync()) { + rep('(load-file "$filePath")'); + } + return; + } + while (true) { + stdout.write('user> '.toBlue); + final input = stdin.readLineSync()?.trim(); + if (input == null) break; + if (input.isEmpty) continue; + try { + final output = rep(input); + stdout.writeln(output); + } on MalError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index 7349beff44..428e9d24ae 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; import 'package:path/path.dart' as p; @@ -93,6 +94,24 @@ final Map ns = { 'list?': MalFunction( (List args, Env env) => MalBool(args.first is MalList), ), + 'cons': MalFunction( + (List args, Env env) => MalList([ + args.first, + if (args.length > 1) ...args.second as MalListBase, + ]), + ), + 'concat': MalFunction( + (List args, Env env) => + MalList((List.from(args)).flattenedToList), + ), + 'vec': MalFunction( + (List args, Env env) => args.isNotEmpty + ? (args.first is! MalVector + ? MalVector(List.from(args.first as MalListBase)) + : args.first) + : MalVector(), + ), + 'empty?': MalFunction( (List args, Env env) => MalBool((args.first as MalListBase).isEmpty), diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index 66938d8b8a..64b5f11ebe 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -32,14 +32,14 @@ final re = RegExp( ); class Token { - Token({ + const Token({ required this.str, required this.input, required this.start, required this.end, }); - String str; - String input; + final String str; + final String input; final int start; final int end; diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index c37fddf2ae..c2a0e56299 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -6,7 +6,7 @@ import 'package:meta/meta.dart'; sealed class MalType { final T val; - MalType(this.val); + const MalType(this.val); String toStr([bool printReadably = false]); @override @mustBeOverridden @@ -305,7 +305,7 @@ class MalMap @override String toStr([bool printReadably = false]) { - if (getLogLevel() <= Level.debug) { + if (shouldLog) { final maxKeyLength = _innerMap.keys.map((e) => e.length).max; return '{\n\t${_innerMap.entries.map((kv) => '${kv.key}${' ' * (maxKeyLength - kv.key.length)}: ${kv.value.toStr(printReadably)}').join('\n\t')}\n}'; } else { @@ -332,7 +332,7 @@ class MalSymbol extends MalType { String get name => super.val; final Token? token; MalSymbol(this.token) : super(token!.str); - MalSymbol.builtin(super.val) : token = null; + const MalSymbol.builtin(super.val) : token = null; @override String toStr([bool printReadably = false]) => name; @@ -348,7 +348,18 @@ class MalSymbol extends MalType { int get hashCode => Object.hashAll([MalSymbol, val]); } -class MalSymbolNotFound extends MalType { +extension ToSymbolBuiltin on String { + MalSymbol get sym => MalSymbol.builtin(this); +} + +const quote = MalSymbol.builtin('quote'); +const unquote = MalSymbol.builtin('unquote'); +const concat = MalSymbol.builtin('concat'); +const cons = MalSymbol.builtin('cons'); +const spliceUnquote = MalSymbol.builtin('splice-unquote'); +const vec = MalSymbol.builtin('vec'); + +final class MalSymbolNotFound extends MalType { MalSymbolNotFound(super.val); @override String toStr([bool printReadably = false]) => printReadably diff --git a/impls/dart3/lib/src/utils/logger.dart b/impls/dart3/lib/src/utils/logger.dart index 49f2df1655..4092b89b81 100644 --- a/impls/dart3/lib/src/utils/logger.dart +++ b/impls/dart3/lib/src/utils/logger.dart @@ -18,6 +18,7 @@ final levelNames = Map.fromEntries( void setLogLevel(String lv) => filter.level = getLogLevelFromName(lv); Level getLogLevel() => filter.level!; +bool get shouldLog => filter.level! <= Level.debug; Level getLogLevelFromName(String lv) { if (levelNames.containsKey(lv)) { From c2ed6a5af9f12bb0e8a264c13c87527bba17f34a Mon Sep 17 00:00:00 2001 From: Krysl Date: Wed, 6 May 2026 22:27:48 +0800 Subject: [PATCH 20/28] dart3: step8 passed --- impls/dart3/bin/step8_macros.dart | 226 ++++++++++++++++++++++++++++++ impls/dart3/lib/src/core.dart | 35 ++++- impls/dart3/lib/src/error.dart | 8 ++ impls/dart3/lib/src/types.dart | 42 +++++- 4 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 impls/dart3/bin/step8_macros.dart diff --git a/impls/dart3/bin/step8_macros.dart b/impls/dart3/bin/step8_macros.dart new file mode 100644 index 0000000000..bf6230757d --- /dev/null +++ b/impls/dart3/bin/step8_macros.dart @@ -0,0 +1,226 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); + +MalList qqLoop(MalListBase list) => MalList( + list.isEmpty + ? [] + : [ + MalList(), + ...list.reversed.map((elt) { + return switch (elt) { + final MalList l when l.length == 2 && l.first == spliceUnquote => + MalList([concat, elt.second]), + _ => MalList([cons, quasiquote(elt)]), + }; + }), + ].reduce((combined, curr) => curr..list.add(combined)), +); + +MalType quasiquote(MalType ast) { + final ret = switch (ast) { + final MalList list when list.length == 2 && list.first == unquote => + list.second, + final MalList list => qqLoop(list), + final MalVector v => MalList([vec, qqLoop(v)]), + MalMap() || MalSymbol() => MalList([quote, ast]), + _ => ast, + }; + return ret; +} + +int depth = 0; +MalType eval(MalType ast, Env env) { + depth++; + int loop = 0; + while (true) { + loop++; + if (env.debugEval) { + final a = shouldLog; + // if (loop == 1 || a) + stdout.writeln( + '${a ? ' ' * depth : ''}${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}', + ); + logger.d( + '${' ' * (depth + 1) + ' ' * 40}${env.showVars('\n${' ' * (depth + 2) + ' ' * 40}')}', + ); + } + + final (maltype, newEnv, conti) = switch (ast) { + final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(), + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ).toTCO(), + final MalMap map => MalMap( + map.map((k, v) => MapEntry(k, eval(v, env))), + ).toTCO(), + final MalListBase list => + (list.isNotEmpty) + ? (switch (list.first) { + MalSymbol(name: 'if') => switch (eval(list.second, env)) { + MalBool(val: true) || + MalInt() || + MalString() || + MalList() || + MalVector() => (list.third, null, true), + MalNil() || _ => + list.length > 3 + ? (list.fourth, null, true) + : (MalNil(), null, true), + }, + MalSymbol(name: 'fn*') => + list.second is MalListBase + ? ((params) => MalClosure( + params, + env, + (List fnArgs) => eval( + list.third, + Env(outer: env, binds: params, exprs: fnArgs), + ), + list.third, + ).toTCO(null, true))( + List.from( + list.second.asMalListBase().list, + ), + ) + : throw UnsupportedError( + 'fn* not support ${list.runtimeType}($list) as params', + ), + MalSymbol(name: 'do') => () { + list + .sublist(1, list.length - 1) + .map((e) => eval(e, env)) + .toList(); + return (list.last, null, true); + }(), + MalSymbol(name: 'quote') => () { + return (list.second, null, false); + }(), + MalSymbol(name: 'quasiquote') => () { + final ret = quasiquote(list.second); + logger.d('eval ${ret.toStr(true)}'); + return (ret, null, true); + }(), + MalSymbol(name: 'defmacro!') => () { + final closure = eval(list.third, env) as MalClosure; + var macro = closure.clone(isMacro: true); + env[list.second.malSymbolName] = macro; + return (macro, null, false); + }(), + _ => switch (eval(list.first, env)) { + final MalFunction fn => + fn + .call(list.args.map((e) => eval(e, env)).toList(), env) + .toTCO(), + final MalMacroFunction fn => + (fn.isTCO) + ? fn.callTCO(list.args, env) + : fn.call(list.args, env).toTCO(), + final MalClosure fn when fn.isNotMacro => ( + fn.ast!, + Env( + outer: fn.env, + binds: fn.params, + exprs: list.args.map((e) => eval(e, env)).toList(), + ), + true, + ), + final MalClosure fn => ( + eval( + fn.ast!, + Env( + outer: fn.env, + binds: fn.params, + exprs: list.args, // + ), + ), + env, + true, + ), + final MalSymbolNotFound fn => throw fn.makeError(), + final fn => throw NotCallableError( + '${fn.toStr()} is not callable', + ), + }, + }) + : (ast, null, false), + _ => ast.toTCO(), + }; + if (newEnv != null) env = newEnv; + if (conti) { + ast = maltype; + continue; + } + logger.d( + '${' ' * depth}${loop == 1 ? 'EVAL=>'.toCyan : 'EVAL=>'} ${prStr(ast, true).toYellow}=>${prStr(maltype, true)}', + ); + depth--; + return maltype; + } +} + +String print(MalType str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction.normal( + 'def!', + (List args, Env env) => + env[args[0].malSymbolName] = eval(args[1], env), + ), + 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + final newEnv = Env(outer: env); + MalListBase first = args.first.asMalListBase( + errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + 'time': MalMacroFunction.normal('time', (List args, Env env) { + final stopwatch = Stopwatch()..start(); + final ret = eval(args.first, env); + stopwatch.stop(); + println('time: ${stopwatch.elapsed}'); + return ret; + }), + ...ns, + 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), + }); +String rep(String str) => print(eval(read(str), replEnv)); + +void main(List args) { + preloading.forEach(rep); + if (args.isNotEmpty) { + final filePath = args.first; + if (args.length > 1) { + replEnv['*ARGV*'] = MalList( + args.sublist(1).map((e) => MalString(e)).toList(), + ); + } + var file = File(filePath); + if (file.existsSync()) { + rep('(load-file "$filePath")'); + } + return; + } + while (true) { + stdout.write('user> '.toBlue); + final input = stdin.readLineSync()?.trim(); + if (input == null) break; + if (input.isEmpty) continue; + try { + final output = rep(input); + stdout.writeln(output); + } on MalError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index 428e9d24ae..b186afe10c 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -97,9 +97,30 @@ final Map ns = { 'cons': MalFunction( (List args, Env env) => MalList([ args.first, - if (args.length > 1) ...args.second as MalListBase, + if (args.length > 1) ...args.second.asMalListBase(), ]), ), + 'nth': MalFunction((List args, Env env) { + var list = args.first.asMalListBase(); + var index = args.second.asInt(); + if (index >= list.length) { + throw ArrayOutOfBoundsError(index, 0, list.length - 1); + } + return list[index]; + }), + 'first': MalFunction( + (List args, Env env) => args.first == MalNil() + ? MalNil() + : args.first.asMalListBase().firstOrNull ?? MalNil(), + ), + 'rest': MalFunction((List args, Env env) { + var list = args.first.asMalListBaseOrNil(); + if (list == null || list.length < 2) { + return MalList(); + } + var rest = list.sublist(1); + return MalList(rest); + }), 'concat': MalFunction( (List args, Env env) => MalList((List.from(args)).flattenedToList), @@ -107,14 +128,14 @@ final Map ns = { 'vec': MalFunction( (List args, Env env) => args.isNotEmpty ? (args.first is! MalVector - ? MalVector(List.from(args.first as MalListBase)) + ? MalVector(List.from(args.first.asMalListBase())) : args.first) : MalVector(), ), 'empty?': MalFunction( (List args, Env env) => - MalBool((args.first as MalListBase).isEmpty), + MalBool(args.first.asMalListBase().isEmpty), ), 'count': MalFunction((List args, Env env) { switch (args.first) { @@ -153,7 +174,7 @@ final Map ns = { (List args, Env env) => MalString(Directory.current.path), ), 'read-string': MalFunction( - (List args, Env env) => readStr((args.first as MalString).val), + (List args, Env env) => readStr(args.first.stringVal), ), 'slurp': MalFunction((List args, Env env) { if (args.first is! MalString) { @@ -161,7 +182,7 @@ final Map ns = { '<${args.first.runtimeType}>${args.first.toStr(true)}', ); } - var file = File((args.first as MalString).val); + var file = File(args.first.stringVal); if (!file.existsSync()) { throw FileNotFoundError(p.normalize(file.absolute.path)); } @@ -195,9 +216,13 @@ final Map ns = { return result; }), + 'macro?': MalFunction( + (List args, Env env) => MalBool(args.first.isMacro), + ), }; const preloading = [ r'''(def! not (fn* (a) (if a false true)))''', r'''(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))''', r'''(def! *ARGV* (list))''', + r'''(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))''', ]; diff --git a/impls/dart3/lib/src/error.dart b/impls/dart3/lib/src/error.dart index 73e8ee3bfd..3e2a3b28f1 100644 --- a/impls/dart3/lib/src/error.dart +++ b/impls/dart3/lib/src/error.dart @@ -53,3 +53,11 @@ abstract class RuntimeError extends MalError { class ArgumentInvalidError extends RuntimeError { ArgumentInvalidError(super.message); } + +class ArrayOutOfBoundsError extends RuntimeError { + final int current; + final int start; + final int end; + ArrayOutOfBoundsError(this.current, this.start, this.end) + : super('Index($current) out of range[$start,$end]'); +} diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index c2a0e56299..987b24c293 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -21,8 +21,35 @@ sealed class MalType { } extension MalTypeAs on MalType { + @pragma('vm:prefer-inline') int asInt() => (this as MalInt).val; + + @pragma('vm:prefer-inline') MalInt asMalInt() => this as MalInt; + + @pragma('vm:prefer-inline') + MalListBase asMalListBase({String? errMsg}) => this is MalListBase + ? this as MalListBase + : throw UnsupportedError( + errMsg ?? 'unsupported $runtimeType as Let* \'s first arg', + ); + @pragma('vm:prefer-inline') + MalListBase? asMalListBaseOrNil({String? errMsg}) => this is MalListBase + ? this as MalListBase + : (this is MalNil + ? null + : throw UnsupportedError( + errMsg ?? 'unsupported $runtimeType as Let* \'s first arg', + )); + + @pragma('vm:prefer-inline') + String get malSymbolName => (this as MalSymbol).name; + + @pragma('vm:prefer-inline') + String get stringVal => (this as MalString).val; + + @pragma('vm:prefer-inline') + bool get isMacro => this is MalClosure && (this as MalClosure).isMacro; } extension Second on List { @@ -465,7 +492,14 @@ class MalClosure extends MalType { final List params; final Env env; final MalType? ast; - MalClosure(this.params, this.env, Function? fn, [this.ast]) : super(fn); + final bool isMacro; + MalClosure( + this.params, + this.env, + Function? fn, [ + this.ast, + this.isMacro = false, + ]) : super(fn); @Deprecated('only for step4') MalType call(List args) { @@ -491,9 +525,15 @@ class MalClosure extends MalType { } return val == other.val && env == other.env && + ast == other.ast && + isMacro == other.isMacro && _listCompare(params, other.params); } @override int get hashCode => val.hashCode; + + bool get isNotMacro => !isMacro; + MalClosure clone({bool isMacro = false}) => + MalClosure(params, env, fn, ast, isMacro); } From 97ec4f98d0aed426a2d728e4024412d1fefbe007 Mon Sep 17 00:00:00 2001 From: Krysl Date: Thu, 7 May 2026 10:56:54 +0800 Subject: [PATCH 21/28] dart3: all step[0~9] passed --- impls/dart3/bin/step4_if_fn_do.dart | 2 +- impls/dart3/bin/step5_tco.dart | 2 +- impls/dart3/bin/step6_file.dart | 2 +- impls/dart3/bin/step7_quote.dart | 9 +- impls/dart3/bin/step8_macros.dart | 3 +- impls/dart3/bin/step9_try.dart | 252 ++++++++++++++++++++++++++++ impls/dart3/lib/src/core.dart | 102 ++++++++--- impls/dart3/lib/src/env.dart | 18 +- impls/dart3/lib/src/error.dart | 9 + impls/dart3/lib/src/reader.dart | 9 +- impls/dart3/lib/src/types.dart | 80 ++++++--- 11 files changed, 431 insertions(+), 57 deletions(-) create mode 100644 impls/dart3/bin/step9_try.dart diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart index 3cdbc605ff..72d34b4e23 100644 --- a/impls/dart3/bin/step4_if_fn_do.dart +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -102,7 +102,7 @@ final replEnv = globalEnv String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { - preloading.forEach(rep); + replEnv.preLoading(rep); while (true) { stdout.write('user> '.toBlue); final input = stdin.readLineSync(); diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index cda714c242..3cb677f7ad 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -144,7 +144,7 @@ final replEnv = globalEnv String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { - preloading.forEach(rep); + replEnv.preLoading(rep); while (true) { stdout.write('user> '.toBlue); final input = stdin.readLineSync(); diff --git a/impls/dart3/bin/step6_file.dart b/impls/dart3/bin/step6_file.dart index e0d61f65ce..cf10d1e8e9 100644 --- a/impls/dart3/bin/step6_file.dart +++ b/impls/dart3/bin/step6_file.dart @@ -148,7 +148,7 @@ final replEnv = globalEnv String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { - preloading.forEach(rep); + replEnv.preLoading(rep); if (args.isNotEmpty) { final filePath = args.first; if (args.length > 1) { diff --git a/impls/dart3/bin/step7_quote.dart b/impls/dart3/bin/step7_quote.dart index fe1b03a659..900fa71a51 100644 --- a/impls/dart3/bin/step7_quote.dart +++ b/impls/dart3/bin/step7_quote.dart @@ -27,11 +27,7 @@ MalType quasiquote(MalType ast) { final MalList list when list.length == 2 && list.first == unquote => list.second, final MalList list => qqLoop(list), - final MalVector v => MalList([ - vec, - // if (v.isNotEmpty) ...(v.first != unquote ? v : v.sublist(1)), - qqLoop(v), - ]), + final MalVector v => MalList([vec, qqLoop(v)]), MalMap() || MalSymbol() => MalList([quote, ast]), _ => ast, }; @@ -46,7 +42,6 @@ MalType eval(MalType ast, Env env) { loop++; if (env.debugEval) { final a = shouldLog; - // if (loop == 1 || a) stdout.writeln( '${a ? ' ' * depth : ''}${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}', ); @@ -190,7 +185,7 @@ final replEnv = globalEnv String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { - preloading.forEach(rep); + replEnv.preLoading(rep); if (args.isNotEmpty) { final filePath = args.first; if (args.length > 1) { diff --git a/impls/dart3/bin/step8_macros.dart b/impls/dart3/bin/step8_macros.dart index bf6230757d..ac20d89a61 100644 --- a/impls/dart3/bin/step8_macros.dart +++ b/impls/dart3/bin/step8_macros.dart @@ -42,7 +42,6 @@ MalType eval(MalType ast, Env env) { loop++; if (env.debugEval) { final a = shouldLog; - // if (loop == 1 || a) stdout.writeln( '${a ? ' ' * depth : ''}${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}', ); @@ -197,7 +196,7 @@ final replEnv = globalEnv String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { - preloading.forEach(rep); + replEnv.preLoading(rep); if (args.isNotEmpty) { final filePath = args.first; if (args.length > 1) { diff --git a/impls/dart3/bin/step9_try.dart b/impls/dart3/bin/step9_try.dart new file mode 100644 index 0000000000..c0d7e77bf7 --- /dev/null +++ b/impls/dart3/bin/step9_try.dart @@ -0,0 +1,252 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); + +MalList qqLoop(MalListBase list) => MalList( + list.isEmpty + ? [] + : [ + MalList(), + ...list.reversed.map((elt) { + return switch (elt) { + final MalList l when l.length == 2 && l.first == spliceUnquote => + MalList([concat, elt.second]), + _ => MalList([cons, quasiquote(elt)]), + }; + }), + ].reduce((combined, curr) => curr..list.add(combined)), +); + +MalType quasiquote(MalType ast) { + final ret = switch (ast) { + final MalList list when list.length == 2 && list.first == unquote => + list.second, + final MalList list => qqLoop(list), + final MalVector v => MalList([vec, qqLoop(v)]), + MalMap() || MalSymbol() => MalList([quote, ast]), + _ => ast, + }; + return ret; +} + +int depth = 0; +MalType eval(MalType ast, Env env) { + depth++; + int loop = 0; + while (true) { + loop++; + if (env.debugEval) { + final a = shouldLog; + stdout.writeln( + '${a ? ' ' * depth : ''}${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}', + ); + logger.d( + '${' ' * (depth + 1) + ' ' * 40}${env.showVars('\n${' ' * (depth + 2) + ' ' * 40}')}', + ); + } + + final (maltype, newEnv, conti) = switch (ast) { + final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(), + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ).toTCO(), + final MalMap map => MalMap( + map.map((k, v) => MapEntry(k, eval(v, env))), + ).toTCO(), + final MalListBase list => + (list.isNotEmpty) + ? (switch (list.first) { + MalSymbol(name: 'if') => switch (eval(list.second, env)) { + MalBool(val: true) || + MalInt() || + MalString() || + MalList() || + MalVector() => (list.third, null, true), + MalNil() || _ => + list.length > 3 + ? (list.fourth, null, true) + : (MalNil(), null, true), + }, + MalSymbol(name: 'fn*') => + list.second is MalListBase + ? ((params) => MalClosure( + params, + env, + (List fnArgs) => eval( + list.third, + Env(outer: env, binds: params, exprs: fnArgs), + ), + list.third, + ).toTCO(null, true))( + List.from( + list.second.asMalListBase().list, + ), + ) + : throw UnsupportedError( + 'fn* not support ${list.runtimeType}($list) as params', + ), + MalSymbol(name: 'do') => () { + list + .sublist(1, list.length - 1) + .map((e) => eval(e, env)) + .toList(); + return (list.last, null, true); + }(), + MalSymbol(name: 'quote') => () { + return (list.second, null, false); + }(), + MalSymbol(name: 'quasiquote') => () { + final ret = quasiquote(list.second); + logger.d('eval ${ret.toStr(true)}'); + return (ret, null, true); + }(), + MalSymbol(name: 'defmacro!') => () { + final closure = eval(list.third, env) as MalClosure; + var macro = closure.clone(isMacro: true); + env[list.second.malSymbolName] = macro; + return (macro, null, false); + }(), + MalSymbol(name: 'try*') => () { + try { + final ret = eval(list.second, env); + return (ret, null, false); + } on MalError catch (e) { + if (list.length < 3) { + rethrow; + } + final catcher = list.third.asMalListBase(); + if (catcher.first.malSymbolName != 'catch*') { + throw ArgumentInvalidError( + 'try*/catch* need a form like "(try* A (catch* B C))"', + ); + } + + final newEnv = Env(outer: env); + newEnv[catcher.second.malSymbolName] = + (e is CustomThrowError) + ? e.err + : MalString(e.message ?? e.toString()); + final errProcess = eval(catcher.third, newEnv); + return (errProcess, null, false); + } + }(), + MalSymbol(name: 'throw') => throw CustomThrowError( + eval(list.second, env), + ), + _ => switch (eval(list.first, env)) { + final MalFunction fn => + fn + .call(list.args.map((e) => eval(e, env)).toList(), env) + .toTCO(), + final MalMacroFunction fn => + (fn.isTCO) + ? fn.callTCO(list.args, env) + : fn.call(list.args, env).toTCO(), + final MalClosure fn when fn.isNotMacro => ( + fn.ast!, + Env( + outer: fn.env, + binds: fn.params, + exprs: list.args.map((e) => eval(e, env)).toList(), + ), + true, + ), + final MalClosure fn => ( + eval( + fn.ast!, + Env( + outer: fn.env, + binds: fn.params, + exprs: list.args, // + ), + ), + env, + true, + ), + final MalSymbolNotFound fn => throw fn.makeError(), + final fn => throw NotCallableError( + '${fn.toStr()} is not callable', + ), + }, + }) + : (ast, null, false), + _ => ast.toTCO(), + }; + if (newEnv != null) env = newEnv; + if (conti) { + ast = maltype; + continue; + } + logger.d( + '${' ' * depth}${loop == 1 ? 'EVAL=>'.toCyan : 'EVAL=>'} ${prStr(ast, true).toYellow}=>${prStr(maltype, true)}', + ); + depth--; + return maltype; + } +} + +String print(MalType str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction.normal( + 'def!', + (List args, Env env) => + env[args[0].malSymbolName] = eval(args[1], env), + ), + 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + final newEnv = Env(outer: env); + MalListBase first = args.first.asMalListBase( + errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + 'time': MalMacroFunction.normal('time', (List args, Env env) { + final stopwatch = Stopwatch()..start(); + final ret = eval(args.first, env); + stopwatch.stop(); + println('time: ${stopwatch.elapsed}'); + return ret; + }), + ...ns, + 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), + }); +String rep(String str) => print(eval(read(str), replEnv)); + +void main(List args) { + replEnv.preLoading(rep); + if (args.isNotEmpty) { + final filePath = args.first; + if (args.length > 1) { + replEnv['*ARGV*'] = MalList( + args.sublist(1).map((e) => MalString(e)).toList(), + ); + } + var file = File(filePath); + if (file.existsSync()) { + rep('(load-file "$filePath")'); + } + return; + } + while (true) { + stdout.write('user> '.toBlue); + final input = stdin.readLineSync()?.trim(); + if (input == null) break; + if (input.isEmpty) continue; + try { + final output = rep(input); + stdout.writeln(output); + } on MalError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index b186afe10c..c6c396a6ad 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -7,6 +7,7 @@ import 'package:path/path.dart' as p; String getName(MalType v) { switch (v) { case MalString(val: final str): + case MalSymbol(name: final str): case MalSymbolNotFound(name: final str): return (str); default: @@ -28,7 +29,10 @@ int getInt(MalType v) { } final Map ns = { - 'loglevel': MalFunction((List args, Env env) { + 'loglevel': MalMacroFunction.normal('loglevel', ( + List args, + Env env, + ) { if (args.isEmpty) { return MalString(Logger.level.name); } @@ -50,9 +54,8 @@ final Map ns = { while (dep > 0 && p.outer != null) { p = p.outer!; } - final ret = p.data; - return MalMap(ret); + return MalMap(p.data.map((k, v) => MapEntry(MalString(k), v))); }), 'type': MalFunction( (List args, Env env) => @@ -192,9 +195,6 @@ final Map ns = { 'atom', (List args, Env env) => MalAtom(args.first), ), - 'atom?': MalFunction( - (List args, Env env) => MalBool(args.first is MalAtom), - ), 'deref': MalFunction( (List args, Env env) => (args.first as MalAtom).val.ref, ), @@ -203,24 +203,86 @@ final Map ns = { }), 'swap!': MalFunction((List args, Env env) { var atom = (args.first as MalAtom); - final fn = args.second; - var args2 = [atom.ref, ...args.sublist(2)]; - final result = switch (fn) { - MalClosure() => fn.call(args2), - MalFunction() => fn.call(args2, env), - _ => throw UnimplementedError( - 'fn type ${fn.runtimeType} is not implemented.', + return atom.ref = call(args.second, [atom.ref, ...args.sublist(2)], env); + }), + 'apply': MalFunction( + (args, env) => call( + args.first, + args.sublist(1, args.length - 1)..addAll(args.last.asMalListBase()), + env, + ), + ), + 'map': MalFunction( + (args, env) => MalList( + args.second + .asMalListBase() + .map((e) => call(args.first, [e], env)) + .toList(), + ), + ), + 'throw': MalFunction((args, env) => throw CustomThrowError(args.first)), + 'atom?': isType(), + 'macro?': isType((e) => e.isMacro), + 'symbol': MalFunction((args, env) => args.first.stringVal.sym), + 'symbol?': isType(), + 'nil?': isType(), + 'true?': isType((e) => e.val), + 'false?': isType((e) => !e.val), + 'keyword': MalFunction((args, env) => MalKeyword(args.first.stringVal)), + 'keyword?': isType(), + 'sequential?': isType(), + 'vector': MalFunction((args, env) => MalVector(args)), + 'vector?': isType(), + 'map?': isType(), + 'hash-map': MalFunction( + (args, env) => MalMap( + Map.fromEntries(args.slices(2).map((l) => MapEntry(l.first, l.second))), + ), + ), + 'assoc': MalFunction( + (args, env) => MalMap( + Map.from((args.first as MalMap).val)..addEntries( + args.sublist(1).slices(2).map((l) => MapEntry(l.first, l.second)), ), - }; - atom.ref = result; - - return result; + ), + ), + 'dissoc': MalFunction((args, env) { + var map = Map.from((args.first as MalMap).val); + args.sublist(1).forEach(map.remove); + return MalMap(map); }), - 'macro?': MalFunction( - (List args, Env env) => MalBool(args.first.isMacro), + 'get': MalFunction((args, env) { + if (args.first is MalNil) { + return nil; + } + final map = args.first as MalMap; + final key = args.second; + if (map.containsKey(key)) { + return map[key]!; + } else { + return MalNil(); + } + }), + 'contains?': MalFunction( + (args, env) => MalBool((args.first as MalMap).containsKey(args.second)), ), + 'keys': MalFunction((args, env) => (args.first as MalMap).keys.toMalList()), + 'vals': MalFunction((args, env) => (args.first as MalMap).values.toMalList()), +}; + +MalType call(MalType fn, List args, Env env) => switch (fn) { + final MalFunction fn => fn.call(args, env), + final MalClosure fn => fn.call(args), + _ => throw UnimplementedError(), }; -const preloading = [ + +MalFunction isType([bool Function(T val)? test]) => + MalFunction( + (args, env) => + MalBool(args.first is T && (test?.call(args.first as T) ?? true)), + ); + +final preloading = [ r'''(def! not (fn* (a) (if a false true)))''', r'''(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))''', r'''(def! *ARGV* (list))''', diff --git a/impls/dart3/lib/src/env.dart b/impls/dart3/lib/src/env.dart index 3cca2fbcf0..3e3eca3ad0 100644 --- a/impls/dart3/lib/src/env.dart +++ b/impls/dart3/lib/src/env.dart @@ -26,6 +26,8 @@ class Flags { } } +typedef RepFn = String Function(String str); + class Env { final Env? outer; final Map data; @@ -62,6 +64,8 @@ class Env { void operator []=(String key, MalType val) => data[key] = val; MalType? operator [](String key) => data[key] ?? outer?[key]; + bool containsKey(String key) => + data.containsKey(key) || (outer?.containsKey(key) ?? false); final List _builtinKeys = ['DEBUG-EVAL', 'not', 'load-file']; void addAll(Map other) { @@ -71,7 +75,9 @@ class Env { MalType getSymbolVal(MalSymbol symbol) { final key = symbol.name; - return data[key] ?? outer?[key] ?? MalSymbolNotFound(symbol.token!); + return data[key] ?? + outer?[key] ?? + (throw MalSymbolNotFound(symbol.token!).makeError()); } late final Flags flags; @@ -101,6 +107,16 @@ class Env { .join(joinStr); return 'Env($depth)${varMap.isNotEmpty ? joinStr : ''}$varMap'; } + + void preLoading(RepFn repFn) { + for (var line in preloading) { + try { + repFn(line); + } on KeyNotFoundError catch (_) { + continue; + } + } + } } final globalEnv = Env(); diff --git a/impls/dart3/lib/src/error.dart b/impls/dart3/lib/src/error.dart index 3e2a3b28f1..77e16d13fa 100644 --- a/impls/dart3/lib/src/error.dart +++ b/impls/dart3/lib/src/error.dart @@ -1,5 +1,7 @@ import 'package:path/path.dart' as p; +import 'types.dart'; + abstract class MalError extends Error { final String? message; MalError(this.message); @@ -10,6 +12,13 @@ abstract class MalError extends Error { : '$runtimeType'; } +class CustomThrowError extends MalError { + final MalType err; + CustomThrowError(this.err):super(err.toStr()); + @override + String toString() => 'Error: $message'; +} + /* ParserError */ abstract class ParserError extends MalError { ParserError(super.message); diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index 64b5f11ebe..c65634ccda 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -107,7 +107,7 @@ MalType readList(Reader reader, ParenthesesType p) { .curly => MalMap(), }; bool isKey = true; - String key = ''; + MalType key = nil; while (true) { final peek = reader.peek(); if (peek == null) throw UnexpectedError('unexpecetd EOF'); @@ -122,7 +122,12 @@ MalType readList(Reader reader, ParenthesesType p) { break; case .curly: if (isKey) { - key = peek.str; + final keyStr = peek.str; + if (keyStr.startsWith('"') && keyStr.endsWith('"')) { + key = MalString(keyStr.substring(1, keyStr.length - 1)); + } else if (keyStr.startsWith(':')) { + key = MalKeyword(keyStr.substring(1)); + } } else { reader.next(); (list as MalMap)[key] = readForm(reader); diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 987b24c293..63394b3cd9 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -27,35 +27,52 @@ extension MalTypeAs on MalType { @pragma('vm:prefer-inline') MalInt asMalInt() => this as MalInt; + @pragma('vm:prefer-inline') + MalString asMalString() => this as MalString; + @pragma('vm:prefer-inline') MalListBase asMalListBase({String? errMsg}) => this is MalListBase ? this as MalListBase - : throw UnsupportedError( - errMsg ?? 'unsupported $runtimeType as Let* \'s first arg', + : throw ArgumentError( + errMsg ?? 'unsupported $runtimeType to MalListBase', ); @pragma('vm:prefer-inline') MalListBase? asMalListBaseOrNil({String? errMsg}) => this is MalListBase ? this as MalListBase : (this is MalNil ? null - : throw UnsupportedError( - errMsg ?? 'unsupported $runtimeType as Let* \'s first arg', + : throw ArgumentError( + errMsg ?? 'unsupported $runtimeType to MalListBase or Nil', )); @pragma('vm:prefer-inline') String get malSymbolName => (this as MalSymbol).name; @pragma('vm:prefer-inline') - String get stringVal => (this as MalString).val; + String get stringVal => switch (this) { + final MalString str => str.val, + final MalKeyword kw => ':${kw.val.substring(1)}', + _ => throw ArgumentError(''), + }; @pragma('vm:prefer-inline') bool get isMacro => this is MalClosure && (this as MalClosure).isMacro; } extension Second on List { + @pragma('vm:prefer-inline') MalType get second => this[1]; + @pragma('vm:prefer-inline') MalType get third => this[2]; + @pragma('vm:prefer-inline') MalType get fourth => this[3]; + @pragma('vm:prefer-inline') + MalList toMalList() => MalList(this); +} + +extension ToMalList on Iterable { + @pragma('vm:prefer-inline') + MalList toMalList() => MalList(toList()); } class MalInt extends MalType { @@ -80,7 +97,7 @@ class MalInt extends MalType { } class MalNil extends MalType { - MalNil() : super(null); + const MalNil() : super(null); @override String toStr([bool printReadably = false]) => 'nil'; @override @@ -88,13 +105,15 @@ class MalNil extends MalType { if (other.runtimeType != MalNil) { return false; } - return val == other.val; + return true; } @override - int get hashCode => val.hashCode; + int get hashCode => (MalNil).hashCode; } +const nil = MalNil(); + class _MalTypeRef { _MalTypeRef(this.ref); MalType ref; @@ -311,10 +330,11 @@ class MalVector extends MalListBase { } class MalMap - with MapMixin - implements MalType> { - MalMap([Map? map]) : _innerMap = map ?? {}; - final Map _innerMap; + with MapMixin + implements MalType> { + MalMap([Map? map]) + : _innerMap = map ?? {}; + final Map _innerMap; @override operator [](Object? key) => _innerMap[key]; @@ -325,7 +345,7 @@ class MalMap void clear() => _innerMap.clear(); @override - Iterable get keys => _innerMap.keys; + Iterable get keys => _innerMap.keys; @override remove(Object? key) => _innerMap.remove(key); @@ -333,22 +353,37 @@ class MalMap @override String toStr([bool printReadably = false]) { if (shouldLog) { - final maxKeyLength = _innerMap.keys.map((e) => e.length).max; - return '{\n\t${_innerMap.entries.map((kv) => '${kv.key}${' ' * (maxKeyLength - kv.key.length)}: ${kv.value.toStr(printReadably)}').join('\n\t')}\n}'; + final maxKeyLength = _innerMap.keys.map((e) => e.toStr().length).max; + return '{\n\t${_innerMap.entries.map((kv) => '${kv.key}${' ' * (maxKeyLength - kv.key.toStr().length)}: ${kv.value.toStr(printReadably)}').join('\n\t')}\n}'; } else { - return '{${_innerMap.entries.map((kv) => '${kv.key} ${kv.value.toStr(printReadably)}').join(' ')}}'; + return '{${_innerMap.entries.map((kv) => '${kv.key.toStr(true)} ${kv.value.toStr(printReadably)}').join(' ')}}'; } } @override - Map get val => _innerMap; + Map get val => _innerMap; @override bool operator ==(covariant MalType other) { if (other is! MalMap) { return false; } - return val == other.val; + if (isEmpty && other.isEmpty) { + return true; + } + if (length != other.length) { + return false; + } + for (final MapEntry(:key, :value) in entries) { + if (other.containsKey(key)) { + if (value != other[key]) { + return false; + } + } else { + return false; + } + } + return true; } @override @@ -410,6 +445,7 @@ final class MalSymbolNotFound extends MalType { typedef MalFn = T Function(List args, Env env); typedef Fn = MalFn; typedef FnTCO = MalFn; +typedef MalClosureFn = MalType Function(List args); class MalFunction extends MalType { Fn get fn => super.val; @@ -486,9 +522,9 @@ class MalMacroFunction extends MalType> { int get hashCode => val.hashCode; } -class MalClosure extends MalType { +class MalClosure extends MalType { @Deprecated('only for step4') - Function? get fn => super.val; + MalClosureFn? get fn => super.val; final List params; final Env env; final MalType? ast; @@ -496,7 +532,7 @@ class MalClosure extends MalType { MalClosure( this.params, this.env, - Function? fn, [ + MalClosureFn? fn, [ this.ast, this.isMacro = false, ]) : super(fn); @@ -506,7 +542,7 @@ class MalClosure extends MalType { // if (fn != null) { // } else - if (fn is MalType Function(List args)) { + if (fn is MalClosureFn) { return fn!(args); } throw UnimplementedError( From 2fbb4d3a17acf8960d45ffdf4d57c404c85a9958 Mon Sep 17 00:00:00 2001 From: Krysl Date: Thu, 7 May 2026 14:30:27 +0800 Subject: [PATCH 22/28] dart3: all mal test passed `make REGRESS=1 MAL_IMPL=dart3 "test^mal^stepA"` --- impls/dart3/bin/stepA_mal.dart | 263 +++++++++++++++++++++++++++++++++ impls/dart3/lib/src/core.dart | 65 +++++++- impls/dart3/lib/src/types.dart | 66 +++++++-- impls/dart3/pubspec.lock | 2 +- impls/dart3/pubspec.yaml | 1 + 5 files changed, 381 insertions(+), 16 deletions(-) create mode 100644 impls/dart3/bin/stepA_mal.dart diff --git a/impls/dart3/bin/stepA_mal.dart b/impls/dart3/bin/stepA_mal.dart new file mode 100644 index 0000000000..152e324f58 --- /dev/null +++ b/impls/dart3/bin/stepA_mal.dart @@ -0,0 +1,263 @@ +// ignore_for_file: file_names + +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalType read(String str) => readStr(str); + +MalList qqLoop(MalListBase list) => MalList( + list.isEmpty + ? [] + : [ + MalList(), + ...list.reversed.map((elt) { + return switch (elt) { + final MalList l when l.length == 2 && l.first == spliceUnquote => + MalList([concat, elt.second]), + _ => MalList([cons, quasiquote(elt)]), + }; + }), + ].reduce((combined, curr) => curr..list.add(combined)), +); + +MalType quasiquote(MalType ast) { + final ret = switch (ast) { + final MalList list when list.length == 2 && list.first == unquote => + list.second, + final MalList list => qqLoop(list), + final MalVector v => MalList([vec, qqLoop(v)]), + MalMap() || MalSymbol() => MalList([quote, ast]), + _ => ast, + }; + return ret; +} + +int depth = 0; +MalType eval(MalType ast, Env env) { + depth++; + int loop = 0; + while (true) { + loop++; + if (env.debugEval) { + final a = shouldLog; + stdout.writeln( + '${a ? ' ' * depth : ''}${loop == 1 ? 'EVAL:'.toCyan : 'EVAL:'} ${prStr(ast, true)}', + ); + logger.d( + '${' ' * (depth + 1) + ' ' * 40}${env.showVars('\n${' ' * (depth + 2) + ' ' * 40}')}', + ); + } + + final (maltype, newEnv, conti) = switch (ast) { + final MalSymbol symbol => env.getSymbolVal(symbol).toTCO(), + MalVector(list: final list) => MalVector( + list.map((e) => eval(e, env)).toList(), + ).toTCO(), + final MalMap map => MalMap( + map.map((k, v) => MapEntry(k, eval(v, env))), + ).toTCO(), + final MalListBase list => + (list.isNotEmpty) + ? (switch (list.first) { + MalSymbol(name: 'if') => switch (eval(list.second, env)) { + MalNil() || MalBool(val: false) => + list.length > 3 + ? (list.fourth, null, true) + : (MalNil(), null, true), + _ => (list.third, null, true), + }, + MalSymbol(name: 'fn*') => + list.second is MalListBase + ? ((params) => MalClosure( + params, + env, + (List fnArgs) => eval( + list.third, + Env(outer: env, binds: params, exprs: fnArgs), + ), + list.third, + ).toTCO(null, true))( + List.from( + list.second.asMalListBase().list, + ), + ) + : throw UnsupportedError( + 'fn* not support ${list.runtimeType}($list) as params', + ), + MalSymbol(name: 'do') => () { + list + .sublist(1, list.length - 1) + .map((e) => eval(e, env)) + .toList(); + return (list.last, null, true); + }(), + MalSymbol(name: 'quote') => () { + return (list.second, null, false); + }(), + MalSymbol(name: 'quasiquote') => () { + final ret = quasiquote(list.second); + logger.d('eval ${ret.toStr(true)}'); + return (ret, null, true); + }(), + MalSymbol(name: 'defmacro!') => () { + final closure = eval(list.third, env) as MalClosure; + var macro = closure.clone(isMacro: true); + env[list.second.malSymbolName] = macro; + return (macro, null, false); + }(), + MalSymbol(name: 'try*') => () { + try { + final ret = eval(list.second, env); + return (ret, null, false); + } on MalError catch (e) { + if (list.length < 3) { + rethrow; + } + final catcher = list.third.asMalListBase(); + if (catcher.first.malSymbolName != 'catch*') { + throw ArgumentInvalidError( + 'try*/catch* need a form like "(try* A (catch* B C))"', + ); + } + + final newEnv = Env(outer: env); + newEnv[catcher.second.malSymbolName] = + (e is CustomThrowError) + ? e.err + : MalString(e.message ?? e.toString()); + final errProcess = eval(catcher.third, newEnv); + return (errProcess, null, false); + } + }(), + MalSymbol(name: 'throw') => throw CustomThrowError( + eval(list.second, env), + ), + final first => switch (eval( + first is MalList ? eval(first, env) : first, + env, + )) { + final MalFunction fn => + fn + .call(list.args.map((e) => eval(e, env)).toList(), env) + .toTCO(), + final MalMacroFunction fn => + (fn.isTCO) + ? fn.callTCO(list.args, env) + : fn.call(list.args, env).toTCO(), + final MalClosure fn when fn.isNotMacro => ( + fn.ast!, + Env( + outer: fn.env, + binds: fn.params, + exprs: list.args.map((e) => eval(e, env)).toList(), + ), + true, + ), + final MalClosure fn => ( + eval( + fn.ast!, + Env( + outer: fn.env, + binds: fn.params, + exprs: list.args, // + ), + ), + env, + true, + ), + final MalSymbolNotFound fn => throw fn.makeError(), + final MalSymbol fn => eval(fn, env).toTCO(null, true), + final fn => throw NotCallableError( + '<${fn.runtimeType}>${fn.toStr()} is not callable', + ), + }, + }) + : (ast, null, false), + _ => ast.toTCO(), + }; + if (newEnv != null) env = newEnv; + if (conti) { + ast = maltype; + continue; + } + logger.d( + '${' ' * depth}${loop == 1 ? 'EVAL=>'.toCyan : 'EVAL=>'} ${prStr(ast, true).toYellow}=>${prStr(maltype, true)}', + ); + depth--; + return maltype; + } +} + +String print(MalType str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction.normal( + 'def!', + (List args, Env env) => + env[args[0].malSymbolName] = eval(args[1], env), + ), + 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + final newEnv = Env(outer: env); + MalListBase first = args.first.asMalListBase( + errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + 'time': MalMacroFunction.normal('time', (List args, Env env) { + final stopwatch = Stopwatch()..start(); + final ret = eval(args.first, env); + stopwatch.stop(); + println('time: ${stopwatch.elapsed}'); + return ret; + }), + ...ns, + 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), + '*host-language*': MalString('dart'), + }); +String rep(String str) => print(eval(read(str), replEnv)); + +final argParser = ArgParser()..addFlag('debug', abbr: 'd'); +void main(List args) { + final results = argParser.parse(args); + if (results.flag('debug')) { + replEnv.debugEval = true; + rep('(loglevel debug)'); + } + replEnv.preLoading(rep); + if (args.isNotEmpty) { + final filePath = args.first; + if (args.length > 1) { + replEnv['*ARGV*'] = MalList( + args.sublist(1).map((e) => MalString(e)).toList(), + ); + } + var file = File(filePath); + if (file.existsSync()) { + rep('(load-file "$filePath")'); + } + return; + } + rep(r'''(println (str "Mal [" *host-language* "]"))'''); + while (true) { + stdout.write('user> '.toBlue); + final input = stdin.readLineSync()?.trim(); + if (input == null) break; + if (input.isEmpty) continue; + try { + final output = rep(input); + stdout.writeln(output); + } on MalError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index c6c396a6ad..43bc7462b5 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -135,7 +135,38 @@ final Map ns = { : args.first) : MalVector(), ), - + 'seq': MalFunction((List args, Env env) { + switch (args.first) { + case final MalList list: + if (list.isEmpty) return nil; + return list; + case final MalVector vec: + if (vec.isEmpty) return nil; + return MalList(vec.list); + case final MalString str: + if (str.val.isEmpty) return nil; + return str.val.runes + .map((ch) => MalString(String.fromCharCode(ch))) + .toMalList(); + case nil: + return nil; + default: + throw ArgumentInvalidError( + 'seq can not using on type ${args.first.runtimeType}', + ); + } + }), + 'conj': MalFunction((args, env) { + final first = args.first.asMalListBase(); + final rest = args.sublist(1); + if (first is MalList) { + return MalList([...rest.reversed, ...first]); + } else if (first is MalVector) { + return MalVector([...first, ...rest]); + } else { + throw ArgumentInvalidError(''); + } + }), 'empty?': MalFunction( (List args, Env env) => MalBool(args.first.asMalListBase().isEmpty), @@ -191,12 +222,13 @@ final Map ns = { } return MalString(file.readAsStringSync()); }), - 'atom': MalMacroFunction.normal( - 'atom', - (List args, Env env) => MalAtom(args.first), - ), + 'atom': MalFunction((List args, Env env) => MalAtom(args.first)), 'deref': MalFunction( - (List args, Env env) => (args.first as MalAtom).val.ref, + (List args, Env env) => (args.first is MalAtom) + ? (args.first as MalAtom).val.ref + : throw ArgumentInvalidError( + "type <${args.first.runtimeType}>${args.first.toStr()} is not a subtype of type 'MalAtom' in type cast", + ), ), 'reset!': MalFunction((List args, Env env) { return (args.first as MalAtom).ref = args.second; @@ -234,6 +266,9 @@ final Map ns = { 'vector': MalFunction((args, env) => MalVector(args)), 'vector?': isType(), 'map?': isType(), + 'fn?': isType((e) => e.isMacro == false), + 'string?': isType(), + 'number?': isType(), 'hash-map': MalFunction( (args, env) => MalMap( Map.fromEntries(args.slices(2).map((l) => MapEntry(l.first, l.second))), @@ -268,6 +303,24 @@ final Map ns = { ), 'keys': MalFunction((args, env) => (args.first as MalMap).keys.toMalList()), 'vals': MalFunction((args, env) => (args.first as MalMap).values.toMalList()), + 'readline': MalFunction((args, env) { + stdout.write(args.first.stringVal.toBlue); + final input = stdin.readLineSync()?.trim(); + if (input == null || input.contains(String.fromCharCode(4))) { + logger.d('Ctrl+D'); + return nil; + } + + logger.d('codeUnits: ${input.codeUnits}'); + return MalString(input); + }), + 'time-ms': MalFunction( + (args, env) => MalInt(DateTime.now().millisecondsSinceEpoch), + ), + 'meta': MalFunction((args, env) => (args.first as MalMeta).metadata), + 'with-meta': MalFunction( + (args, env) => (args.first as MalMeta).clone()..metadata = args.second, + ), }; MalType call(MalType fn, List args, Env env) => switch (fn) { diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index 63394b3cd9..addc2c867e 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -256,8 +256,13 @@ bool _listCompare(List a, List b) { return true; } -abstract class MalListBase extends ListMixin - implements MalType> { +abstract interface class MalMeta { + MalType metadata = nil; + T clone(); +} + +abstract class MalListBase extends ListMixin + implements MalType>, MalMeta { MalListBase([List? list]) : _inner = list ?? []; abstract final ParenthesesType type; final List _inner; @@ -305,7 +310,7 @@ abstract class MalListBase extends ListMixin } } -class MalList extends MalListBase { +class MalList extends MalListBase { MalList([super.list]); @override bool operator ==(covariant MalType other) => listCompare(this, other); @@ -315,9 +320,15 @@ class MalList extends MalListBase { @override ParenthesesType get type => .round; + + @override + MalType metadata = nil; + + @override + MalList clone() => MalList(List.from(list)); } -class MalVector extends MalListBase { +class MalVector extends MalListBase { MalVector([super.list]); @override bool operator ==(covariant MalType other) => listCompare(this, other); @@ -327,11 +338,17 @@ class MalVector extends MalListBase { @override ParenthesesType get type => .square; + + @override + MalType metadata = nil; + + @override + MalVector clone() => MalVector(List.from(list)); } class MalMap with MapMixin - implements MalType> { + implements MalType>, MalMeta { MalMap([Map? map]) : _innerMap = map ?? {}; final Map _innerMap; @@ -388,6 +405,12 @@ class MalMap @override int get hashCode => throw UnimplementedError(); + + @override + MalType metadata = nil; + + @override + MalMap clone() => MalMap(Map.from(_innerMap)); } class MalSymbol extends MalType { @@ -447,11 +470,15 @@ typedef Fn = MalFn; typedef FnTCO = MalFn; typedef MalClosureFn = MalType Function(List args); -class MalFunction extends MalType { +mixin MalCallable on MalType {} + +class MalFunction extends MalType + with MalCallable + implements MalMeta { Fn get fn => super.val; MalFunction(super.val); @override - String toStr([bool printReadably = false]) => fn.toString(); + String toStr([bool printReadably = false]) => '${fn.toString()}'; MalType call(List args, Env env) { return fn(args, env); @@ -467,6 +494,12 @@ class MalFunction extends MalType { @override int get hashCode => val.hashCode; + + @override + MalType metadata = nil; + + @override + MalFunction clone() => MalFunction(val); } typedef TCO = (MalType ast, Env? env, bool conti); @@ -476,7 +509,9 @@ extension ToTCO on MalType { } /// without eval args -class MalMacroFunction extends MalType> { +class MalMacroFunction extends MalType> + with MalCallable + implements MalMeta> { final String debugName; MalFn get fn => super.val; final bool isTCO; @@ -520,9 +555,17 @@ class MalMacroFunction extends MalType> { @override int get hashCode => val.hashCode; + + @override + MalType metadata = nil; + + @override + MalMacroFunction clone() => MalMacroFunction._(debugName, val, tco: isTCO); } -class MalClosure extends MalType { +class MalClosure extends MalType + with MalCallable + implements MalMeta { @Deprecated('only for step4') MalClosureFn? get fn => super.val; final List params; @@ -570,6 +613,11 @@ class MalClosure extends MalType { int get hashCode => val.hashCode; bool get isNotMacro => !isMacro; + + @override MalClosure clone({bool isMacro = false}) => MalClosure(params, env, fn, ast, isMacro); + + @override + MalType metadata = nil; } diff --git a/impls/dart3/pubspec.lock b/impls/dart3/pubspec.lock index 5e488a386b..091dc0c17d 100644 --- a/impls/dart3/pubspec.lock +++ b/impls/dart3/pubspec.lock @@ -26,7 +26,7 @@ packages: source: hosted version: "2.0.3" args: - dependency: transitive + dependency: "direct main" description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 diff --git a/impls/dart3/pubspec.yaml b/impls/dart3/pubspec.yaml index aa671bc0bb..6cfc9a75f8 100644 --- a/impls/dart3/pubspec.yaml +++ b/impls/dart3/pubspec.yaml @@ -9,6 +9,7 @@ environment: # Add regular dependencies here. dependencies: ansicolor: ^2.0.3 + args: ^2.7.0 collection: ^1.19.1 logger: ^2.7.0 meta: ^1.18.2 From 09f83d728faadd83179c0811861b143c05a344c5 Mon Sep 17 00:00:00 2001 From: Krysl Date: Thu, 7 May 2026 14:43:13 +0800 Subject: [PATCH 23/28] dart3: fix `(keyword :abc)` test --- impls/dart3/lib/src/types.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index addc2c867e..dfafe42f23 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -51,7 +51,7 @@ extension MalTypeAs on MalType { @pragma('vm:prefer-inline') String get stringVal => switch (this) { final MalString str => str.val, - final MalKeyword kw => ':${kw.val.substring(1)}', + final MalKeyword kw => kw.val.substring(1), _ => throw ArgumentError(''), }; From 4d066eee6586a831fbde4046a0d18b2eb8af38cd Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 8 May 2026 14:57:14 +0800 Subject: [PATCH 24/28] dart3: all docker test passed make DOCKERIZE=1 "test^dart3" make DOCKERIZE=1 MAL_IMPL=dart3 "test^mal" --- Makefile | 12 ++- Makefile.impls | 1 + impls/dart3/Dockerfile | 45 ++++++++++++ impls/dart3/Makefile | 5 ++ impls/dart3/bin/pub_test.dart | 13 ++++ impls/dart3/bin/step0_repl.dart | 2 + impls/dart3/bin/step1_read_print.dart | 1 + impls/dart3/bin/step2_eval.dart | 1 + impls/dart3/bin/step3_env.dart | 1 + impls/dart3/bin/step4_if_fn_do.dart | 1 + impls/dart3/bin/step5_tco.dart | 1 + impls/dart3/bin/step6_file.dart | 1 + impls/dart3/bin/step7_quote.dart | 1 + impls/dart3/bin/step8_macros.dart | 1 + impls/dart3/bin/step9_try.dart | 1 + impls/dart3/bin/stepA_mal.dart | 1 + impls/dart3/lib/src/core.dart | 1 + impls/dart3/pubspec.lock | 102 +++++++++++++------------- impls/dart3/run | 10 +++ 19 files changed, 147 insertions(+), 54 deletions(-) create mode 100644 impls/dart3/Dockerfile create mode 100644 impls/dart3/Makefile create mode 100644 impls/dart3/bin/pub_test.dart diff --git a/Makefile b/Makefile index ff1cd7a643..963c6c78d9 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,9 @@ OPTIONAL=1 # Run target/rule within docker image for the implementation DOCKERIZE = +# On Windows Git/MSYS shells, disable automatic path conversion for docker args. +MSYS_DOCKER_ENV = $(if $(filter Windows_NT,$(OS)),MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*',) + # # General settings and utility functions @@ -136,6 +139,7 @@ actual_impl = $(if $(filter mal,$(1)),$(patsubst %-mal,%,$(MAL_IMPL)),$(1)) # for this impl get_build_command = $(strip $(foreach mode,$(1)_MODE, \ $(if $(strip $(DOCKERIZE)),\ + $(MSYS_DOCKER_ENV) \ docker run \ -it --rm -u $(shell id -u) \ -v $(dir $(abspath $(lastword $(MAKEFILE_LIST)))):/mal \ @@ -152,6 +156,7 @@ get_build_command = $(strip $(foreach mode,$(1)_MODE, \ # necessary to launch the given impl and step get_run_prefix = $(strip $(foreach mode,$(call actual_impl,$(1))_MODE, \ $(if $(strip $(DOCKERIZE) $(4)),\ + $(MSYS_DOCKER_ENV) \ docker run -e STEP=$($2) -e MAL_IMPL=$(MAL_IMPL) \ -it --rm -u $(shell id -u) \ -v $(dir $(abspath $(lastword $(MAKEFILE_LIST)))):/mal \ @@ -215,9 +220,10 @@ ALL_REPL = $(strip $(sort \ .PHONY: $(foreach i,$(DO_IMPLS),$(foreach s,$(STEPS),$(call $(i)_STEP_TO_PROG,$(s)))) $(foreach i,$(DO_IMPLS),$(foreach s,$(STEPS),$(call $(i)_STEP_TO_PROG,$(s)))): $(foreach impl,$(word 2,$(subst /, ,$(@))),\ - $(if $(DOCKERIZE), \ - $(call get_build_command,$(impl)) $(patsubst impls/$(impl)/%,%,$(@)), \ - $(call get_build_command,$(impl)) $(subst impls/$(impl)/,,$(@)))) + $(if $(filter-out mal,$(impl)),\ + $(if $(DOCKERIZE), \ + $(call get_build_command,$(call actual_impl,$(impl))) $(patsubst impls/$(impl)/%,%,$(@)), \ + $(call get_build_command,$(impl)) $(subst impls/$(impl)/,,$(@))))) # Allow IMPL, build^IMPL, IMPL^STEP, and build^IMPL^STEP $(DO_IMPLS): $$(foreach s,$$(STEPS),$$(call $$(@)_STEP_TO_PROG,$$(s))) diff --git a/Makefile.impls b/Makefile.impls index 2aa1c413ec..874f7d284d 100644 --- a/Makefile.impls +++ b/Makefile.impls @@ -124,6 +124,7 @@ crystal_STEP_TO_PROG = impls/crystal/$($(1)) cs_STEP_TO_PROG = impls/cs/$($(1)).exe d_STEP_TO_PROG = impls/d/$($(1)) dart_STEP_TO_PROG = impls/dart/$($(1)).dart +dart3_STEP_TO_PROG = impls/dart3/bin/$($(1)).dart elisp_STEP_TO_PROG = impls/elisp/$($(1)).el elixir_STEP_TO_PROG = impls/elixir/lib/mix/tasks/$($(1)).ex elm_STEP_TO_PROG = impls/elm/$($(1)).js diff --git a/impls/dart3/Dockerfile b/impls/dart3/Dockerfile new file mode 100644 index 0000000000..5fcaa63f35 --- /dev/null +++ b/impls/dart3/Dockerfile @@ -0,0 +1,45 @@ +FROM ubuntu:24.04 +LABEL maintainer="Krysl " +LABEL org.opencontainers.image.source=https://github.com/kanaka/mal +LABEL org.opencontainers.image.description="mal test container: dart3" + +########################################################## +# General requirements for testing or common across many +# implementations +########################################################## + +RUN apt-get -y update + +# Required for running tests +RUN apt-get -y install make python3 +RUN ln -sf /usr/bin/python3 /usr/bin/python + +# Some typical implementation and test requirements +RUN apt-get -y install curl libreadline-dev libedit-dev + +########################################################## +# Specific implementation requirements +########################################################## + +RUN apt-get -y install apt-transport-https wget gnupg +RUN wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub \ + | gpg --dearmor -o /usr/share/keyrings/dart.gpg +RUN echo 'deb [signed-by=/usr/share/keyrings/dart.gpg arch=amd64] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main' \ + | tee /etc/apt/sources.list.d/dart_stable.list +RUN apt-get -y update +# Pin to a stable 3.11.x release to satisfy package language version requirements. +RUN apt-get -y install dart=3.11.4-1 + +# Run dart with writable home/cache for arbitrary uid in docker run (-u ). +ENV HOME=/tmp +ENV PUB_CACHE=/opt/dart-pub-cache +RUN mkdir -p /opt/dart-pub-cache /tmp/dart3-deps && chmod -R 777 /opt/dart-pub-cache /tmp/dart3-deps + +# Pre-fetch dependencies into image cache so tests do not need to run pub get each time. +COPY pubspec.yaml /tmp/dart3-deps/ +COPY pubspec.lock /tmp/dart3-deps/ +RUN cd /tmp/dart3-deps && dart pub get +RUN chmod -R 777 /opt/dart-pub-cache + +RUN mkdir -p /mal +WORKDIR /mal diff --git a/impls/dart3/Makefile b/impls/dart3/Makefile new file mode 100644 index 0000000000..b3c660f49d --- /dev/null +++ b/impls/dart3/Makefile @@ -0,0 +1,5 @@ +all: + @true + + +clean: diff --git a/impls/dart3/bin/pub_test.dart b/impls/dart3/bin/pub_test.dart new file mode 100644 index 0000000000..5ae343f8bf --- /dev/null +++ b/impls/dart3/bin/pub_test.dart @@ -0,0 +1,13 @@ +import 'dart:io'; + +import 'package:args/args.dart'; + +void main(List args) { + final parser = ArgParser()..addOption('print', abbr: 'p'); + final ret = parser.parse(args); + final p = ret.option('print'); + if (p != null) { + stdout.writeln(p); + } + return; +} diff --git a/impls/dart3/bin/step0_repl.dart b/impls/dart3/bin/step0_repl.dart index a6e420b684..7d3641c5a5 100644 --- a/impls/dart3/bin/step0_repl.dart +++ b/impls/dart3/bin/step0_repl.dart @@ -11,6 +11,8 @@ String rep(String str) => print(eval(read(str))); void main(List args) { while (true) { stdout.write('user> '); + if (!stdin.hasTerminal) stdout.write('\n'); + final input = stdin.readLineSync(); if (input == null) break; final output = rep(input); diff --git a/impls/dart3/bin/step1_read_print.dart b/impls/dart3/bin/step1_read_print.dart index e74a8469ad..1b38379203 100644 --- a/impls/dart3/bin/step1_read_print.dart +++ b/impls/dart3/bin/step1_read_print.dart @@ -10,6 +10,7 @@ String rep(String str) => print(eval(read(str))); void main(List args) { while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync(); if (input == null) break; try { diff --git a/impls/dart3/bin/step2_eval.dart b/impls/dart3/bin/step2_eval.dart index e96253e607..155417a691 100644 --- a/impls/dart3/bin/step2_eval.dart +++ b/impls/dart3/bin/step2_eval.dart @@ -39,6 +39,7 @@ String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync(); if (input == null) break; try { diff --git a/impls/dart3/bin/step3_env.dart b/impls/dart3/bin/step3_env.dart index d0acfef16b..ddc52dd218 100644 --- a/impls/dart3/bin/step3_env.dart +++ b/impls/dart3/bin/step3_env.dart @@ -71,6 +71,7 @@ String rep(String str) => print(eval(read(str), replEnv)); void main(List args) { while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync(); if (input == null) break; try { diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart index 72d34b4e23..a67bfb3cc2 100644 --- a/impls/dart3/bin/step4_if_fn_do.dart +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -105,6 +105,7 @@ void main(List args) { replEnv.preLoading(rep); while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync(); if (input == null) break; try { diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index 3cb677f7ad..eb62117ee3 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -147,6 +147,7 @@ void main(List args) { replEnv.preLoading(rep); while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync(); if (input == null) break; try { diff --git a/impls/dart3/bin/step6_file.dart b/impls/dart3/bin/step6_file.dart index cf10d1e8e9..1ccbd90ce2 100644 --- a/impls/dart3/bin/step6_file.dart +++ b/impls/dart3/bin/step6_file.dart @@ -164,6 +164,7 @@ void main(List args) { } while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync()?.trim(); if (input == null) break; if (input.isEmpty) continue; diff --git a/impls/dart3/bin/step7_quote.dart b/impls/dart3/bin/step7_quote.dart index 900fa71a51..aea507dbb4 100644 --- a/impls/dart3/bin/step7_quote.dart +++ b/impls/dart3/bin/step7_quote.dart @@ -201,6 +201,7 @@ void main(List args) { } while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync()?.trim(); if (input == null) break; if (input.isEmpty) continue; diff --git a/impls/dart3/bin/step8_macros.dart b/impls/dart3/bin/step8_macros.dart index ac20d89a61..8f122eb616 100644 --- a/impls/dart3/bin/step8_macros.dart +++ b/impls/dart3/bin/step8_macros.dart @@ -212,6 +212,7 @@ void main(List args) { } while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync()?.trim(); if (input == null) break; if (input.isEmpty) continue; diff --git a/impls/dart3/bin/step9_try.dart b/impls/dart3/bin/step9_try.dart index c0d7e77bf7..a12ab27c33 100644 --- a/impls/dart3/bin/step9_try.dart +++ b/impls/dart3/bin/step9_try.dart @@ -239,6 +239,7 @@ void main(List args) { } while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync()?.trim(); if (input == null) break; if (input.isEmpty) continue; diff --git a/impls/dart3/bin/stepA_mal.dart b/impls/dart3/bin/stepA_mal.dart index 152e324f58..3f9aa1216f 100644 --- a/impls/dart3/bin/stepA_mal.dart +++ b/impls/dart3/bin/stepA_mal.dart @@ -250,6 +250,7 @@ void main(List args) { rep(r'''(println (str "Mal [" *host-language* "]"))'''); while (true) { stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync()?.trim(); if (input == null) break; if (input.isEmpty) continue; diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index 43bc7462b5..95bda718db 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -305,6 +305,7 @@ final Map ns = { 'vals': MalFunction((args, env) => (args.first as MalMap).values.toMalList()), 'readline': MalFunction((args, env) { stdout.write(args.first.stringVal.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); final input = stdin.readLineSync()?.trim(); if (input == null || input.contains(String.fromCharCode(4))) { logger.d('Ctrl+D'); diff --git a/impls/dart3/pubspec.lock b/impls/dart3/pubspec.lock index 091dc0c17d..926fc11373 100644 --- a/impls/dart3/pubspec.lock +++ b/impls/dart3/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _fe_analyzer_shared sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "100.0.0" analyzer: @@ -14,7 +14,7 @@ packages: description: name: analyzer sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "13.0.0" ansicolor: @@ -22,7 +22,7 @@ packages: description: name: ansicolor sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.3" args: @@ -30,7 +30,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" async: @@ -38,7 +38,7 @@ packages: description: name: async sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.13.1" boolean_selector: @@ -46,7 +46,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" cli_config: @@ -54,7 +54,7 @@ packages: description: name: cli_config sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" clock: @@ -62,7 +62,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" collection: @@ -70,7 +70,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.19.1" convert: @@ -78,7 +78,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.2" coverage: @@ -86,7 +86,7 @@ packages: description: name: coverage sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.15.0" crypto: @@ -94,7 +94,7 @@ packages: description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.7" file: @@ -102,7 +102,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.1" frontend_server_client: @@ -110,7 +110,7 @@ packages: description: name: frontend_server_client sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.0" glob: @@ -118,7 +118,7 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.3" http_multi_server: @@ -126,7 +126,7 @@ packages: description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.2" http_parser: @@ -134,7 +134,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.2" io: @@ -142,7 +142,7 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.5" lints: @@ -150,7 +150,7 @@ packages: description: name: lints sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.0" logger: @@ -158,7 +158,7 @@ packages: description: name: logger sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" logging: @@ -166,7 +166,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" matcher: @@ -174,7 +174,7 @@ packages: description: name: matcher sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.20" meta: @@ -182,7 +182,7 @@ packages: description: name: meta sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.18.2" mime: @@ -190,7 +190,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" node_preamble: @@ -198,7 +198,7 @@ packages: description: name: node_preamble sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.2" package_config: @@ -206,7 +206,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" path: @@ -214,7 +214,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.1" pool: @@ -222,7 +222,7 @@ packages: description: name: pool sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.2" pub_semver: @@ -230,7 +230,7 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" shelf: @@ -238,7 +238,7 @@ packages: description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.2" shelf_packages_handler: @@ -246,7 +246,7 @@ packages: description: name: shelf_packages_handler sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" shelf_static: @@ -254,7 +254,7 @@ packages: description: name: shelf_static sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.3" shelf_web_socket: @@ -262,7 +262,7 @@ packages: description: name: shelf_web_socket sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" source_map_stack_trace: @@ -270,7 +270,7 @@ packages: description: name: source_map_stack_trace sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" source_maps: @@ -278,7 +278,7 @@ packages: description: name: source_maps sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.10.13" source_span: @@ -286,7 +286,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.10.2" stack_trace: @@ -294,7 +294,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.12.1" stream_channel: @@ -302,7 +302,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" string_scanner: @@ -310,7 +310,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.1" term_glyph: @@ -318,7 +318,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.2" test: @@ -326,7 +326,7 @@ packages: description: name: test sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.31.1" test_api: @@ -334,7 +334,7 @@ packages: description: name: test_api sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.12" test_core: @@ -342,7 +342,7 @@ packages: description: name: test_core sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.18" typed_data: @@ -350,7 +350,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" vm_service: @@ -358,7 +358,7 @@ packages: description: name: vm_service sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "15.2.0" watcher: @@ -366,7 +366,7 @@ packages: description: name: watcher sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" web: @@ -374,7 +374,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" web_socket: @@ -382,7 +382,7 @@ packages: description: name: web_socket sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" web_socket_channel: @@ -390,7 +390,7 @@ packages: description: name: web_socket_channel sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" webkit_inspection_protocol: @@ -398,7 +398,7 @@ packages: description: name: webkit_inspection_protocol sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" yaml: @@ -406,7 +406,7 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.3" sdks: diff --git a/impls/dart3/run b/impls/dart3/run index c57af5ee26..dc47ed4483 100644 --- a/impls/dart3/run +++ b/impls/dart3/run @@ -1,2 +1,12 @@ #!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" + +# package_config.json can exist but still be unusable across host/container path +# differences. Probe with a tiny package import program and run pub get on demand. +if ! dart "$script_dir/bin/pub_test.dart" >/dev/null 2>&1; then + dart pub get --directory "$script_dir" --offline +fi + exec dart $(dirname $0)/bin/${STEP:-stepA_mal}.dart "${@}" From 1a5f4d79af578cad84a422545108ad166062786f Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 8 May 2026 15:05:55 +0800 Subject: [PATCH 25/28] dart3: update readme/IMPLS.yml --- IMPLS.yml | 1 + README.md | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/IMPLS.yml b/IMPLS.yml index 17e38a53eb..e67a0684f1 100644 --- a/IMPLS.yml +++ b/IMPLS.yml @@ -20,6 +20,7 @@ IMPL: - {IMPL: d, d_MODE: ldc2} - {IMPL: d, d_MODE: dmd} - {IMPL: dart} + - {IMPL: dart3} - {IMPL: elisp} - {IMPL: elixir} - {IMPL: elm} diff --git a/README.md b/README.md index 3dca01ab2d..046f825679 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ process guide](process/guide.md) there is also a [mal/make-a-lisp FAQ](docs/FAQ.md) where I attempt to answer some common questions. -**3. Mal is implemented in 89 languages (95 different implementations and 118 runtime modes)** +**3. Mal is implemented in 89 languages (96 different implementations and 118 runtime modes)** | Language | Creator | | -------- | ------- | @@ -63,6 +63,7 @@ FAQ](docs/FAQ.md) where I attempt to answer some common questions. | [Crystal](#crystal) | [Linda_pp](https://github.com/rhysd) | | [D](#d) | [Dov Murik](https://github.com/dubek) | | [Dart](#dart) | [Harry Terkelsen](https://github.com/hterkelsen) | +| [Dart 3](#dart3) | [Krysl](https://github.com/krysl) | | [Elixir](#elixir) | [Martin Ek](https://github.com/ekmartin) | | [Elm](#elm) | [Jos van Bakel](https://github.com/c0deaddict) | | [Emacs Lisp](#emacs-lisp) | [Vasilij Schneidermann](https://github.com/wasamasa) | @@ -420,6 +421,15 @@ cd impls/dart dart ./stepX_YYY ``` +### Dart3 + +The Dart 3 implementation has been tested with Dart 3.11.4. + +``` +cd impls/dart3 +dart ./bin/stepX_YYY +``` + ### Emacs Lisp The Emacs Lisp implementation of mal has been tested with Emacs 24.3 From f84d977d44b8421b9b6dddfbfa751c06436d2b61 Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 8 May 2026 15:16:15 +0800 Subject: [PATCH 26/28] dart3: fix ci --- impls/dart3/run | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 impls/dart3/run diff --git a/impls/dart3/run b/impls/dart3/run old mode 100644 new mode 100755 From 2903e3b45a98f3c088ca08a3515f7fa0a59f66f3 Mon Sep 17 00:00:00 2001 From: Krysl Date: Fri, 8 May 2026 17:29:31 +0800 Subject: [PATCH 27/28] dart3: impls missing hashCode function, and a few other code optimize Signed-off-by: Krysl --- impls/dart3/bin/stepA_mal.dart | 5 +++-- impls/dart3/lib/src/types.dart | 30 +++++++++++------------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/impls/dart3/bin/stepA_mal.dart b/impls/dart3/bin/stepA_mal.dart index 3f9aa1216f..c39515b2a5 100644 --- a/impls/dart3/bin/stepA_mal.dart +++ b/impls/dart3/bin/stepA_mal.dart @@ -234,13 +234,14 @@ void main(List args) { rep('(loglevel debug)'); } replEnv.preLoading(rep); - if (args.isNotEmpty) { - final filePath = args.first; + + if (results.rest.isNotEmpty) { if (args.length > 1) { replEnv['*ARGV*'] = MalList( args.sublist(1).map((e) => MalString(e)).toList(), ); } + final filePath = results.rest.first; var file = File(filePath); if (file.existsSync()) { rep('(load-file "$filePath")'); diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index dfafe42f23..f6721f43ea 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -133,7 +133,7 @@ class MalAtom extends MalType<_MalTypeRef> { } @override - int get hashCode => throw UnimplementedError(); + int get hashCode => Object.hashAll([MalAtom, ref]); @override String toStr([bool printReadably = false]) => @@ -299,7 +299,7 @@ abstract class MalListBase extends ListMixin bool operator ==(covariant MalType other) => listCompare(this, other); @override - int get hashCode => val.hashCode; + int get hashCode => Object.hashAll(val); bool listCompare(ListBase a, MalType other) { if (other is ListBase) { @@ -316,7 +316,7 @@ class MalList extends MalListBase { bool operator ==(covariant MalType other) => listCompare(this, other); @override - int get hashCode => throw UnimplementedError(); + int get hashCode => Object.hashAll(val); @override ParenthesesType get type => .round; @@ -334,7 +334,7 @@ class MalVector extends MalListBase { bool operator ==(covariant MalType other) => listCompare(this, other); @override - int get hashCode => throw UnimplementedError(); + int get hashCode => Object.hashAll(val); @override ParenthesesType get type => .square; @@ -404,7 +404,7 @@ class MalMap } @override - int get hashCode => throw UnimplementedError(); + int get hashCode => Object.hashAll([...val.keys, ...val.values]); @override MalType metadata = nil; @@ -547,14 +547,16 @@ class MalMacroFunction extends MalType> @override bool operator ==(covariant MalType other) { - if (other is! MalMacroFunction) { + if (other is! MalMacroFunction || + isTCO != other.isTCO || + debugName != other.debugName) { return false; } return val == other.val; } @override - int get hashCode => val.hashCode; + int get hashCode => Object.hashAll([fn, isTCO, debugName]); @override MalType metadata = nil; @@ -581,17 +583,7 @@ class MalClosure extends MalType ]) : super(fn); @Deprecated('only for step4') - MalType call(List args) { - // if (fn != null) { - - // } else - if (fn is MalClosureFn) { - return fn!(args); - } - throw UnimplementedError( - 'funcion type ${fn.runtimeType} is not implemented', - ); - } + MalType call(List args) => fn!(args); @override String toStr([bool printReadably = false]) => @@ -610,7 +602,7 @@ class MalClosure extends MalType } @override - int get hashCode => val.hashCode; + int get hashCode => Object.hashAll([fn, params, env, ast, isMacro]); bool get isNotMacro => !isMacro; From 3032ccbba3c9b95a631ca2b51d43fd642f581222 Mon Sep 17 00:00:00 2001 From: Krysl Date: Sat, 9 May 2026 08:44:50 +0800 Subject: [PATCH 28/28] dart3: enable stricter type checks & code optimization Signed-off-by: Krysl --- impls/dart3/Dockerfile | 1 + impls/dart3/analysis_options.yaml | 6 +- impls/dart3/bin/step1_read_print.dart | 6 +- impls/dart3/bin/step2_eval.dart | 8 +- impls/dart3/bin/step3_env.dart | 27 +- impls/dart3/bin/step4_if_fn_do.dart | 35 +-- impls/dart3/bin/step5_tco.dart | 42 ++- impls/dart3/bin/step6_file.dart | 42 ++- impls/dart3/bin/step7_quote.dart | 57 ++--- impls/dart3/bin/step8_macros.dart | 29 +-- impls/dart3/bin/step9_try.dart | 29 +-- impls/dart3/bin/stepA_mal.dart | 29 +-- impls/dart3/lib/src/core.dart | 95 ++++--- impls/dart3/lib/src/env.dart | 14 +- impls/dart3/lib/src/error.dart | 2 +- impls/dart3/lib/src/print.dart | 2 +- impls/dart3/lib/src/reader.dart | 14 +- impls/dart3/lib/src/types.dart | 355 ++++++++------------------ impls/dart3/run | 25 +- 19 files changed, 317 insertions(+), 501 deletions(-) diff --git a/impls/dart3/Dockerfile b/impls/dart3/Dockerfile index 5fcaa63f35..792fd54d67 100644 --- a/impls/dart3/Dockerfile +++ b/impls/dart3/Dockerfile @@ -33,6 +33,7 @@ RUN apt-get -y install dart=3.11.4-1 # Run dart with writable home/cache for arbitrary uid in docker run (-u ). ENV HOME=/tmp ENV PUB_CACHE=/opt/dart-pub-cache +ENV IN_DOCKER=true RUN mkdir -p /opt/dart-pub-cache /tmp/dart3-deps && chmod -R 777 /opt/dart-pub-cache /tmp/dart3-deps # Pre-fetch dependencies into image cache so tests do not need to run pub get each time. diff --git a/impls/dart3/analysis_options.yaml b/impls/dart3/analysis_options.yaml index 010473a090..6b9db05f98 100644 --- a/impls/dart3/analysis_options.yaml +++ b/impls/dart3/analysis_options.yaml @@ -19,7 +19,11 @@ linter: rules: - avoid_print -# analyzer: +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true # exclude: # - path/to/excluded/files/** diff --git a/impls/dart3/bin/step1_read_print.dart b/impls/dart3/bin/step1_read_print.dart index 1b38379203..a6b6b584d6 100644 --- a/impls/dart3/bin/step1_read_print.dart +++ b/impls/dart3/bin/step1_read_print.dart @@ -2,9 +2,9 @@ import 'dart:io'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); -MalType eval(MalType val) => val; -String print(MalType str) => prStr(str, true); +MalAny read(String str) => readStr(str); +MalAny eval(MalAny val) => val; +String print(MalAny str) => prStr(str, true); String rep(String str) => print(eval(read(str))); void main(List args) { diff --git a/impls/dart3/bin/step2_eval.dart b/impls/dart3/bin/step2_eval.dart index 155417a691..c639914c97 100644 --- a/impls/dart3/bin/step2_eval.dart +++ b/impls/dart3/bin/step2_eval.dart @@ -2,13 +2,13 @@ import 'dart:io'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); -MalType eval(MalType ast, Env env) { +MalAny read(String str) => readStr(str); +MalAny eval(MalAny ast, Env env) { if (env.debugEval) { stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); } - MalType listCall(MalList list, Env env, MalList ast) { + MalAny listCall(MalList list, Env env, MalList ast) { if (list.isNotEmpty) { var fn = eval(list.first, env); if (fn is MalFunction) { @@ -31,7 +31,7 @@ MalType eval(MalType ast, Env env) { }; } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = Env(data: ns); String rep(String str) => print(eval(read(str), replEnv)); diff --git a/impls/dart3/bin/step3_env.dart b/impls/dart3/bin/step3_env.dart index ddc52dd218..0c30ce096e 100644 --- a/impls/dart3/bin/step3_env.dart +++ b/impls/dart3/bin/step3_env.dart @@ -3,19 +3,19 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); -MalType eval(MalType ast, Env env) { +MalAny read(String str) => readStr(str); +MalAny eval(MalAny ast, Env env) { if (env.debugEval) { stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); } - MalType listCall(MalList list, Env env, MalList ast) { + MalAny listCall(MalList list, Env env, MalList ast) { if (list.isNotEmpty) { var fn = eval(list.first, env); if (fn is MalFunction) { return fn.call(list.args.map((e) => eval(e, env)).toList(), env); } else if (fn is MalMacroFunction) { - return fn.call(list.args, env); + return fn.callWithoutTCO(list.args, env); } else if (fn is MalSymbolNotFound) { throw fn.makeError(); } @@ -36,27 +36,20 @@ MalType eval(MalType ast, Env env) { }; } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = Env( data: { 'def!': MalMacroFunction.normal( 'def!', - (List args, Env env) => + (List args, Env env) => env[(args[0] as MalSymbol).name] = eval(args[1], env), ), - 'let*': MalMacroFunction.normal('let*', (List args, Env env) { + 'let*': MalMacroFunction.normal('let*', (List args, Env env) { final newEnv = Env(outer: env); - List first; - if (args.first is MalList) { - first = (args.first as MalList); - } else if (args.first is MalVector) { - first = (args.first as MalVector); - } else { - throw UnsupportedError( - 'unsupported ${args.first.runtimeType} as Let* \'s first arg', - ); - } + MalListBase first = args.first.asMalListBase( + errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); for (final [key, val] in first.slices(2)) { newEnv[(key as MalSymbol).name] = eval(val, newEnv); diff --git a/impls/dart3/bin/step4_if_fn_do.dart b/impls/dart3/bin/step4_if_fn_do.dart index a67bfb3cc2..9fdb8c2d8e 100644 --- a/impls/dart3/bin/step4_if_fn_do.dart +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -5,19 +5,19 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); -MalType eval(MalType ast, Env env) { +MalAny read(String str) => readStr(str); +MalAny eval(MalAny ast, Env env) { if (env.debugEval) { stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); } - MalType listCall(MalListBase list, Env env, MalListBase ast) { + MalAny listCall(MalListBase list, Env env, MalListBase ast) { if (list.isNotEmpty) { var fn = eval(list.first, env); if (fn is MalFunction) { return fn.call(list.args.map((e) => eval(e, env)).toList(), env); } else if (fn is MalMacroFunction) { - return fn.call(list.args, env); + return fn.callWithoutTCO(list.args, env); } else if (fn is MalClosure) { return fn.call(list.args.map((e) => eval(e, env)).toList()); } else if (fn is MalSymbolNotFound) { @@ -40,37 +40,30 @@ MalType eval(MalType ast, Env env) { }; } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = globalEnv ..addAll({ 'def!': MalMacroFunction.normal( 'def!', - (List args, Env env) => + (List args, Env env) => env[(args[0] as MalSymbol).name] = eval(args[1], env), ), - 'let*': MalMacroFunction.normal('let*', (List args, Env env) { + 'let*': MalMacroFunction.normal('let*', (List args, Env env) { final newEnv = Env(outer: env); - List first; - if (args.first is MalList) { - first = (args.first as MalList); - } else if (args.first is MalVector) { - first = (args.first as MalVector); - } else { - throw UnsupportedError( - 'unsupported ${args.first.runtimeType} as Let* \'s first arg', - ); - } + MalListBase first = args.first.asMalListBase( + errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); for (final [key, val] in first.slices(2)) { newEnv[(key as MalSymbol).name] = eval(val, newEnv); } return eval(args[1], newEnv); }), - 'do': MalMacroFunction.normal('do', (List args, Env env) { + 'do': MalMacroFunction.normal('do', (List args, Env env) { return args.map((e) => eval(e, env)).toList().last; }), - 'if': MalMacroFunction.normal('if', (List args, Env env) { + 'if': MalMacroFunction.normal('if', (List args, Env env) { final br = eval(args.first, env); if (br is! MalNil && !(br is MalBool && br.val == false)) { return eval(args[1], env); @@ -80,7 +73,7 @@ final replEnv = globalEnv return MalNil(); } }), - 'fn*': MalMacroFunction.normal('fn*', (List args, Env env) { + 'fn*': MalMacroFunction.normal('fn*', (List args, Env env) { final first = args.first; final list = ((first is MalListBase ? first : null))?.list; if (list == null) { @@ -93,7 +86,7 @@ final replEnv = globalEnv return MalClosure( params, env, - (List fnArgs) => + (List fnArgs) => eval(args.second, Env(outer: env, binds: params, exprs: fnArgs)), ); }), diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart index eb62117ee3..522c0d4978 100644 --- a/impls/dart3/bin/step5_tco.dart +++ b/impls/dart3/bin/step5_tco.dart @@ -5,9 +5,9 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); +MalAny read(String str) => readStr(str); int depth = 0; -MalType eval(MalType ast, Env env) { +MalAny eval(MalAny ast, Env env) { depth++; int loop = 0; while (true) { @@ -43,7 +43,7 @@ MalType eval(MalType ast, Env env) { }, MalSymbol(name: 'fn*') => list.second is MalListBase - ? ((params) => MalClosure( + ? ((List params) => MalClosure( params, env, null, @@ -61,10 +61,7 @@ MalType eval(MalType ast, Env env) { fn .call(list.args.map((e) => eval(e, env)).toList(), env) .toTCO(), - final MalMacroFunction fn => - (fn.isTCO) - ? fn.callTCO(list.args, env) - : fn.call(list.args, env).toTCO(), + final MalMacroFunction fn => fn.call(list.args, env), final MalClosure fn => ( fn.ast!, Env( @@ -99,45 +96,38 @@ MalType eval(MalType ast, Env env) { } } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = globalEnv ..addAll({ - 'def!': MalMacroFunction.normal( + 'def!': MalMacroFunction( 'def!', - (List args, Env env) => - env[(args[0] as MalSymbol).name] = eval(args[1], env), + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), ), - 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + 'let*': MalMacroFunction('let*', (List args, Env env) { final newEnv = Env(outer: env); - List first; - if (args.first is MalList) { - first = (args.first as MalList); - } else if (args.first is MalVector) { - first = (args.first as MalVector); - } else { - throw UnsupportedError( - 'unsupported ${args.first.runtimeType} as Let* \'s first arg', - ); - } + MalListBase first = args.first.asMalListBase( + errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); for (final [key, val] in first.slices(2)) { - newEnv[(key as MalSymbol).name] = eval(val, newEnv); + newEnv[key.malSymbolName] = eval(val, newEnv); } return (args.second, newEnv, true); }), - 'do': MalMacroFunction.tco('do', (List args, Env env) { + 'do': MalMacroFunction('do', (List args, Env env) { if (args.length > 1) { args.sublist(0, args.length - 1).map((e) => eval(e, env)).toList().last; } return (args.last, null, true); }), - 'time': MalMacroFunction.normal('time', (List args, Env env) { + 'time': MalMacroFunction('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); stopwatch.stop(); println('time: ${stopwatch.elapsed}'); - return ret; + return ret.toTCO(); }), ...ns, }); diff --git a/impls/dart3/bin/step6_file.dart b/impls/dart3/bin/step6_file.dart index 1ccbd90ce2..5cf2f34b51 100644 --- a/impls/dart3/bin/step6_file.dart +++ b/impls/dart3/bin/step6_file.dart @@ -5,9 +5,9 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); +MalAny read(String str) => readStr(str); int depth = 0; -MalType eval(MalType ast, Env env) { +MalAny eval(MalAny ast, Env env) { depth++; int loop = 0; while (true) { @@ -43,10 +43,10 @@ MalType eval(MalType ast, Env env) { }, MalSymbol(name: 'fn*') => list.second is MalListBase - ? ((params) => MalClosure( + ? ((List params) => MalClosure( params, env, - (List fnArgs) => eval( + (List fnArgs) => eval( list.third, Env(outer: env, binds: params, exprs: fnArgs), ), @@ -71,10 +71,7 @@ MalType eval(MalType ast, Env env) { fn .call(list.args.map((e) => eval(e, env)).toList(), env) .toTCO(), - final MalMacroFunction fn => - (fn.isTCO) - ? fn.callTCO(list.args, env) - : fn.call(list.args, env).toTCO(), + final MalMacroFunction fn => fn.call(list.args, env), final MalClosure fn => ( fn.ast!, Env( @@ -108,39 +105,32 @@ MalType eval(MalType ast, Env env) { } } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = globalEnv ..addAll({ - 'def!': MalMacroFunction.normal( + 'def!': MalMacroFunction( 'def!', - (List args, Env env) => - env[(args[0] as MalSymbol).name] = eval(args[1], env), + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), ), - 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + 'let*': MalMacroFunction('let*', (List args, Env env) { final newEnv = Env(outer: env); - List first; - if (args.first is MalList) { - first = (args.first as MalList); - } else if (args.first is MalVector) { - first = (args.first as MalVector); - } else { - throw UnsupportedError( - 'unsupported ${args.first.runtimeType} as Let* \'s first arg', - ); - } + MalListBase first = args.first.asMalListBase( + errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); for (final [key, val] in first.slices(2)) { - newEnv[(key as MalSymbol).name] = eval(val, newEnv); + newEnv[key.malSymbolName] = eval(val, newEnv); } return (args.second, newEnv, true); }), - 'time': MalMacroFunction.normal('time', (List args, Env env) { + 'time': MalMacroFunction('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); stopwatch.stop(); println('time: ${stopwatch.elapsed}'); - return ret; + return ret.toTCO(); }), ...ns, 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), diff --git a/impls/dart3/bin/step7_quote.dart b/impls/dart3/bin/step7_quote.dart index aea507dbb4..8c18eb9428 100644 --- a/impls/dart3/bin/step7_quote.dart +++ b/impls/dart3/bin/step7_quote.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); +MalAny read(String str) => readStr(str); MalList qqLoop(MalListBase list) => MalList( list.isEmpty @@ -22,7 +22,7 @@ MalList qqLoop(MalListBase list) => MalList( ].reduce((combined, curr) => curr..list.add(combined)), ); -MalType quasiquote(MalType ast) { +MalAny quasiquote(MalAny ast) { final ret = switch (ast) { final MalList list when list.length == 2 && list.first == unquote => list.second, @@ -35,7 +35,7 @@ MalType quasiquote(MalType ast) { } int depth = 0; -MalType eval(MalType ast, Env env) { +MalAny eval(MalAny ast, Env env) { depth++; int loop = 0; while (true) { @@ -62,22 +62,18 @@ MalType eval(MalType ast, Env env) { (list.isNotEmpty) ? (switch (list.first) { MalSymbol(name: 'if') => switch (eval(list.second, env)) { - MalBool(val: true) || - MalInt() || - MalString() || - MalList() || - MalVector() => (list.third, null, true), - MalNil() || _ => + MalNil() || MalBool(val: false) => list.length > 3 ? (list.fourth, null, true) : (MalNil(), null, true), + _ => (list.third, null, true), }, MalSymbol(name: 'fn*') => list.second is MalListBase - ? ((params) => MalClosure( + ? ((List params) => MalClosure( params, env, - (List fnArgs) => eval( + (List fnArgs) => eval( list.third, Env(outer: env, binds: params, exprs: fnArgs), ), @@ -105,15 +101,15 @@ MalType eval(MalType ast, Env env) { logger.d('eval ${ret.toStr(true)}'); return (ret, null, true); }(), - _ => switch (eval(list.first, env)) { + final first => switch (eval( + first is MalList ? eval(first, env) : first, + env, + )) { final MalFunction fn => fn .call(list.args.map((e) => eval(e, env)).toList(), env) .toTCO(), - final MalMacroFunction fn => - (fn.isTCO) - ? fn.callTCO(list.args, env) - : fn.call(list.args, env).toTCO(), + final MalMacroFunction fn => fn.call(list.args, env), final MalClosure fn => ( fn.ast!, Env( @@ -145,39 +141,32 @@ MalType eval(MalType ast, Env env) { } } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = globalEnv ..addAll({ - 'def!': MalMacroFunction.normal( + 'def!': MalMacroFunction( 'def!', - (List args, Env env) => - env[(args[0] as MalSymbol).name] = eval(args[1], env), + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), ), - 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + 'let*': MalMacroFunction('let*', (List args, Env env) { final newEnv = Env(outer: env); - List first; - if (args.first is MalList) { - first = (args.first as MalList); - } else if (args.first is MalVector) { - first = (args.first as MalVector); - } else { - throw UnsupportedError( - 'unsupported ${args.first.runtimeType} as Let* \'s first arg', - ); - } + MalListBase first = args.first.asMalListBase( + errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', + ); for (final [key, val] in first.slices(2)) { - newEnv[(key as MalSymbol).name] = eval(val, newEnv); + newEnv[key.malSymbolName] = eval(val, newEnv); } return (args.second, newEnv, true); }), - 'time': MalMacroFunction.normal('time', (List args, Env env) { + 'time': MalMacroFunction('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); stopwatch.stop(); println('time: ${stopwatch.elapsed}'); - return ret; + return ret.toTCO(); }), ...ns, 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), diff --git a/impls/dart3/bin/step8_macros.dart b/impls/dart3/bin/step8_macros.dart index 8f122eb616..b5926d4952 100644 --- a/impls/dart3/bin/step8_macros.dart +++ b/impls/dart3/bin/step8_macros.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); +MalAny read(String str) => readStr(str); MalList qqLoop(MalListBase list) => MalList( list.isEmpty @@ -22,7 +22,7 @@ MalList qqLoop(MalListBase list) => MalList( ].reduce((combined, curr) => curr..list.add(combined)), ); -MalType quasiquote(MalType ast) { +MalAny quasiquote(MalAny ast) { final ret = switch (ast) { final MalList list when list.length == 2 && list.first == unquote => list.second, @@ -35,7 +35,7 @@ MalType quasiquote(MalType ast) { } int depth = 0; -MalType eval(MalType ast, Env env) { +MalAny eval(MalAny ast, Env env) { depth++; int loop = 0; while (true) { @@ -74,10 +74,10 @@ MalType eval(MalType ast, Env env) { }, MalSymbol(name: 'fn*') => list.second is MalListBase - ? ((params) => MalClosure( + ? ((List params) => MalClosure( params, env, - (List fnArgs) => eval( + (List fnArgs) => eval( list.third, Env(outer: env, binds: params, exprs: fnArgs), ), @@ -116,10 +116,7 @@ MalType eval(MalType ast, Env env) { fn .call(list.args.map((e) => eval(e, env)).toList(), env) .toTCO(), - final MalMacroFunction fn => - (fn.isTCO) - ? fn.callTCO(list.args, env) - : fn.call(list.args, env).toTCO(), + final MalMacroFunction fn => fn.call(list.args, env), final MalClosure fn when fn.isNotMacro => ( fn.ast!, Env( @@ -163,16 +160,16 @@ MalType eval(MalType ast, Env env) { } } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = globalEnv ..addAll({ - 'def!': MalMacroFunction.normal( + 'def!': MalMacroFunction( 'def!', - (List args, Env env) => - env[args[0].malSymbolName] = eval(args[1], env), + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), ), - 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + 'let*': MalMacroFunction('let*', (List args, Env env) { final newEnv = Env(outer: env); MalListBase first = args.first.asMalListBase( errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', @@ -183,12 +180,12 @@ final replEnv = globalEnv } return (args.second, newEnv, true); }), - 'time': MalMacroFunction.normal('time', (List args, Env env) { + 'time': MalMacroFunction('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); stopwatch.stop(); println('time: ${stopwatch.elapsed}'); - return ret; + return ret.toTCO(); }), ...ns, 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), diff --git a/impls/dart3/bin/step9_try.dart b/impls/dart3/bin/step9_try.dart index a12ab27c33..fd169d606c 100644 --- a/impls/dart3/bin/step9_try.dart +++ b/impls/dart3/bin/step9_try.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); +MalAny read(String str) => readStr(str); MalList qqLoop(MalListBase list) => MalList( list.isEmpty @@ -22,7 +22,7 @@ MalList qqLoop(MalListBase list) => MalList( ].reduce((combined, curr) => curr..list.add(combined)), ); -MalType quasiquote(MalType ast) { +MalAny quasiquote(MalAny ast) { final ret = switch (ast) { final MalList list when list.length == 2 && list.first == unquote => list.second, @@ -35,7 +35,7 @@ MalType quasiquote(MalType ast) { } int depth = 0; -MalType eval(MalType ast, Env env) { +MalAny eval(MalAny ast, Env env) { depth++; int loop = 0; while (true) { @@ -74,10 +74,10 @@ MalType eval(MalType ast, Env env) { }, MalSymbol(name: 'fn*') => list.second is MalListBase - ? ((params) => MalClosure( + ? ((List params) => MalClosure( params, env, - (List fnArgs) => eval( + (List fnArgs) => eval( list.third, Env(outer: env, binds: params, exprs: fnArgs), ), @@ -143,10 +143,7 @@ MalType eval(MalType ast, Env env) { fn .call(list.args.map((e) => eval(e, env)).toList(), env) .toTCO(), - final MalMacroFunction fn => - (fn.isTCO) - ? fn.callTCO(list.args, env) - : fn.call(list.args, env).toTCO(), + final MalMacroFunction fn => fn.call(list.args, env), final MalClosure fn when fn.isNotMacro => ( fn.ast!, Env( @@ -190,16 +187,16 @@ MalType eval(MalType ast, Env env) { } } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = globalEnv ..addAll({ - 'def!': MalMacroFunction.normal( + 'def!': MalMacroFunction( 'def!', - (List args, Env env) => - env[args[0].malSymbolName] = eval(args[1], env), + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), ), - 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + 'let*': MalMacroFunction('let*', (List args, Env env) { final newEnv = Env(outer: env); MalListBase first = args.first.asMalListBase( errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', @@ -210,12 +207,12 @@ final replEnv = globalEnv } return (args.second, newEnv, true); }), - 'time': MalMacroFunction.normal('time', (List args, Env env) { + 'time': MalMacroFunction('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); stopwatch.stop(); println('time: ${stopwatch.elapsed}'); - return ret; + return ret.toTCO(); }), ...ns, 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), diff --git a/impls/dart3/bin/stepA_mal.dart b/impls/dart3/bin/stepA_mal.dart index c39515b2a5..e68eb8ca3a 100644 --- a/impls/dart3/bin/stepA_mal.dart +++ b/impls/dart3/bin/stepA_mal.dart @@ -8,7 +8,7 @@ import 'package:args/args.dart'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -MalType read(String str) => readStr(str); +MalAny read(String str) => readStr(str); MalList qqLoop(MalListBase list) => MalList( list.isEmpty @@ -25,7 +25,7 @@ MalList qqLoop(MalListBase list) => MalList( ].reduce((combined, curr) => curr..list.add(combined)), ); -MalType quasiquote(MalType ast) { +MalAny quasiquote(MalAny ast) { final ret = switch (ast) { final MalList list when list.length == 2 && list.first == unquote => list.second, @@ -38,7 +38,7 @@ MalType quasiquote(MalType ast) { } int depth = 0; -MalType eval(MalType ast, Env env) { +MalAny eval(MalAny ast, Env env) { depth++; int loop = 0; while (true) { @@ -73,10 +73,10 @@ MalType eval(MalType ast, Env env) { }, MalSymbol(name: 'fn*') => list.second is MalListBase - ? ((params) => MalClosure( + ? ((List params) => MalClosure( params, env, - (List fnArgs) => eval( + (List fnArgs) => eval( list.third, Env(outer: env, binds: params, exprs: fnArgs), ), @@ -145,10 +145,7 @@ MalType eval(MalType ast, Env env) { fn .call(list.args.map((e) => eval(e, env)).toList(), env) .toTCO(), - final MalMacroFunction fn => - (fn.isTCO) - ? fn.callTCO(list.args, env) - : fn.call(list.args, env).toTCO(), + final MalMacroFunction fn => fn.call(list.args, env), final MalClosure fn when fn.isNotMacro => ( fn.ast!, Env( @@ -193,16 +190,16 @@ MalType eval(MalType ast, Env env) { } } -String print(MalType str) => prStr(str, true); +String print(MalAny str) => prStr(str, true); final replEnv = globalEnv ..addAll({ - 'def!': MalMacroFunction.normal( + 'def!': MalMacroFunction( 'def!', - (List args, Env env) => - env[args[0].malSymbolName] = eval(args[1], env), + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), ), - 'let*': MalMacroFunction.tco('let*', (List args, Env env) { + 'let*': MalMacroFunction('let*', (List args, Env env) { final newEnv = Env(outer: env); MalListBase first = args.first.asMalListBase( errMsg: 'unsupported ${args.first.runtimeType} as Let* \'s first arg', @@ -213,12 +210,12 @@ final replEnv = globalEnv } return (args.second, newEnv, true); }), - 'time': MalMacroFunction.normal('time', (List args, Env env) { + 'time': MalMacroFunction('time', (List args, Env env) { final stopwatch = Stopwatch()..start(); final ret = eval(args.first, env); stopwatch.stop(); println('time: ${stopwatch.elapsed}'); - return ret; + return ret.toTCO(); }), ...ns, 'eval': MalFunction((args, env) => eval(args.first, env.outer ?? env)), diff --git a/impls/dart3/lib/src/core.dart b/impls/dart3/lib/src/core.dart index 95bda718db..159b68b854 100644 --- a/impls/dart3/lib/src/core.dart +++ b/impls/dart3/lib/src/core.dart @@ -4,7 +4,7 @@ import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; import 'package:path/path.dart' as p; -String getName(MalType v) { +String getName(MalAny v) { switch (v) { case MalString(val: final str): case MalSymbol(name: final str): @@ -17,7 +17,7 @@ String getName(MalType v) { } } -int getInt(MalType v) { +int getInt(MalAny v) { switch (v) { case MalInt(val: final val): return val; @@ -28,27 +28,24 @@ int getInt(MalType v) { } } -final Map ns = { - 'loglevel': MalMacroFunction.normal('loglevel', ( - List args, - Env env, - ) { +final Map ns = { + 'loglevel': MalMacroFunction('loglevel', (List args, Env env) { if (args.isEmpty) { - return MalString(Logger.level.name); + return MalString(Logger.level.name).toTCO(); } setLogLevel(getName(args.first)); - return MalNil(); + return MalNil().toTCO(); }), - 'log': MalFunction((List args, Env env) { + 'log': MalFunction((List args, Env env) { logger.log( getLogLevelFromName(getName(args.first)), args.map((e) => e.toStr()), ); return MalNil(); }), - 'env': MalFunction((List args, Env env) { + 'env': MalFunction((List args, Env env) { int dep = args.isEmpty ? 0 : getInt(args.first); Env p = env; while (dep > 0 && p.outer != null) { @@ -58,52 +55,52 @@ final Map ns = { return MalMap(p.data.map((k, v) => MapEntry(MalString(k), v))); }), 'type': MalFunction( - (List args, Env env) => + (List args, Env env) => MalString(args.first.runtimeType.toString()), ), - 'prn': MalFunction((List args, Env env) { + 'prn': MalFunction((List args, Env env) { println(args.isNotEmpty ? args.map((e) => prStr(e, true)).join(' ') : ''); return MalNil(); }), - 'println': MalFunction((List args, Env env) { + 'println': MalFunction((List args, Env env) { println(args.isNotEmpty ? args.map((e) => prStr(e, false)).join(' ') : ''); return MalNil(); }), 'pr-str': MalFunction( - (List args, Env env) => + (List args, Env env) => MalString(args.map((e) => prStr(e, true)).join(' ')), ), 'str': MalFunction( - (List args, Env env) => + (List args, Env env) => MalString(args.map((e) => prStr(e, false)).join('')), ), '+': MalFunction( - (List args, Env env) => + (List args, Env env) => args.first.asMalInt() + args.second.asMalInt(), ), '-': MalFunction( - (List args, Env env) => + (List args, Env env) => args.first.asMalInt() - args.second.asMalInt(), ), '*': MalFunction( - (List args, Env env) => + (List args, Env env) => args.first.asMalInt() * args.second.asMalInt(), ), '/': MalFunction( - (List args, Env env) => + (List args, Env env) => args.first.asMalInt() / args.second.asMalInt(), ), - 'list': MalFunction((List args, Env env) => MalList(args)), + 'list': MalFunction((List args, Env env) => MalList(args)), 'list?': MalFunction( - (List args, Env env) => MalBool(args.first is MalList), + (List args, Env env) => MalBool(args.first is MalList), ), 'cons': MalFunction( - (List args, Env env) => MalList([ + (List args, Env env) => MalList([ args.first, if (args.length > 1) ...args.second.asMalListBase(), ]), ), - 'nth': MalFunction((List args, Env env) { + 'nth': MalFunction((List args, Env env) { var list = args.first.asMalListBase(); var index = args.second.asInt(); if (index >= list.length) { @@ -112,11 +109,11 @@ final Map ns = { return list[index]; }), 'first': MalFunction( - (List args, Env env) => args.first == MalNil() + (List args, Env env) => args.first == MalNil() ? MalNil() : args.first.asMalListBase().firstOrNull ?? MalNil(), ), - 'rest': MalFunction((List args, Env env) { + 'rest': MalFunction((List args, Env env) { var list = args.first.asMalListBaseOrNil(); if (list == null || list.length < 2) { return MalList(); @@ -125,17 +122,17 @@ final Map ns = { return MalList(rest); }), 'concat': MalFunction( - (List args, Env env) => + (List args, Env env) => MalList((List.from(args)).flattenedToList), ), 'vec': MalFunction( - (List args, Env env) => args.isNotEmpty + (List args, Env env) => args.isNotEmpty ? (args.first is! MalVector - ? MalVector(List.from(args.first.asMalListBase())) + ? MalVector(List.from(args.first.asMalListBase())) : args.first) : MalVector(), ), - 'seq': MalFunction((List args, Env env) { + 'seq': MalFunction((List args, Env env) { switch (args.first) { case final MalList list: if (list.isEmpty) return nil; @@ -168,10 +165,9 @@ final Map ns = { } }), 'empty?': MalFunction( - (List args, Env env) => - MalBool(args.first.asMalListBase().isEmpty), + (List args, Env env) => MalBool(args.first.asMalListBase().isEmpty), ), - 'count': MalFunction((List args, Env env) { + 'count': MalFunction((List args, Env env) { switch (args.first) { case MalList(length: final len): return MalInt(len); @@ -185,32 +181,32 @@ final Map ns = { ); } }), - '=': MalFunction((List args, Env env) { + '=': MalFunction((List args, Env env) { return MalBool(args.first == args.second); }), '>': MalFunction( - (List args, Env env) => + (List args, Env env) => MalBool(args.first.asInt() > args.second.asInt()), ), '>=': MalFunction( - (List args, Env env) => + (List args, Env env) => MalBool(args.first.asInt() >= args.second.asInt()), ), '<': MalFunction( - (List args, Env env) => + (List args, Env env) => MalBool(args.first.asInt() < args.second.asInt()), ), '<=': MalFunction( - (List args, Env env) => + (List args, Env env) => MalBool(args.first.asInt() <= args.second.asInt()), ), 'pwd': MalFunction( - (List args, Env env) => MalString(Directory.current.path), + (List args, Env env) => MalString(Directory.current.path), ), 'read-string': MalFunction( - (List args, Env env) => readStr(args.first.stringVal), + (List args, Env env) => readStr(args.first.stringVal), ), - 'slurp': MalFunction((List args, Env env) { + 'slurp': MalFunction((List args, Env env) { if (args.first is! MalString) { throw UnsupportedError( '<${args.first.runtimeType}>${args.first.toStr(true)}', @@ -222,18 +218,18 @@ final Map ns = { } return MalString(file.readAsStringSync()); }), - 'atom': MalFunction((List args, Env env) => MalAtom(args.first)), + 'atom': MalFunction((List args, Env env) => MalAtom(args.first)), 'deref': MalFunction( - (List args, Env env) => (args.first is MalAtom) + (List args, Env env) => (args.first is MalAtom) ? (args.first as MalAtom).val.ref : throw ArgumentInvalidError( "type <${args.first.runtimeType}>${args.first.toStr()} is not a subtype of type 'MalAtom' in type cast", ), ), - 'reset!': MalFunction((List args, Env env) { + 'reset!': MalFunction((List args, Env env) { return (args.first as MalAtom).ref = args.second; }), - 'swap!': MalFunction((List args, Env env) { + 'swap!': MalFunction((List args, Env env) { var atom = (args.first as MalAtom); return atom.ref = call(args.second, [atom.ref, ...args.sublist(2)], env); }), @@ -276,13 +272,13 @@ final Map ns = { ), 'assoc': MalFunction( (args, env) => MalMap( - Map.from((args.first as MalMap).val)..addEntries( + Map.from((args.first as MalMap).val)..addEntries( args.sublist(1).slices(2).map((l) => MapEntry(l.first, l.second)), ), ), ), 'dissoc': MalFunction((args, env) { - var map = Map.from((args.first as MalMap).val); + var map = Map.from((args.first as MalMap).val); args.sublist(1).forEach(map.remove); return MalMap(map); }), @@ -324,18 +320,17 @@ final Map ns = { ), }; -MalType call(MalType fn, List args, Env env) => switch (fn) { +MalAny call(MalAny fn, List args, Env env) => switch (fn) { final MalFunction fn => fn.call(args, env), final MalClosure fn => fn.call(args), _ => throw UnimplementedError(), }; -MalFunction isType([bool Function(T val)? test]) => +MalFunction isType([bool Function(T val)? test]) => MalFunction( (args, env) => MalBool(args.first is T && (test?.call(args.first as T) ?? true)), ); - final preloading = [ r'''(def! not (fn* (a) (if a false true)))''', r'''(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))''', diff --git a/impls/dart3/lib/src/env.dart b/impls/dart3/lib/src/env.dart index 3e3eca3ad0..752fa7ee03 100644 --- a/impls/dart3/lib/src/env.dart +++ b/impls/dart3/lib/src/env.dart @@ -30,12 +30,12 @@ typedef RepFn = String Function(String str); class Env { final Env? outer; - final Map data; + final Map data; Env({ this.outer, - Map? data, + Map? data, List? binds, - List? exprs, + List? exprs, }) : data = data ?? {} { if (binds == null) { assert(exprs == null); @@ -62,18 +62,18 @@ class Env { flags = Flags(this); } - void operator []=(String key, MalType val) => data[key] = val; - MalType? operator [](String key) => data[key] ?? outer?[key]; + void operator []=(String key, MalAny val) => data[key] = val; + MalAny? operator [](String key) => data[key] ?? outer?[key]; bool containsKey(String key) => data.containsKey(key) || (outer?.containsKey(key) ?? false); final List _builtinKeys = ['DEBUG-EVAL', 'not', 'load-file']; - void addAll(Map other) { + void addAll(Map other) { _builtinKeys.addAll(other.keys); data.addAll(other); } - MalType getSymbolVal(MalSymbol symbol) { + MalAny getSymbolVal(MalSymbol symbol) { final key = symbol.name; return data[key] ?? outer?[key] ?? diff --git a/impls/dart3/lib/src/error.dart b/impls/dart3/lib/src/error.dart index 77e16d13fa..b1b22e0a57 100644 --- a/impls/dart3/lib/src/error.dart +++ b/impls/dart3/lib/src/error.dart @@ -13,7 +13,7 @@ abstract class MalError extends Error { } class CustomThrowError extends MalError { - final MalType err; + final MalAny err; CustomThrowError(this.err):super(err.toStr()); @override String toString() => 'Error: $message'; diff --git a/impls/dart3/lib/src/print.dart b/impls/dart3/lib/src/print.dart index c01405ff51..4c0df20dcb 100644 --- a/impls/dart3/lib/src/print.dart +++ b/impls/dart3/lib/src/print.dart @@ -20,7 +20,7 @@ List clearTestOutput() { return list; } -String prStr(MalType val, [bool printReadably = false]) { +String prStr(MalAny val, [bool printReadably = false]) { final debugOn = globalEnv.debugStr; if (debugOn) { debugPrint( diff --git a/impls/dart3/lib/src/reader.dart b/impls/dart3/lib/src/reader.dart index c65634ccda..511e1ed096 100644 --- a/impls/dart3/lib/src/reader.dart +++ b/impls/dart3/lib/src/reader.dart @@ -73,7 +73,7 @@ List tokenize(String str) { final intRe = RegExp(r'^-?[0-9]+$'); final strRe2 = RegExp(r'''"(?(?:\\.|[^\\"])*)"?'''); -MalType readAtom(Reader reader) { +MalAny readAtom(Reader reader) { final token = reader.next(); if (token == null) throw UnexpectedError('unexpecetd EOF'); @@ -98,7 +98,7 @@ MalType readAtom(Reader reader) { return MalSymbol(token); } -MalType readList(Reader reader, ParenthesesType p) { +MalAny readList(Reader reader, ParenthesesType p) { assert(reader.peek()!.str == p.left); reader.next(); final list = switch (p) { @@ -107,7 +107,7 @@ MalType readList(Reader reader, ParenthesesType p) { .curly => MalMap(), }; bool isKey = true; - MalType key = nil; + MalAny key = nil; while (true) { final peek = reader.peek(); if (peek == null) throw UnexpectedError('unexpecetd EOF'); @@ -118,7 +118,7 @@ MalType readList(Reader reader, ParenthesesType p) { switch (p) { case .round: case .square: - (list as ListBase).add(readForm(reader)); + (list as ListBase).add(readForm(reader)); break; case .curly: if (isKey) { @@ -136,7 +136,7 @@ MalType readList(Reader reader, ParenthesesType p) { } isKey = !isKey; } - return list as MalType; + return list as MalAny; } const macros = { @@ -147,7 +147,7 @@ const macros = { '@': 'deref', '^': 'with-meta', }; -MalType readForm(Reader reader) { +MalAny readForm(Reader reader) { var token = reader.peek(); MalList readQuote(String token) { reader.next(); @@ -171,7 +171,7 @@ MalType readForm(Reader reader) { }; } -MalType readStr(String str) { +MalAny readStr(String str) { final token = tokenize(str); final reader = Reader(token); diff --git a/impls/dart3/lib/src/types.dart b/impls/dart3/lib/src/types.dart index f6721f43ea..a4d5b4f7d7 100644 --- a/impls/dart3/lib/src/types.dart +++ b/impls/dart3/lib/src/types.dart @@ -2,25 +2,30 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:mal/mal.dart'; -import 'package:meta/meta.dart'; + +typedef MalAny = MalType; sealed class MalType { final T val; const MalType(this.val); String toStr([bool printReadably = false]); + @override - @mustBeOverridden - bool operator ==(covariant MalType other); + bool operator ==(covariant MalAny other) { + if (runtimeType != other.runtimeType) { + return false; + } + return val == other.val; + } @override - @mustBeOverridden - int get hashCode; + int get hashCode => val.hashCode; @override String toString() => toStr(); } -extension MalTypeAs on MalType { +extension MalTypeAs on MalAny { @pragma('vm:prefer-inline') int asInt() => (this as MalInt).val; @@ -59,18 +64,18 @@ extension MalTypeAs on MalType { bool get isMacro => this is MalClosure && (this as MalClosure).isMacro; } -extension Second on List { +extension Second on List { @pragma('vm:prefer-inline') - MalType get second => this[1]; + MalAny get second => this[1]; @pragma('vm:prefer-inline') - MalType get third => this[2]; + MalAny get third => this[2]; @pragma('vm:prefer-inline') - MalType get fourth => this[3]; + MalAny get fourth => this[3]; @pragma('vm:prefer-inline') MalList toMalList() => MalList(this); } -extension ToMalList on Iterable { +extension ToMalList on Iterable { @pragma('vm:prefer-inline') MalList toMalList() => MalList(toList()); } @@ -84,49 +89,34 @@ class MalInt extends MalType { MalInt operator -(MalInt other) => MalInt(val - other.val); MalInt operator *(MalInt other) => MalInt(val * other.val); MalInt operator /(MalInt other) => MalInt(val ~/ other.val); - @override - bool operator ==(covariant MalType other) { - if (other is! MalInt) { - return false; - } - return val == other.val; - } - - @override - int get hashCode => val.hashCode; } class MalNil extends MalType { const MalNil() : super(null); @override String toStr([bool printReadably = false]) => 'nil'; - @override - bool operator ==(covariant MalType other) { - if (other.runtimeType != MalNil) { - return false; - } - return true; - } - - @override - int get hashCode => (MalNil).hashCode; } const nil = MalNil(); -class _MalTypeRef { - _MalTypeRef(this.ref); - MalType ref; +class MalTypeRef { + MalTypeRef(this.ref); + MalAny ref; + @override + operator ==(covariant MalTypeRef other) => ref == other.ref; + + @override + int get hashCode => ref.hashCode; } -class MalAtom extends MalType<_MalTypeRef> { - MalType get ref => val.ref; - set ref(MalType newVal) => val.ref = newVal; +class MalAtom extends MalType { + MalAny get ref => val.ref; + set ref(MalAny newVal) => val.ref = newVal; - MalAtom(MalType val) : super(_MalTypeRef(val)); + MalAtom(MalAny val) : super(MalTypeRef(val)); @override - bool operator ==(covariant MalType other) { - if (other.runtimeType != MalNil) { + bool operator ==(covariant MalAny other) { + if (other is! MalAtom) { return false; } return val == other.val; @@ -144,34 +134,12 @@ class MalBool extends MalType { MalBool(super.val); @override String toStr([bool printReadably = false]) => val ? 'true' : 'false'; - - @override - bool operator ==(covariant MalType other) { - if (other is! MalBool) { - return false; - } - return val == other.val; - } - - @override - int get hashCode => val.hashCode; } class MalKeyword extends MalType { MalKeyword(String val) : super('\u029E$val'); @override String toStr([bool printReadably = false]) => ':${val.substring(1)}'; - - @override - bool operator ==(covariant MalType other) { - if (other is! MalKeyword) { - return false; - } - return val == other.val; - } - - @override - int get hashCode => val.hashCode; } class MalString extends MalType { @@ -184,40 +152,6 @@ class MalString extends MalType { @override String toStr([bool printReadably = false]) => printReadably == true ? '"${val.toPrintable()}"' : val; - - @override - bool operator ==(covariant MalType other) { - if (other is! MalString) { - return false; - } - return val == other.val; - } - - @override - int get hashCode => val.hashCode; -} - -class MalComment extends MalType { - MalComment(super.val) - : assert(() { - logger.t('make MalComment($val)'); - return true; - }()); - - @override - String toStr([bool printReadably = false]) => - printReadably == true ? ';${val.toPrintable()}' : val; - - @override - bool operator ==(covariant MalType other) { - if (other is! MalComment) { - return false; - } - return val == other.val; - } - - @override - int get hashCode => val.hashCode; } extension type const Parentheses._((String, String) p) { @@ -246,7 +180,7 @@ enum ParenthesesType { String get right => p.right; } -bool _listCompare(List a, List b) { +bool _listCompare(List a, List b) { if (a.length != b.length) { return false; } @@ -256,52 +190,48 @@ bool _listCompare(List a, List b) { return true; } -abstract interface class MalMeta { - MalType metadata = nil; - T clone(); +mixin MalMeta on MalType { + MalAny metadata = nil; + MalMeta clone(); } -abstract class MalListBase extends ListMixin - implements MalType>, MalMeta { - MalListBase([List? list]) : _inner = list ?? []; - abstract final ParenthesesType type; - final List _inner; +abstract class MalListBase extends MalType> + with ListMixin, MalMeta { + MalListBase([List? list]) : super(list ?? []); + ParenthesesType get type; @override - int get length => _inner.length; + int get length => val.length; @override - set length(int newLength) => _inner.length = newLength; + set length(int newLength) => val.length = newLength; @override - MalType operator [](int index) => _inner[index]; + E operator [](int index) => val[index]; @override - void operator []=(int index, MalType value) => _inner[index] = value; + void operator []=(int index, E value) => val[index] = value; @override String toStr([bool printReadably = false]) => - '${type.left}${_inner.map((e) => e.toStr(printReadably)).join(' ')}${type.right}'; + '${type.left}${val.map((e) => e.toStr(printReadably)).join(' ')}${type.right}'; @override - Iterable map(T Function(MalType e) f) => _inner.map(f); + Iterable map(T Function(E e) f) => val.map(f); - List get list => _inner; + List get list => val; @override - void add(MalType element) => _inner.add(element); + void add(E element) => val.add(element); - List get args => _inner.sublist(1); - - @override - List get val => _inner; + List get args => val.sublist(1); @override - bool operator ==(covariant MalType other) => listCompare(this, other); + bool operator ==(covariant MalAny other) => listCompare(this, other); @override int get hashCode => Object.hashAll(val); - bool listCompare(ListBase a, MalType other) { + bool listCompare(ListBase a, MalAny other) { if (other is ListBase) { return _listCompare(a, other as ListBase); } else { @@ -310,78 +240,57 @@ abstract class MalListBase extends ListMixin } } -class MalList extends MalListBase { +class MalList extends MalListBase { MalList([super.list]); - @override - bool operator ==(covariant MalType other) => listCompare(this, other); - - @override - int get hashCode => Object.hashAll(val); @override ParenthesesType get type => .round; @override - MalType metadata = nil; - - @override - MalList clone() => MalList(List.from(list)); + MalList clone() => MalList(List.from(list)); } -class MalVector extends MalListBase { +class MalVector extends MalListBase { MalVector([super.list]); - @override - bool operator ==(covariant MalType other) => listCompare(this, other); - - @override - int get hashCode => Object.hashAll(val); @override ParenthesesType get type => .square; - @override - MalType metadata = nil; - @override MalVector clone() => MalVector(List.from(list)); } -class MalMap - with MapMixin - implements MalType>, MalMeta { - MalMap([Map? map]) - : _innerMap = map ?? {}; - final Map _innerMap; +class MalMap extends MalType> + with MapMixin, MalMeta { + MalMap([Map? map]) : super(map ?? {}); + @override - operator [](Object? key) => _innerMap[key]; + operator [](covariant K key) => val[key]; @override - void operator []=(key, value) => _innerMap[key] = value; + void operator []=(K key, V value) => val[key] = value; @override - void clear() => _innerMap.clear(); + void clear() => val.clear(); @override - Iterable get keys => _innerMap.keys; + Iterable get keys => val.keys; @override - remove(Object? key) => _innerMap.remove(key); + remove(Object? key) => val.remove(key); @override String toStr([bool printReadably = false]) { if (shouldLog) { - final maxKeyLength = _innerMap.keys.map((e) => e.toStr().length).max; - return '{\n\t${_innerMap.entries.map((kv) => '${kv.key}${' ' * (maxKeyLength - kv.key.toStr().length)}: ${kv.value.toStr(printReadably)}').join('\n\t')}\n}'; + final maxKeyLength = val.keys.map((e) => e.toStr().length).max; + return '{\n\t${val.entries.map((kv) => '${kv.key}${' ' * (maxKeyLength - kv.key.toStr().length)}: ${kv.value.toStr(printReadably)}').join('\n\t')}\n}'; } else { - return '{${_innerMap.entries.map((kv) => '${kv.key.toStr(true)} ${kv.value.toStr(printReadably)}').join(' ')}}'; + return '{${val.entries.map((kv) => '${kv.key.toStr(true)} ${kv.value.toStr(printReadably)}').join(' ')}}'; } } @override - Map get val => _innerMap; - - @override - bool operator ==(covariant MalType other) { + bool operator ==(covariant MalAny other) { if (other is! MalMap) { return false; } @@ -407,10 +316,7 @@ class MalMap int get hashCode => Object.hashAll([...val.keys, ...val.values]); @override - MalType metadata = nil; - - @override - MalMap clone() => MalMap(Map.from(_innerMap)); + MalMap clone() => MalMap(Map.from(val)); } class MalSymbol extends MalType { @@ -420,17 +326,6 @@ class MalSymbol extends MalType { const MalSymbol.builtin(super.val) : token = null; @override String toStr([bool printReadably = false]) => name; - - @override - bool operator ==(covariant MalType other) { - if (other is! MalSymbol) { - return false; - } - return val == other.val; - } - - @override - int get hashCode => Object.hashAll([MalSymbol, val]); } extension ToSymbolBuiltin on String { @@ -454,7 +349,7 @@ final class MalSymbolNotFound extends MalType { Error makeError() => KeyNotFoundError(toStr()); @override - bool operator ==(covariant MalType other) { + bool operator ==(covariant MalAny other) { if (other is! MalSymbolNotFound) { return false; } @@ -465,114 +360,72 @@ final class MalSymbolNotFound extends MalType { int get hashCode => val.hashCode; } -typedef MalFn = T Function(List args, Env env); -typedef Fn = MalFn; -typedef FnTCO = MalFn; -typedef MalClosureFn = MalType Function(List args); - -mixin MalCallable on MalType {} - -class MalFunction extends MalType - with MalCallable - implements MalMeta { - Fn get fn => super.val; - MalFunction(super.val); - @override - String toStr([bool printReadably = false]) => '${fn.toString()}'; +typedef Fn = T Function(List args, Env env); +typedef FnMalType = Fn; +typedef FnTCO = Fn; +typedef MalClosureFn = MalAny Function(List args); - MalType call(List args, Env env) { - return fn(args, env); - } +mixin _MalCallable on MalType {} +typedef MalCallable = _MalCallable; - @override - bool operator ==(covariant MalType other) { - if (other is! MalFunction) { - return false; - } - return val == other.val; - } +abstract class MalFunctionBase extends MalType> + with _MalCallable, MalMeta { + MalFunctionBase(super.val); + Fn get fn => super.val; - @override - int get hashCode => val.hashCode; + T call(List args, Env env) => fn(args, env); +} +class MalFunction extends MalFunctionBase with _MalCallable, MalMeta { + MalFunction(super.val); @override - MalType metadata = nil; + String toStr([bool printReadably = false]) => '${fn.toString()}'; @override MalFunction clone() => MalFunction(val); } -typedef TCO = (MalType ast, Env? env, bool conti); +typedef TCO = (MalAny ast, Env? env, bool conti); -extension ToTCO on MalType { +extension ToTCO on MalAny { TCO toTCO([Env? env, bool cont = false]) => (this, env, cont); } /// without eval args -class MalMacroFunction extends MalType> - with MalCallable - implements MalMeta> { +class MalMacroFunction extends MalFunctionBase with _MalCallable, MalMeta { final String debugName; - MalFn get fn => super.val; - final bool isTCO; - MalMacroFunction._(this.debugName, super.val, {bool tco = false}) - : isTCO = tco; - static MalMacroFunction normal( - String debugName, - Fn val, { - bool tco = false, - }) => MalMacroFunction._(debugName, val); - static MalMacroFunction tco(String debugName, FnTCO val) => - ._(debugName, val, tco: true); - @override - String toStr([bool printReadably = false]) => '$debugName ${fn.toString()}'; + MalMacroFunction(this.debugName, super.val); - R call(List args, Env env) { - if (fn is Fn) { - return fn(args, env) as R; - } - throw UnimplementedError( - 'funcion type ${fn.runtimeType} is not implemented', - ); - } + @Deprecated('only for step3/4') + factory MalMacroFunction.normal(String debugName, FnMalType fn) => + MalMacroFunction(debugName, (args, env) => fn(args, env).toTCO()); - TCO callTCO(List args, Env env) { - if (fn is FnTCO) { - return fn(args, env) as TCO; - } - throw UnimplementedError( - 'funcion "$debugName" type ${fn.runtimeType} is not implemented for tco', - ); - } + @Deprecated('only for step3/4') + MalAny callWithoutTCO(List args, Env env) => fn(args, env).$1; @override - bool operator ==(covariant MalType other) { - if (other is! MalMacroFunction || - isTCO != other.isTCO || - debugName != other.debugName) { + String toStr([bool printReadably = false]) => '$debugName ${fn.toString()}'; + + @override + bool operator ==(covariant MalAny other) { + if (other is! MalMacroFunction || debugName != other.debugName) { return false; } return val == other.val; } @override - int get hashCode => Object.hashAll([fn, isTCO, debugName]); + int get hashCode => Object.hashAll([fn, debugName]); @override - MalType metadata = nil; - - @override - MalMacroFunction clone() => MalMacroFunction._(debugName, val, tco: isTCO); + MalMacroFunction clone() => MalMacroFunction(debugName, val); } -class MalClosure extends MalType - with MalCallable - implements MalMeta { - @Deprecated('only for step4') +class MalClosure extends MalType with _MalCallable, MalMeta { MalClosureFn? get fn => super.val; final List params; final Env env; - final MalType? ast; + final MalAny? ast; final bool isMacro; MalClosure( this.params, @@ -582,15 +435,14 @@ class MalClosure extends MalType this.isMacro = false, ]) : super(fn); - @Deprecated('only for step4') - MalType call(List args) => fn!(args); + MalAny call(List args) => fn!(args); @override String toStr([bool printReadably = false]) => '# ${ast?.toStr(printReadably)}'; @override - bool operator ==(covariant MalType other) { + bool operator ==(covariant MalAny other) { if (other is! MalClosure) { return false; } @@ -609,7 +461,4 @@ class MalClosure extends MalType @override MalClosure clone({bool isMacro = false}) => MalClosure(params, env, fn, ast, isMacro); - - @override - MalType metadata = nil; } diff --git a/impls/dart3/run b/impls/dart3/run index dc47ed4483..d794e77660 100755 --- a/impls/dart3/run +++ b/impls/dart3/run @@ -3,10 +3,31 @@ set -euo pipefail script_dir="$(cd "$(dirname "$0")" && pwd)" +if [[ -n "${IN_DOCKER:-}" ]]; then + probe_cmd=(dart --packages="$script_dir/.dart_tool/package_config_docker.json" "$script_dir/bin/pub_test.dart") +else + probe_cmd=(dart "$script_dir/bin/pub_test.dart") +fi + # package_config.json can exist but still be unusable across host/container path # differences. Probe with a tiny package import program and run pub get on demand. -if ! dart "$script_dir/bin/pub_test.dart" >/dev/null 2>&1; then +# Use different dependencies inside and outside Docker to prevent Docker tests +# from breaking external dependencies. +if ! "${probe_cmd[@]}" >/dev/null 2>&1; then + has_host_package_config=0 + if [[ -f "$script_dir/.dart_tool/package_config.json" ]]; then + cp "$script_dir/.dart_tool/package_config.json" "$script_dir/.dart_tool/package_config.json.bak" + has_host_package_config=1 + fi dart pub get --directory "$script_dir" --offline + mv "$script_dir/.dart_tool/package_config.json" "$script_dir/.dart_tool/package_config_docker.json" + if [[ "$has_host_package_config" -eq 1 ]]; then + mv "$script_dir/.dart_tool/package_config.json.bak" "$script_dir/.dart_tool/package_config.json" + fi fi -exec dart $(dirname $0)/bin/${STEP:-stepA_mal}.dart "${@}" +if [[ -n "${IN_DOCKER:-}" ]]; then + exec dart --packages=$(dirname $0)/.dart_tool/package_config_docker.json $(dirname $0)/bin/${STEP:-stepA_mal}.dart "${@}" +else + exec dart $(dirname $0)/bin/${STEP:-stepA_mal}.dart "${@}" +fi