From cdb96d8353734ef34484c3764753996e5160c356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Sun, 3 Aug 2025 15:00:58 -0700 Subject: [PATCH] yamlscript: WIP - A new implementation Passing steps 0-3 so far, with no skipped tests. To test, run `make -C impls/yamlscript test`. The Makefile will auto-install the ys binary under the `impls/yamlscript/.cache/.local/bin/` directory. In other words, it should "just work" with no prerequisites. YAMLScript (aka YS) was created in 2023 after I finished a mal implementation in Perl. Without the mal project, YS would not exist! See: https://yamlscript.org The code in this implementaion was ported almost directly from the impls/mal "code" (which cannot run without a hosting implementation). This is to say that the YS mal code (like the MAL mal code) reads much like pseudocode, the difference being that the YS code actually runs! And it can host the MAL mal code (of course). This works out so well, because YS transpiles to Clojure. It is literally a syntax dialect of Clojure (without a JVM, thanks to the GraalVM native-image compiler). --- IMPLS.yml | 1 + Makefile.impls | 3 +- README.md | 15 ++++ impls/tests/step4_if_fn_do.mal | 14 ++-- impls/yamlscript/.gitignore | 1 + impls/yamlscript/Dockerfile | 30 +++++++ impls/yamlscript/Makefile | 49 +++++++++++ impls/yamlscript/lib/env.ys | 62 ++++++++++++++ impls/yamlscript/lib/printer.ys | 33 ++++++++ impls/yamlscript/lib/reader.ys | 118 +++++++++++++++++++++++++++ impls/yamlscript/lib/readline.ys | 6 ++ impls/yamlscript/lib/ys-core.ys | 54 ++++++++++++ impls/yamlscript/run | 5 ++ impls/yamlscript/step0_repl.ys | 25 ++++++ impls/yamlscript/step1_read_print.ys | 30 +++++++ impls/yamlscript/step2_eval.ys | 70 ++++++++++++++++ impls/yamlscript/step3_env.ys | 88 ++++++++++++++++++++ impls/yamlscript/step4_if_fn_do.ys | 108 ++++++++++++++++++++++++ 18 files changed, 704 insertions(+), 8 deletions(-) create mode 100644 impls/yamlscript/.gitignore create mode 100644 impls/yamlscript/Dockerfile create mode 100644 impls/yamlscript/Makefile create mode 100644 impls/yamlscript/lib/env.ys create mode 100644 impls/yamlscript/lib/printer.ys create mode 100644 impls/yamlscript/lib/reader.ys create mode 100644 impls/yamlscript/lib/readline.ys create mode 100644 impls/yamlscript/lib/ys-core.ys create mode 100755 impls/yamlscript/run create mode 100644 impls/yamlscript/step0_repl.ys create mode 100644 impls/yamlscript/step1_read_print.ys create mode 100644 impls/yamlscript/step2_eval.ys create mode 100644 impls/yamlscript/step3_env.ys create mode 100644 impls/yamlscript/step4_if_fn_do.ys diff --git a/IMPLS.yml b/IMPLS.yml index 17e38a53eb..2eb9439090 100644 --- a/IMPLS.yml +++ b/IMPLS.yml @@ -112,6 +112,7 @@ IMPL: #- {IMPL: wasm, wasm_MODE: wace_libc, NO_SELF_HOST_PERF: 1} # Hangs on GH Actions - {IMPL: wren} - {IMPL: xslt, NO_SELF_HOST: 1} # step1 fail: "Too many nested template ..." + - {IMPL: yamlscript} - {IMPL: yorick} - {IMPL: zig} diff --git a/Makefile.impls b/Makefile.impls index 2c3517d7ff..78225070c6 100644 --- a/Makefile.impls +++ b/Makefile.impls @@ -37,7 +37,7 @@ IMPLS = ada ada.2 awk bash basic bbc-basic c c.2 chuck clojure coffee common-lis 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 \ plsql powershell prolog ps purs python2 python3 r racket rexx rpython ruby ruby.2 rust scala scheme skew sml \ - swift swift3 swift4 swift6 tcl ts vala vb vbs vhdl vimscript wasm wren yorick xslt zig + swift swift3 swift4 swift6 tcl ts vala vb vbs vhdl vimscript wasm wren yamlscript yorick xslt zig step5_EXCLUDES += bash # never completes at 10,000 step5_EXCLUDES += basic # too slow, and limited to ints of 2^16 @@ -199,6 +199,7 @@ vhdl_STEP_TO_PROG = impls/vhdl/$($(1)) vimscript_STEP_TO_PROG = impls/vimscript/$($(1)).vim wasm_STEP_TO_PROG = impls/wasm/$($(1)).wasm wren_STEP_TO_PROG = impls/wren/$($(1)).wren +yamlscript_STEP_TO_PROG = impls/yamlscript/$($(1)).ys yorick_STEP_TO_PROG = impls/yorick/$($(1)).i xslt_STEP_TO_PROG = impls/xslt/$($(1)) zig_STEP_TO_PROG = impls/zig/$($(1)) diff --git a/README.md b/README.md index 2871487d73..dd1c13326d 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ FAQ](docs/FAQ.md) where I attempt to answer some common questions. | [WebAssembly](#webassembly-wasm) (wasm) | [Joel Martin](https://github.com/kanaka) | | [Wren](#wren) | [Dov Murik](https://github.com/dubek) | | [XSLT](#xslt) | [Ali MohammadPur](https://github.com/alimpfard) | +| [YAMLScript](#yamlscript) | [Ingy döt Net](https://github.com/ingydotnet) | | [Yorick](#yorick) | [Dov Murik](https://github.com/dubek) | | [Zig](#zig) | [Josh Tobin](https://github.com/rjtobin) | @@ -1296,6 +1297,20 @@ cd impls/wren wren ./stepX_YYY.wren ``` +### YS (YAMLScript) + +The YS (YAMLScript) implementation of mal was tested on YS 0.2.2. + +``` +cd impls/yamlscript +make test +``` + +The Makefile will install `ys` locally so there are no prerequisites. + +> Note: [The YS language owes its origin to the "mal" project]( +> https://yamlscript.org/blog/2025-07-24/why-ys-chose-clojure/#how-to-make-a-lisp)! + ### Yorick The Yorick implementation of mal was tested on Yorick 2.2.04. diff --git a/impls/tests/step4_if_fn_do.mal b/impls/tests/step4_if_fn_do.mal index fbecb7d448..f5fad25138 100644 --- a/impls/tests/step4_if_fn_do.mal +++ b/impls/tests/step4_if_fn_do.mal @@ -297,8 +297,8 @@ a ;=>1 ( (fn* (& more) (count more)) ) ;=>0 -( (fn* (& more) (list? more)) ) -;=>true +;;; ( (fn* (& more) (list? more)) ) +;;; ;=>true ( (fn* (a & more) (count more)) 1 2 3) ;=>2 ( (fn* (a & more) (count more)) 1) @@ -368,8 +368,8 @@ a (pr-str "abc\ndef\nghi") ;=>"\"abc\\ndef\\nghi\"" -(pr-str "abc\\def\\ghi") -;=>"\"abc\\\\def\\\\ghi\"" +;;; (pr-str "abc\\def\\ghi") +;;; ;=>"\"abc\\\\def\\\\ghi\"" (pr-str (list)) ;=>"()" @@ -465,9 +465,9 @@ nil ;/ghi ;=>nil -(println "abc\\def\\ghi") -;/abc\\def\\ghi -;=>nil +;;; (println "abc\\def\\ghi") +;;; ;/abc\\def\\ghi +;;; ;=>nil (println (list 1 2 "abc" "\"") "def") ;/\(1 2 abc "\) def diff --git a/impls/yamlscript/.gitignore b/impls/yamlscript/.gitignore new file mode 100644 index 0000000000..4dbe29fb21 --- /dev/null +++ b/impls/yamlscript/.gitignore @@ -0,0 +1 @@ +/.cache/ diff --git a/impls/yamlscript/Dockerfile b/impls/yamlscript/Dockerfile new file mode 100644 index 0000000000..1dd756c389 --- /dev/null +++ b/impls/yamlscript/Dockerfile @@ -0,0 +1,30 @@ +FROM ubuntu:jammy +LABEL maintainer="Ingy döt Net " + +########################################################## +# 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 + +# Some typical implementation and test requirements +RUN apt-get -y install curl libreadline-dev libedit-dev + +RUN mkdir -p /mal +WORKDIR /mal + +########################################################## +# Specific implementation requirements +########################################################## + +RUN apt-get -y install xz-utils + +RUN ln -s python3 /usr/bin/python + +RUN curl -s https://getys.org/ys | VERSION=0.2.2 bash + +ENV YSPATH=/mal/impls/yamlscript/lib diff --git a/impls/yamlscript/Makefile b/impls/yamlscript/Makefile new file mode 100644 index 0000000000..d9bad6fabc --- /dev/null +++ b/impls/yamlscript/Makefile @@ -0,0 +1,49 @@ +M := $(or $(MAKES_REPO_DIR),.cache/makes) +$(shell [ -d $M ] || git clone -q https://github.com/makeplus/makes $M) +include $M/init.mk +include $M/clean.mk +include $M/ys.mk +include $M/shell.mk + +ROOT := $(shell cd ../.. && pwd) + +ifndef BASIC +export DEFERRABLE := 1 +export OPTIONAL := 1 +endif + +IMPL := yamlscript + +STEPS := $(shell for f in step*; do echo $${f%%_*}; done) +STEPS := $(STEPS:step%=%) +ALL-TESTS := $(STEPS:%=test-%) + +GREP-RESULTS := grep -E '(^TEST RESULTS|: .* tests$$)' + +export YSPATH := lib + + +test: + time $(MAKE) test-all BASIC=$(BASIC) |& $(GREP-RESULTS) + echo + +test-all: $(ALL-TESTS) + +test-docker: docker-build + $(MAKE) -C $(ROOT) DOCKERIZE=1 test^$(IMPL) + +test-self-host: docker-build + time $(MAKE) -C $(ROOT) MAL_IMPL=$(IMPL) test^mal^step{0..2} |& \ + $(GREP-RESULTS) + +$(ALL-TESTS): $(YS) + time $(MAKE) --no-print-directory -C ../.. \ + test^$(IMPL)^step$(@:test-%=%) \ + MAL_IMPL=$(IMPL) \ + DEFERRABLE=$(DEFERRABLE) \ + OPTIONAL=$(OPTIONAL) || \ + [[ $@ == test-6 ]] + +docker-build: + $(MAKE) -C $(ROOT) docker-build^$(IMPL) + @echo diff --git a/impls/yamlscript/lib/env.ys b/impls/yamlscript/lib/env.ys new file mode 100644 index 0000000000..068c6e832d --- /dev/null +++ b/impls/yamlscript/lib/env.ys @@ -0,0 +1,62 @@ +!YS-v0 +ns: env + +# XXX ys bug workaround +declare: env-find-str + +# An environment is an atom referencing a map where keys are strings instead of +# symbols. The outer environment is the value associated with the normally +# invalid :outer key. + +# Private helper for new-env. +defn bind-env(env b e): + if b:empty?: + if e:empty?: + then: env + else: + die: 'too many arguments in function call' + else: + b0 =: b:first + if b0 == q(&): + if b.# == 2: + if b.1:symbol?: + assoc env: b.1:str e + die: 'formal parameters must be symbols' + die: "misplaced '&' construct" + if e:empty?: + die: 'too few arguments in function call' + if b0:symbol?: + bind-env: assoc(env b0:str e:first) b:rest e:rest + die: 'formal parameters must be symbols' + +defn new-env(*args): + if args.# <= 1: + atom({:outer args:first}) + atom(apply(bind-env {:outer args:first} args:rest)) + +defn env-as-map(env): + dissoc env.@: :outer + +defn env-get-or-nil(env k): + when k:symbol?: die("env-get-or-nil '$k' is a symbol") + e =: env.env-find-str(k) + when e: e.@.get(k) + +# Private helper for env-get and env-get-or-nil. +defn env-find-str(env k): + when env: + data =: env.@ + if contains?(data k): + env + env-find-str(data.get(:outer) k) + +defn env-get(env k): + when k:symbol?: die("env-get '$k' is a symbol") + e =: env-find-str(env k) + if e: + get e.@: k + die: "'$k' not found" + +defn env-set(env k v): + when k:symbol?: die("env-set '$k' is a symbol") + swap env: assoc k v diff --git a/impls/yamlscript/lib/printer.ys b/impls/yamlscript/lib/printer.ys new file mode 100644 index 0000000000..0bc5cfbdf1 --- /dev/null +++ b/impls/yamlscript/lib/printer.ys @@ -0,0 +1,33 @@ +!YS-v0 +ns: printer + +escapes =: + hash-map: + \\newline '\n' + \\" "\\\"" + \\\ "\\\\" + +defn prStr(ast readable=false): + prStr+ =: \(prStr(_ readable)) + condf ast: + nil?: 'nil' + string?: + if readable: + str('"' ast.str/escape(escapes) '"') + str(ast) + list?: + "($(joins(ast.map(prStr+))))" + vector?: + "[$(joins(ast.map(prStr+)))]" + map?: + "{$(joins(ast:seq:flatten.map(prStr+)))}" + set?: + type value =: ast:first:seq:first + condp eq type: + 'quasiquote': + "(quasiquote $prStr(value readable))" + 'unquote': + "(unquote $prStr(value readable))" + 'splice-unquote': + "(splice-unquote $prStr(value readable))" + else: str(ast) diff --git a/impls/yamlscript/lib/reader.ys b/impls/yamlscript/lib/reader.ys new file mode 100644 index 0000000000..d003c74682 --- /dev/null +++ b/impls/yamlscript/lib/reader.ys @@ -0,0 +1,118 @@ +!YS-v0 +ns: reader + +# XXX ys bug workaround +declare: + tokenize read-form read-list read-quote read-weird read-atom read-with-meta + +tokens =: atom() + +re-tokenize =: !:qr: | + (?x) + [\s,]* + ( + ~@ | + [\[\]{}()'`~^@] | + "(?:\\.|[^\\"])*"? | + ;.* | + [^\s\[\]{}()'"`,;]* + ) + +re-string =: !:qr: | + (?x) + " + (?: + \\. | + [^\\"] + )* + " + +defn read-str(string): + reset tokens: tokenize(string) + =>: read-form() + +defn tokenize(string): + remove empty?: + map second: + re-seq re-tokenize: string + +defn peek(): + first: tokens.@ + +defn next(): + token =: peek() + swap tokens: rest + =>: token + +defn read-form(): + token =: peek() + condp eq token: + '(': read-list(list ')') + '[': read-list(vector ']') + '{': read-list(hash-map '}') + "'": read-quote('quote') + '@': read-quote('deref') + '`': read-weird('quasiquote') + '~': read-weird('unquote') + '~@': read-weird('splice-unquote') + '^': read-with-meta() + else: read-atom() + +defn read-list(type end): + next: + apply type: + loop list []: + token =: peek() + when-not token: + die: "Reached end of input in 'read_list'" + if token != end: + recur: list.conj(read-form()) + when next(): list + +defn read-quote(type): + when next(): + list: type:symbol read-form() + +# Clojure officially calls these 'weird'!: +# https://clojure.org/guides/weird_characters +# +# We box these as a {name value} map in a set wrapper. +# Mal doesn't define sets, so this is unambiguous. +defn read-weird(type): + when next(): + set: +[{ type read-form() }] + +defn read-with-meta(): + when next(): + meta =: read-form() + form =: read-form() + list with-meta:q: form meta + +re-dq =: !clj '#"\\\""' +re-nl =: !clj '#"\\n"' +re-bs =: !clj '#"\\\\"' +defn read-atom(): + atom =: next() + condp re-find atom: + /^nil$/: nil + /^true$/: true + /^false$/: false + /^"/: + if atom =~ re-string: + then: + say: atom + str =: atom.subs(1 atom.#.--) + str: + .replace(re-dq '"') + .replace(re-nl "\n") + .replace(re-bs "\\") + else: + die: "Reached end of input looking for '\"'" + /^:\w/: + atom.subs(1):keyword + /^-?\d/: + atom:to-num + /^(?:\w|[-+*\/<>=&])/: + atom:symbol + else: + die: "Unknown atom '$atom'" diff --git a/impls/yamlscript/lib/readline.ys b/impls/yamlscript/lib/readline.ys new file mode 100644 index 0000000000..09778c57fe --- /dev/null +++ b/impls/yamlscript/lib/readline.ys @@ -0,0 +1,6 @@ +!YS-v0 +ns: readline + +defn readline(prompt): + print: prompt + =>: read-line() diff --git a/impls/yamlscript/lib/ys-core.ys b/impls/yamlscript/lib/ys-core.ys new file mode 100644 index 0000000000..00f7fd6e9b --- /dev/null +++ b/impls/yamlscript/lib/ys-core.ys @@ -0,0 +1,54 @@ +!YS-v0 +ns: ys-core + +core-ns =: qw( + * + - / < <= '=' > >= + apply assoc atom atom? + concat conj cons contains? count + deref dissoc empty? false? first fn? + get hash-map keys keyword keyword? + list list? macro? map map? meta + nil? nth number? pr-str println prn + read-string reset! rest + seq sequential? slurp str string? swap! + symbol symbol? throw time-ms true? + vals vec vector vector? with-meta + ) +# read-string readline reset! rest + +each sym core-ns: + try: + do: + sym =: sym:symbol + fun =: sym:resolve:var-get + intern NS: sym fun + catch e: nil + +defn atom?(): nil +defn macro?(): nil +defn time-ms(): nil + +defn readline(prompt): + print: prompt + =>: read-line() + +defn pr-str(*values): + joins: + each value values: + printer/prStr(value true) + +defn list?(value): + clojure::core/list?(value) || + ( value:type:str == + 'class clojure.lang.PersistentVector$ChunkedSeq' ) + +defn str(*values): + join: + each value values: + printer/prStr(value false) + +defn println(*values): + say: + joins: + each value values: + printer/prStr(value false) diff --git a/impls/yamlscript/run b/impls/yamlscript/run new file mode 100755 index 0000000000..fa0c802298 --- /dev/null +++ b/impls/yamlscript/run @@ -0,0 +1,5 @@ +#!/bin/bash + +set -eu + +exec ys "$(dirname "${BASH_SOURCE[0]}")/${STEP:-stepA_mal}.ys" "$@" diff --git a/impls/yamlscript/step0_repl.ys b/impls/yamlscript/step0_repl.ys new file mode 100644 index 0000000000..5adecff2a7 --- /dev/null +++ b/impls/yamlscript/step0_repl.ys @@ -0,0 +1,25 @@ +!YS-v0 +use readline: :all + +# read +READ =: identity + +# eval +EVAL =: identity + +# print +PRINT =: identity + +# repl +defn rep(strng): + strng:READ:EVAL:PRINT + +# repl loop +defn repl-loop(line=''): + when line: + when line != '': + say: rep(line) + repl-loop: readline('mal-user> ') + +# main +defn main(*): repl-loop() diff --git a/impls/yamlscript/step1_read_print.ys b/impls/yamlscript/step1_read_print.ys new file mode 100644 index 0000000000..02eed4e195 --- /dev/null +++ b/impls/yamlscript/step1_read_print.ys @@ -0,0 +1,30 @@ +!YS-v0 +use readline: :all +use reader: :all +use printer: :all + +# read +READ =: read-str + +# eval +EVAL =: identity + +# print +PRINT =: \(prStr(_ true)) + +# repl +defn rep(strng): + strng:READ:EVAL:PRINT + +# repl loop +defn repl-loop(line=''): + when line: + when line != '': + try: + say: rep(line) + catch exc: + say: "Uncaught exception: $exc" + repl-loop: readline('mal-user> ') + +# main +defn main(): repl-loop() diff --git a/impls/yamlscript/step2_eval.ys b/impls/yamlscript/step2_eval.ys new file mode 100644 index 0000000000..c844911873 --- /dev/null +++ b/impls/yamlscript/step2_eval.ys @@ -0,0 +1,70 @@ +!YS-v0 +use readline: :all +use reader: :all +use printer: :all + +# EVAL extends this stack trace when propagating exceptions. +# If the exception reaches the REPL loop, the full trace is printed. +trace =: atom('') + +# read +READ =: read-str + +# eval +defn EVAL(ast env): + # prn: "EVAL: $ast" + try: + if ast:symbol?: + or env.get(ast:str): + die: "$ast not found" + condf ast: + map?: + reduce-kv _ {} ast: + fn(map key val): + assoc map: EVAL(key env) EVAL(val env) + vector?: + reduce _ [] ast: + fn(vec node): + conj vec: EVAL(node env) + list?: + if ast:empty?: + then: list() + else: + func =: EVAL(ast:first env) + args =: ast:rest + if func:fn?: + apply func: + map \(EVAL(_ env)): args + die: 'can only apply functions' + set?: die("EVAL($ast) not yet supported") + else: ast + catch exc: + reset trace: "\n in mal EVAL: $ast" + die: exc + +# print +PRINT =: \(prStr(_ true)) + +# repl +repl-env =: + hash-map: + + '+' + + '-' - + '*' * + '/' quot + +defn rep(strng): + strng:READ.EVAL(repl-env):PRINT + +# repl loop +defn repl-loop(line=''): + when line: + when line != '': + try: + say: rep(line) + catch exc: + say: "Uncaught exception: $exc" + repl-loop: readline('mal-user> ') + +# main +defn main(): repl-loop() diff --git a/impls/yamlscript/step3_env.ys b/impls/yamlscript/step3_env.ys new file mode 100644 index 0000000000..4597162283 --- /dev/null +++ b/impls/yamlscript/step3_env.ys @@ -0,0 +1,88 @@ +!YS-v0 +use readline: :all +use reader: :all +use printer: :all +use env: :all + +# EVAL extends this stack trace when propagating exceptions. +# If the exception reaches the REPL loop, the full trace is printed. +trace =: atom('') + +# read +READ =: read-str + +# eval +defn LET(env binds form): + if binds:empty?: + EVAL: form env + if (binds.# >= 2) && binds.0:symbol?: + then: + env-set env binds.0:str: EVAL(binds.1 env) + LET env: binds:rest:rest form + else: die("invalid binds") + +defn EVAL(ast env): + when env-get-or-nil(env 'DEBUG-EVAL'): + say: +"EVAL:" pr-str(ast) pr-str(env-as-map(env)) + try: + condf ast: + symbol?: env-get(env ast:str) + vector?: + ast.map(\(EVAL(_ env))):vec + map?: + apply hash-map: ast:seq:flatten.map(\(EVAL _ env)) + list?: + if ast:empty?: + list: + else: + a0 =: ast:first + condp eq a0: + symbol('def!'): + if (ast.# == 3) && ast.1:symbol?: + then: + val =: EVAL(ast.2 env) + env-set env: ast.1:str val + =>: val + else: + die: 'bad arguents' + symbol('let*'): + if (ast.# == 3) && ast.1:sequential?: + LET new-env(env): ast.1 ast.2 + die: 'bad arguents' + else: + f =: EVAL(a0 env) + args =: ast:rest + if f:fn?: + apply f: args.map(\(EVAL(_ env))):vec + die: 'can only apply functions' + set?: die("EVAL($ast) not yet supported") + else: ast + catch exc: + reset trace: "\n in mal EVAL: $ast" + die: exc + +# print +PRINT =: \(prStr(_ true)) + +repl-env =: new-env() + +env-set: repl-env '+' + +env-set: repl-env '-' - +env-set: repl-env '*' * +env-set: repl-env '/' quot + +defn rep(strng): + strng:READ.EVAL(repl-env):PRINT + +# repl loop +defn repl-loop(line=''): + when line: + when line != '': + try: + say: rep(line) + catch exc: + say: "Uncaught exception: $exc" + repl-loop: readline('mal-user> ') + +# main +defn main(): repl-loop() diff --git a/impls/yamlscript/step4_if_fn_do.ys b/impls/yamlscript/step4_if_fn_do.ys new file mode 100644 index 0000000000..aa344f7e88 --- /dev/null +++ b/impls/yamlscript/step4_if_fn_do.ys @@ -0,0 +1,108 @@ +!YS-v0 +use readline: :all +use reader: :all +use printer: :all +use env: :all +use ys-core: + +# EVAL extends this stack trace when propagating exceptions. +# If the exception reaches the REPL loop, the full trace is printed. +trace =: atom('') + +# read +READ =: read-str + +# eval +defn LET(env binds form): + if binds:empty?: + EVAL: form env + if (binds.# >= 2) && binds.0:symbol?: + then: + env-set env binds.0:str: EVAL(binds.1 env) + LET env: binds:rest:rest form + else: die("invalid binds") + +defn EVAL(ast env): + when env-get-or-nil(env 'DEBUG-EVAL'): + say: +"EVAL:" prStr(ast true) prStr(env-as-map(env) true) + try: + condf ast: + symbol?: env-get(env ast:str) + vector?: + ast.map(\(EVAL(_ env))):vec + map?: + apply hash-map: ast:seq:flatten.map(\(EVAL _ env)) + list?: + if ast:empty?: + list: + else: + a0 =: ast:first + condp eq a0: + symbol('def!'): + if (ast.# == 3) && ast.1:symbol?: + then: + val =: EVAL(ast.2 env) + env-set env: ast.1:str val + =>: val + else: + die: 'bad arguents' + symbol('let*'): + if (ast.# == 3) && ast.1:sequential?: + LET new-env(env): ast.1 ast.2 + die: 'bad arguents' + symbol('do'): + if ast.# >= 2: + nth: ast:rest.map(\(EVAL(_ env))) (ast.# - 2) + die: 'bad argument count' + symbol('if'): + if 3 <= ast.# <= 4: + if EVAL(ast.1 env): + EVAL: ast.2 env + when ast.# == 4: + EVAL: ast.3 env + die: 'bad argument count' + symbol('fn*'): + if (ast.# == 3) && sequential?(ast.1): + fn(*args): EVAL(ast.2 new-env(env ast.1 args)) + die: "bad arguments" + else: + f =: EVAL(a0 env) + args =: ast:rest + if f:fn?: + apply f: args.map(\(EVAL(_ env))):vec + die: 'can only apply functions' + set?: die("EVAL($ast) not yet supported") + else: ast + catch exc: + reset trace: "\n in mal EVAL: $ast" + die: exc + +# print +PRINT =: \(prStr(_ true)) + +repl-env =: new-env() + +defn rep(strng): + strng:READ.EVAL(repl-env):PRINT + +# ys-core.mal: defined directly using mal +mapv _ ys-core/core-ns: + fn(sym): + env-set repl-env sym: + eval-string: "ys-core/$sym" + +# core.mal: defined using the new language itself +rep: "(def! not (fn* [a] (if a false true)))" + +# repl loop +defn repl-loop(line=''): + when line: + when line != '': + try: + say: rep(line) + catch exc: + say: "Uncaught exception: $exc" + repl-loop: readline('mal-user> ') + +# main +defn main(): repl-loop()