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()