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/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 2c3517d7ff..874f7d284d 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 @@ -123,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/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 diff --git a/impls/dart3/.gitignore b/impls/dart3/.gitignore new file mode 100644 index 0000000000..64287c1135 --- /dev/null +++ b/impls/dart3/.gitignore @@ -0,0 +1,4 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ +test/generated/ 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/Dockerfile b/impls/dart3/Dockerfile new file mode 100644 index 0000000000..792fd54d67 --- /dev/null +++ b/impls/dart3/Dockerfile @@ -0,0 +1,46 @@ +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 +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. +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/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..6b9db05f98 --- /dev/null +++ b/impls/dart3/analysis_options.yaml @@ -0,0 +1,34 @@ +# 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: + - avoid_print + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true +# 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/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 new file mode 100644 index 0000000000..7d3641c5a5 --- /dev/null +++ b/impls/dart3/bin/step0_repl.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +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) { + stdout.write('user> '); + if (!stdin.hasTerminal) stdout.write('\n'); + + final input = stdin.readLineSync(); + if (input == null) break; + final output = rep(input); + stdout.writeln(output); + } +} diff --git a/impls/dart3/bin/step1_read_print.dart b/impls/dart3/bin/step1_read_print.dart new file mode 100644 index 0000000000..a6b6b584d6 --- /dev/null +++ b/impls/dart3/bin/step1_read_print.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:mal/mal.dart'; + +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) { + while (true) { + stdout.write('user> '.toBlue); + if (!stdin.hasTerminal) stdout.write('\n'); + 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/bin/step2_eval.dart b/impls/dart3/bin/step2_eval.dart new file mode 100644 index 0000000000..c639914c97 --- /dev/null +++ b/impls/dart3/bin/step2_eval.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:mal/mal.dart'; + +MalAny read(String str) => readStr(str); +MalAny eval(MalAny ast, Env env) { + if (env.debugEval) { + stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); + } + + 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); + } + throw NotCallableError('${fn.toStr()} is not callable'); + } else { + return ast; + } + } + + 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(MalAny str) => prStr(str, true); + +final replEnv = Env(data: ns); +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 { + final output = rep(input); + stdout.writeln(output); + } on ParserError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/bin/step3_env.dart b/impls/dart3/bin/step3_env.dart new file mode 100644 index 0000000000..0c30ce096e --- /dev/null +++ b/impls/dart3/bin/step3_env.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalAny read(String str) => readStr(str); +MalAny eval(MalAny ast, Env env) { + if (env.debugEval) { + stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); + } + + 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.callWithoutTCO(list.args, env); + } else if (fn is MalSymbolNotFound) { + throw fn.makeError(); + } + throw NotCallableError('${fn.toStr()} is not callable'); + } else { + return ast; + } + } + + 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(MalAny str) => prStr(str, true); + +final replEnv = Env( + data: { + 'def!': MalMacroFunction.normal( + 'def!', + (List args, Env env) => + env[(args[0] as MalSymbol).name] = eval(args[1], env), + ), + 'let*': MalMacroFunction.normal('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 as MalSymbol).name] = eval(val, newEnv); + } + return eval(args[1], newEnv); + }), + ...ns, + }, +); +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 { + final output = rep(input); + stdout.writeln(output); + } on ParserError catch (e) { + 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..9fdb8c2d8e --- /dev/null +++ b/impls/dart3/bin/step4_if_fn_do.dart @@ -0,0 +1,111 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalAny read(String str) => readStr(str); +MalAny eval(MalAny ast, Env env) { + if (env.debugEval) { + stdout.writeln('${'EVAL:'.toCyan} ${prStr(ast, true)}'); + } + + 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.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) { + throw fn.makeError(); + } + throw NotCallableError('${fn.toStr()} is not callable'); + } else { + return ast; + } + } + + 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(MalAny 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.normal('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 as MalSymbol).name] = eval(val, newEnv); + } + return eval(args[1], newEnv); + }), + '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) { + 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.normal('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)), + ); + }), + ...ns, + }); +String rep(String str) => print(eval(read(str), replEnv)); + +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 { + final output = rep(input); + stdout.writeln(output); + } on ParserError catch (e) { + stdout.writeln(e.toString()); + } + } +} diff --git a/impls/dart3/bin/step5_tco.dart b/impls/dart3/bin/step5_tco.dart new file mode 100644 index 0000000000..522c0d4978 --- /dev/null +++ b/impls/dart3/bin/step5_tco.dart @@ -0,0 +1,150 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalAny read(String str) => readStr(str); +int depth = 0; +MalAny eval(MalAny 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 + ? ((List params) => MalClosure( + params, + env, + null, + list.third, + ).toTCO(null, true))( + 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.call(list.args, env), + final MalClosure fn => ( + fn.ast!, + 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(), + 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(maltype, true)}', + ); + } + depth--; + return maltype; + } +} + +String print(MalAny str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction( + 'def!', + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), + ), + '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', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + '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('time', (List args, Env env) { + final stopwatch = Stopwatch()..start(); + final ret = eval(args.first, env); + stopwatch.stop(); + println('time: ${stopwatch.elapsed}'); + return ret.toTCO(); + }), + ...ns, + }); +String rep(String str) => print(eval(read(str), replEnv)); + +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 { + final output = rep(input); + stdout.writeln(output); + } 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..5cf2f34b51 --- /dev/null +++ b/impls/dart3/bin/step6_file.dart @@ -0,0 +1,168 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalAny read(String str) => readStr(str); +int depth = 0; +MalAny eval(MalAny 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 + ? ((List 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.call(list.args, env), + 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(MalAny str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction( + 'def!', + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), + ), + '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', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + '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.toTCO(); + }), + ...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); + if (!stdin.hasTerminal) stdout.write('\n'); + 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/bin/step7_quote.dart b/impls/dart3/bin/step7_quote.dart new file mode 100644 index 0000000000..8c18eb9428 --- /dev/null +++ b/impls/dart3/bin/step7_quote.dart @@ -0,0 +1,204 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalAny 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)), +); + +MalAny quasiquote(MalAny 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; +MalAny eval(MalAny 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 + ? ((List 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); + }(), + 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.call(list.args, env), + 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(MalAny str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction( + 'def!', + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), + ), + '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', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + '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.toTCO(); + }), + ...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); + if (!stdin.hasTerminal) stdout.write('\n'); + 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/bin/step8_macros.dart b/impls/dart3/bin/step8_macros.dart new file mode 100644 index 0000000000..b5926d4952 --- /dev/null +++ b/impls/dart3/bin/step8_macros.dart @@ -0,0 +1,223 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalAny 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)), +); + +MalAny quasiquote(MalAny 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; +MalAny eval(MalAny 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 + ? ((List 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.call(list.args, env), + 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(MalAny str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction( + 'def!', + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), + ), + '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', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + '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.toTCO(); + }), + ...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); + if (!stdin.hasTerminal) stdout.write('\n'); + 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/bin/step9_try.dart b/impls/dart3/bin/step9_try.dart new file mode 100644 index 0000000000..fd169d606c --- /dev/null +++ b/impls/dart3/bin/step9_try.dart @@ -0,0 +1,250 @@ +import 'dart:core'; +import 'dart:core' as core show print; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +MalAny 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)), +); + +MalAny quasiquote(MalAny 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; +MalAny eval(MalAny 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 + ? ((List 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.call(list.args, env), + 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(MalAny str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction( + 'def!', + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), + ), + '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', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + '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.toTCO(); + }), + ...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); + if (!stdin.hasTerminal) stdout.write('\n'); + 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/bin/stepA_mal.dart b/impls/dart3/bin/stepA_mal.dart new file mode 100644 index 0000000000..e68eb8ca3a --- /dev/null +++ b/impls/dart3/bin/stepA_mal.dart @@ -0,0 +1,262 @@ +// 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'; + +MalAny 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)), +); + +MalAny quasiquote(MalAny 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; +MalAny eval(MalAny 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 + ? ((List 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.call(list.args, env), + 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(MalAny str) => prStr(str, true); + +final replEnv = globalEnv + ..addAll({ + 'def!': MalMacroFunction( + 'def!', + (List args, Env env) => + (env[(args[0] as MalSymbol).name] = eval(args[1], env)).toTCO(), + ), + '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', + ); + + for (final [key, val] in first.slices(2)) { + newEnv[key.malSymbolName] = eval(val, newEnv); + } + return (args.second, newEnv, true); + }), + '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.toTCO(); + }), + ...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 (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")'); + } + return; + } + 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; + try { + final output = rep(input); + stdout.writeln(output); + } on MalError 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 new file mode 100644 index 0000000000..da486dfc16 --- /dev/null +++ b/impls/dart3/lib/mal.dart @@ -0,0 +1,9 @@ +export 'src/types.dart'; +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 new file mode 100644 index 0000000000..159b68b854 --- /dev/null +++ b/impls/dart3/lib/src/core.dart @@ -0,0 +1,339 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; +import 'package:path/path.dart' as p; + +String getName(MalAny v) { + switch (v) { + case MalString(val: final str): + case MalSymbol(name: 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.', + ); + } +} + +int getInt(MalAny 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': MalMacroFunction('loglevel', (List args, Env env) { + if (args.isEmpty) { + return MalString(Logger.level.name).toTCO(); + } + + setLogLevel(getName(args.first)); + + return MalNil().toTCO(); + }), + '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) { + int dep = args.isEmpty ? 0 : getInt(args.first); + Env p = env; + while (dep > 0 && p.outer != null) { + p = p.outer!; + } + + return MalMap(p.data.map((k, v) => MapEntry(MalString(k), v))); + }), + '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(); + }), + '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('')), + ), + '+': 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), + ), + 'cons': MalFunction( + (List args, Env env) => MalList([ + args.first, + 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), + ), + 'vec': MalFunction( + (List args, Env env) => args.isNotEmpty + ? (args.first is! MalVector + ? MalVector(List.from(args.first.asMalListBase())) + : 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), + ), + '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); + }), + '>': 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()), + ), + 'pwd': MalFunction( + (List args, Env env) => MalString(Directory.current.path), + ), + 'read-string': MalFunction( + (List args, Env env) => readStr(args.first.stringVal), + ), + '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.stringVal); + if (!file.existsSync()) { + throw FileNotFoundError(p.normalize(file.absolute.path)); + } + return MalString(file.readAsStringSync()); + }), + 'atom': MalFunction((List args, Env env) => MalAtom(args.first)), + 'deref': MalFunction( + (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; + }), + 'swap!': MalFunction((List args, Env env) { + var atom = (args.first as MalAtom); + 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(), + '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))), + ), + ), + '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)), + ), + ), + ), + 'dissoc': MalFunction((args, env) { + var map = Map.from((args.first as MalMap).val); + args.sublist(1).forEach(map.remove); + return MalMap(map); + }), + '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()), + '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'); + 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, + ), +}; + +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( + (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))''', + 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/env.dart b/impls/dart3/lib/src/env.dart new file mode 100644 index 0000000000..752fa7ee03 --- /dev/null +++ b/impls/dart3/lib/src/env.dart @@ -0,0 +1,122 @@ +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); + } + } +} + +typedef RepFn = String Function(String str); + +class Env { + final Env? outer; + final Map 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), + ); + 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; + } + final val = exprs![i]; + this[name] = val; + logger.d( + '${' ' * (dep + 1)}bind $name on <${val.runtimeType}>${val.toStr()}', + ); + } + } + flags = Flags(this); + } + + 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) { + _builtinKeys.addAll(other.keys); + data.addAll(other); + } + + MalAny getSymbolVal(MalSymbol symbol) { + final key = symbol.name; + return data[key] ?? + outer?[key] ?? + (throw MalSymbolNotFound(symbol.token!).makeError()); + } + + 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; + + 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()}'; + 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'; + } + + 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 new file mode 100644 index 0000000000..b1b22e0a57 --- /dev/null +++ b/impls/dart3/lib/src/error.dart @@ -0,0 +1,72 @@ +import 'package:path/path.dart' as p; + +import 'types.dart'; + +abstract class MalError extends Error { + final String? message; + MalError(this.message); + @override + String toString() => + (message != null) // + ? '$runtimeType: $message' + : '$runtimeType'; +} + +class CustomThrowError extends MalError { + final MalAny err; + CustomThrowError(this.err):super(err.toStr()); + @override + String toString() => 'Error: $message'; +} + +/* 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 (cwd:${p.current})'); +} + +/* RuntimeError */ +abstract class RuntimeError extends MalError { + RuntimeError(super.message); +} + +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/print.dart b/impls/dart3/lib/src/print.dart new file mode 100644 index 0000000000..4c0df20dcb --- /dev/null +++ b/impls/dart3/lib/src/print.dart @@ -0,0 +1,33 @@ +import 'dart:io'; +import 'dart:collection'; + +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(MalAny 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 new file mode 100644 index 0000000000..511e1ed096 --- /dev/null +++ b/impls/dart3/lib/src/reader.dart @@ -0,0 +1,180 @@ +import 'dart:collection'; + +import 'package:mal/mal.dart'; + +class Reader { + final List _tokens; + late final int _len; + Reader(this._tokens) { + _len = _tokens.length; + } + int _index = 0; + + Token? next() { + var token = peek(); + _index++; + return token; + } + + Token? peek() { + if (_index >= _len) return null; + return _tokens[_index]; + } +} + +const specialDoubleChRe = r'''~@'''; +const specialSingleChRe = r'''[\[\]{}()'`~^@]'''; +const strRe = r'''"(?:\\.|[^\\"])*"?'''; +const commentRe = r''';.*'''; +const normalSeqRe = r'''[^\s\[\]{}('"`,;)]*'''; +final re = RegExp( + '[\\s,]*($specialDoubleChRe|$specialSingleChRe|$strRe|$commentRe|$normalSeqRe)', +); + +class Token { + const Token({ + required this.str, + required this.input, + required this.start, + required this.end, + }); + final String str; + final 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)}') : ''; + + @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) { + return re + .allMatches(str) + .map( + (e) => Token(str: e.group(1)!, input: str, start: e.start, end: e.end), + ) + .where((e) => e.str.isNotEmpty) + .toList(); +} + +final intRe = RegExp(r'^-?[0-9]+$'); +final strRe2 = RegExp(r'''"(?(?:\\.|[^\\"])*)"?'''); +MalAny readAtom(Reader reader) { + final token = reader.next(); + if (token == null) throw UnexpectedError('unexpecetd EOF'); + + if (intRe.hasMatch(token.str)) { + final val = int.parse(token.str); + return MalInt(val); + } 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.str[0] == ':') { + return MalKeyword(token.str.substring(1)); + } else if (token.str == 'nil') { + return MalNil(); + } else if (token.str == 'true') { + return MalBool(true); + } else if (token.str == 'false') { + return MalBool(false); + } + return MalSymbol(token); +} + +MalAny readList(Reader reader, ParenthesesType p) { + assert(reader.peek()!.str == p.left); + reader.next(); + final list = switch (p) { + .round => MalList(), + .square => MalVector(), + .curly => MalMap(), + }; + bool isKey = true; + MalAny key = nil; + while (true) { + final peek = reader.peek(); + if (peek == null) throw UnexpectedError('unexpecetd EOF'); + if (peek.str == p.right) { + reader.next(); + break; + } + switch (p) { + case .round: + case .square: + (list as ListBase).add(readForm(reader)); + break; + case .curly: + if (isKey) { + 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); + } + break; + } + isKey = !isKey; + } + return list as MalAny; +} + +const macros = { + "'": 'quote', + '`': 'quasiquote', + '~': 'unquote', + '~@': 'splice-unquote', + '@': 'deref', + '^': 'with-meta', +}; +MalAny readForm(Reader reader) { + var token = reader.peek(); + MalList readQuote(String token) { + reader.next(); + if (token == '^') { + final a = readForm(reader); + final b = readForm(reader); + return MalList([MalSymbol.builtin(macros[token]!), b, a]); + } else { + return MalList([MalSymbol.builtin(macros[token]!), readForm(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), + }; +} + +MalAny 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..a4d5b4f7d7 --- /dev/null +++ b/impls/dart3/lib/src/types.dart @@ -0,0 +1,464 @@ +import 'dart:collection'; + +import 'package:collection/collection.dart'; +import 'package:mal/mal.dart'; + +typedef MalAny = MalType; + +sealed class MalType { + final T val; + const MalType(this.val); + String toStr([bool printReadably = false]); + + @override + bool operator ==(covariant MalAny other) { + if (runtimeType != other.runtimeType) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; + + @override + String toString() => toStr(); +} + +extension MalTypeAs on MalAny { + @pragma('vm:prefer-inline') + int asInt() => (this as MalInt).val; + + @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 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 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 => 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') + MalAny get second => this[1]; + @pragma('vm:prefer-inline') + MalAny get third => this[2]; + @pragma('vm:prefer-inline') + MalAny 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 { + MalInt(super.val); + @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); +} + +class MalNil extends MalType { + const MalNil() : super(null); + @override + String toStr([bool printReadably = false]) => 'nil'; +} + +const nil = MalNil(); + +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 { + MalAny get ref => val.ref; + set ref(MalAny newVal) => val.ref = newVal; + + MalAtom(MalAny val) : super(MalTypeRef(val)); + @override + bool operator ==(covariant MalAny other) { + if (other is! MalAtom) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => Object.hashAll([MalAtom, ref]); + + @override + String toStr([bool printReadably = false]) => + '(atom ${val.ref.toStr(printReadably)})'; +} + +class MalBool extends MalType { + MalBool(super.val); + @override + String toStr([bool printReadably = false]) => val ? 'true' : 'false'; +} + +class MalKeyword extends MalType { + MalKeyword(String val) : super('\u029E$val'); + @override + String toStr([bool printReadably = false]) => ':${val.substring(1)}'; +} + +class MalString extends MalType { + MalString(super.val) + : assert(() { + logger.t('make MalString($val)'); + return true; + }()); + + @override + String toStr([bool printReadably = false]) => + printReadably == true ? '"${val.toPrintable()}"' : val; +} + +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; +} + +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; +} + +mixin MalMeta on MalType { + MalAny metadata = nil; + MalMeta clone(); +} + +abstract class MalListBase extends MalType> + with ListMixin, MalMeta { + MalListBase([List? list]) : super(list ?? []); + ParenthesesType get type; + @override + int get length => val.length; + + @override + set length(int newLength) => val.length = newLength; + + @override + E operator [](int index) => val[index]; + + @override + void operator []=(int index, E value) => val[index] = value; + + @override + String toStr([bool printReadably = false]) => + '${type.left}${val.map((e) => e.toStr(printReadably)).join(' ')}${type.right}'; + + @override + Iterable map(T Function(E e) f) => val.map(f); + + List get list => val; + + @override + void add(E element) => val.add(element); + + List get args => val.sublist(1); + + @override + bool operator ==(covariant MalAny other) => listCompare(this, other); + + @override + int get hashCode => Object.hashAll(val); + + bool listCompare(ListBase a, MalAny other) { + if (other is ListBase) { + return _listCompare(a, other as ListBase); + } else { + return false; + } + } +} + +class MalList extends MalListBase { + MalList([super.list]); + + @override + ParenthesesType get type => .round; + + @override + MalList clone() => MalList(List.from(list)); +} + +class MalVector extends MalListBase { + MalVector([super.list]); + + @override + ParenthesesType get type => .square; + + @override + MalVector clone() => MalVector(List.from(list)); +} + +class MalMap extends MalType> + with MapMixin, MalMeta { + MalMap([Map? map]) : super(map ?? {}); + + @override + operator [](covariant K key) => val[key]; + + @override + void operator []=(K key, V value) => val[key] = value; + + @override + void clear() => val.clear(); + + @override + Iterable get keys => val.keys; + + @override + remove(Object? key) => val.remove(key); + + @override + String toStr([bool printReadably = false]) { + if (shouldLog) { + 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 '{${val.entries.map((kv) => '${kv.key.toStr(true)} ${kv.value.toStr(printReadably)}').join(' ')}}'; + } + } + + @override + bool operator ==(covariant MalAny other) { + if (other is! MalMap) { + return false; + } + 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 + int get hashCode => Object.hashAll([...val.keys, ...val.values]); + + @override + MalMap clone() => MalMap(Map.from(val)); +} + +class MalSymbol extends MalType { + String get name => super.val; + final Token? token; + MalSymbol(this.token) : super(token!.str); + const MalSymbol.builtin(super.val) : token = null; + @override + String toStr([bool printReadably = false]) => name; +} + +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 + ? "'$name not found\n${super.val.tokenIndicator}" + : "'$name not found"; + String get name => super.val.str; + Error makeError() => KeyNotFoundError(toStr()); + + @override + bool operator ==(covariant MalAny other) { + if (other is! MalSymbolNotFound) { + return false; + } + return val == other.val; + } + + @override + int get hashCode => val.hashCode; +} + +typedef Fn = T Function(List args, Env env); +typedef FnMalType = Fn; +typedef FnTCO = Fn; +typedef MalClosureFn = MalAny Function(List args); + +mixin _MalCallable on MalType {} +typedef MalCallable = _MalCallable; + +abstract class MalFunctionBase extends MalType> + with _MalCallable, MalMeta { + MalFunctionBase(super.val); + Fn get fn => super.val; + + T call(List args, Env env) => fn(args, env); +} + +class MalFunction extends MalFunctionBase with _MalCallable, MalMeta { + MalFunction(super.val); + @override + String toStr([bool printReadably = false]) => '${fn.toString()}'; + + @override + MalFunction clone() => MalFunction(val); +} + +typedef TCO = (MalAny ast, Env? env, bool conti); + +extension ToTCO on MalAny { + TCO toTCO([Env? env, bool cont = false]) => (this, env, cont); +} + +/// without eval args +class MalMacroFunction extends MalFunctionBase with _MalCallable, MalMeta { + final String debugName; + MalMacroFunction(this.debugName, super.val); + + @Deprecated('only for step3/4') + factory MalMacroFunction.normal(String debugName, FnMalType fn) => + MalMacroFunction(debugName, (args, env) => fn(args, env).toTCO()); + + @Deprecated('only for step3/4') + MalAny callWithoutTCO(List args, Env env) => fn(args, env).$1; + + @override + 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, debugName]); + + @override + MalMacroFunction clone() => MalMacroFunction(debugName, val); +} + +class MalClosure extends MalType with _MalCallable, MalMeta { + MalClosureFn? get fn => super.val; + final List params; + final Env env; + final MalAny? ast; + final bool isMacro; + MalClosure( + this.params, + this.env, + MalClosureFn? fn, [ + this.ast, + this.isMacro = false, + ]) : super(fn); + + MalAny call(List args) => fn!(args); + + @override + String toStr([bool printReadably = false]) => + '# ${ast?.toStr(printReadably)}'; + + @override + bool operator ==(covariant MalAny other) { + if (other is! MalClosure) { + return false; + } + return val == other.val && + env == other.env && + ast == other.ast && + isMacro == other.isMacro && + _listCompare(params, other.params); + } + + @override + int get hashCode => Object.hashAll([fn, params, env, ast, isMacro]); + + bool get isNotMacro => !isMacro; + + @override + MalClosure clone({bool isMacro = false}) => + MalClosure(params, env, fn, ast, isMacro); +} 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/logger.dart b/impls/dart3/lib/src/utils/logger.dart new file mode 100644 index 0000000000..4092b89b81 --- /dev/null +++ b/impls/dart3/lib/src/utils/logger.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import 'package:logger/logger.dart'; +export 'package:logger/logger.dart'; + +import '../error.dart'; + +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) => filter.level = getLogLevelFromName(lv); +Level getLogLevel() => filter.level!; +bool get shouldLog => filter.level! <= Level.debug; + +Level getLogLevelFromName(String lv) { + if (levelNames.containsKey(lv)) { + return levelNames[lv]!; + } else { + throw ArgumentInvalidError( + '$lv is not valid, acceptable parameters are ${levelNames.keys}.', + ); + } +} 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..2903e33db7 --- /dev/null +++ b/impls/dart3/lib/src/utils/str_escape.dart @@ -0,0 +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', +}; +final needEscape = escapeMap.map((key, value) => MapEntry(value, key)) +// ..removeWhere((k, v) => k == '"') +; +const backslash = r'\'; + +extension Escape on String { + String escape() { + 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 out.toString(); + } + + String toPrintable() { + 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 out.toString(); + } +} diff --git a/impls/dart3/pubspec.lock b/impls/dart3/pubspec.lock new file mode 100644 index 0000000000..926fc11373 --- /dev/null +++ b/impls/dart3/pubspec.lock @@ -0,0 +1,413 @@ +# 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.dev" + source: hosted + version: "100.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" + url: "https://pub.dev" + source: hosted + version: "13.0.0" + ansicolor: + dependency: "direct main" + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + args: + dependency: "direct main" + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + meta: + dependency: "direct main" + description: + name: meta + sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739 + url: "https://pub.dev" + source: hosted + version: "1.18.2" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f + url: "https://pub.dev" + source: hosted + version: "1.31.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + url: "https://pub.dev" + source: hosted + version: "0.7.12" + test_core: + dependency: transitive + description: + name: test_core + sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 + url: "https://pub.dev" + source: hosted + version: "0.6.18" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + 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..6cfc9a75f8 --- /dev/null +++ b/impls/dart3/pubspec.yaml @@ -0,0 +1,20 @@ +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: + ansicolor: ^2.0.3 + args: ^2.7.0 + collection: ^1.19.1 + logger: ^2.7.0 + meta: ^1.18.2 + path: ^1.9.1 + +dev_dependencies: + lints: ^6.0.0 + test: ^1.25.6 diff --git a/impls/dart3/run b/impls/dart3/run new file mode 100755 index 0000000000..d794e77660 --- /dev/null +++ b/impls/dart3/run @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +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. +# 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 + +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 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..aa417180df --- /dev/null +++ b/impls/dart3/tool/generate_mal_tests.dart @@ -0,0 +1,473 @@ +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 '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) { + 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); +}