diff --git a/.travis.yml b/.travis.yml index 4b64719f..df389e3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,7 @@ matrix: - {env: IMPL=hy, services: [docker]} - {env: IMPL=io NO_SELF_HOST_PERF=1, services: [docker]} # perf OOM - {env: IMPL=java, services: [docker]} + - {env: IMPL=jq, services: [docker]} - {env: IMPL=js, services: [docker]} - {env: IMPL=julia, services: [docker]} - {env: IMPL=kotlin, services: [docker]} diff --git a/Makefile b/Makefile index bb63177b..fbaaf6cc 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ DOCKERIZE = IMPLS = ada ada.2 awk bash basic bbc-basic c chuck clojure coffee common-lisp cpp crystal cs d dart \ elisp elixir elm erlang es6 factor fantom forth fsharp go groovy gnu-smalltalk \ - guile haskell haxe hy io java js julia kotlin livescript logo lua make mal \ + guile haskell haxe hy io java js jq julia kotlin livescript logo lua make mal \ matlab miniMAL nasm nim objc objpascal ocaml perl perl6 php picolisp pike plpgsql \ plsql powershell ps python python.2 r racket rexx rpython ruby rust scala scheme skew \ swift swift3 swift4 swift5 tcl ts vala vb vhdl vimscript wasm wren yorick zig @@ -218,6 +218,7 @@ hy_STEP_TO_PROG = hy/$($(1)).hy io_STEP_TO_PROG = io/$($(1)).io java_STEP_TO_PROG = java/target/classes/mal/$($(1)).class js_STEP_TO_PROG = js/$($(1)).js +jq_STEP_PROG = jq/$($(1)).jq julia_STEP_TO_PROG = julia/$($(1)).jl kotlin_STEP_TO_PROG = kotlin/$($(1)).jar livescript_STEP_TO_PROG = livescript/$($(1)).js diff --git a/README.md b/README.md index 2ce8169b..402a259c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **1. Mal is a Clojure inspired Lisp interpreter** -**2. Mal is implemented in 80 languages (83 different implementations and 103 runtime modes)** +**2. Mal is implemented in 81 languages (84 different implementations and 104 runtime modes)** | Language | Creator | | -------- | ------- | @@ -45,6 +45,7 @@ | [Io](#io) | [Dov Murik](https://github.com/dubek) | | [Java](#java-17) | [Joel Martin](https://github.com/kanaka) | | [JavaScript](#javascriptnode) ([Demo](http://kanaka.github.io/mal)) | [Joel Martin](https://github.com/kanaka) | +| [Jq](#jq) | [Ali MohammadPur](https://github.com/alimpfard) | | [Julia](#julia) | [Joel Martin](https://github.com/kanaka) | | [Kotlin](#kotlin) | [Javier Fernandez-Ivern](https://github.com/ivern) | | [LiveScript](#livescript) | [Jos van Bakel](https://github.com/c0deaddict) | @@ -604,6 +605,17 @@ cd julia julia stepX_YYY.jl ``` +### Jq + +Tested against version 1.6, with a lot of cheating in the IO department + +``` +cd jq +STEP=stepA_YYY ./run + # with Debug +DEBUG=true STEP=stepA_YYY ./run +``` + ### Kotlin The Kotlin implementation of mal has been tested with Kotlin 1.0. diff --git a/jq/Dockerfile b/jq/Dockerfile new file mode 100644 index 00000000..80d2c08d --- /dev/null +++ b/jq/Dockerfile @@ -0,0 +1,32 @@ +FROM ubuntu:bionic +MAINTAINER Joel Martin + +########################################################## +# General requirements for testing or common across many +# implementations +########################################################## + +RUN apt-get -y update + +# Required for running tests +RUN apt-get -y install make python + +# Some typical implementation and test requirements +RUN apt-get -y install curl libreadline-dev libedit-dev libpcre3-dev + +RUN mkdir -p /mal +WORKDIR /mal + +######################################################### +# Specific implementation requirements +######################################################### + +RUN apt-get -y install python3.8 wget +RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.8 10 + +# grab jq 1.6 from github releases +RUN wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 + +RUN chmod +x jq-linux64 +# a bit ugly, but it'll do? +RUN mv jq-linux64 /usr/bin/jq diff --git a/jq/Makefile b/jq/Makefile new file mode 100644 index 00000000..1263948f --- /dev/null +++ b/jq/Makefile @@ -0,0 +1 @@ +all: diff --git a/jq/core.jq b/jq/core.jq new file mode 100644 index 00000000..2ba743a9 --- /dev/null +++ b/jq/core.jq @@ -0,0 +1,485 @@ +include "utils"; +include "printer"; +include "reader"; + +def core_identify: + { + "env": { + kind: "fn", + function: "env", + inputs: 0 + }, + "prn": { + kind: "fn", + function: "prn", + inputs: -1 + }, + "pr-str": { + kind: "fn", + function: "pr-str", + inputs: -1 + }, + "str": { + kind: "fn", + function: "str", + inputs: -1 + }, + "println": { + kind: "fn", + function: "println", + inputs: -1 + }, + "list": { + kind: "fn", + function: "list", + inputs: -1 + }, + "list?": { + kind: "fn", + function: "list?", + inputs: 1 + }, + "empty?": { + kind: "fn", + function: "empty?", + inputs: 1 + }, + "count": { + kind: "fn", + function: "count", + inputs: 1 + }, + "=": { + kind: "fn", + function: "=", + inputs: 2 + }, + "<": { + kind: "fn", + function: "<", + inputs: 2 + }, + "<=": { + kind: "fn", + function: "<=", + inputs: 2 + }, + ">": { + kind: "fn", + function: ">", + inputs: 2 + }, + ">=": { + kind: "fn", + function: ">=", + inputs: 2 + }, + "read-string": { + kind: "fn", + function: "read-string", + inputs: 1 + }, + "slurp": { + kind: "fn", + function: "slurp", + inputs: 1 + }, + "atom": { + kind: "fn", + function: "atom", + inputs: 1 + }, + "atom?": { + kind: "fn", + function: "atom?", + inputs: 1 + }, + "deref": { + kind: "fn", + function: "deref", + inputs: 1 + }, + "reset!": { # defined in interp + kind: "fn", + function: "reset!", + inputs: 2 + }, + "swap!": { # defined in interp + kind: "fn", + function: "swap!", + inputs: -3 + }, + "cons": { + kind: "fn", + function: "cons", + inputs: 2 + }, + "concat": { + kind: "fn", + function: "concat", + inputs: -1 + }, + "nth": { + kind: "fn", + function: "nth", + inputs: 2 + }, + "first": { + kind: "fn", + function: "first", + inputs: 1 + }, + "rest": { + kind: "fn", + function: "rest", + inputs: 1 + }, + "throw": { + kind: "fn", + function: "throw", + inputs: 1 + }, + "apply": { # defined in interp + kind: "fn", + function: "apply", + inputs: -3 + }, + "map": { # defined in interp + kind: "fn", + function: "map", + inputs: 2 + }, + "nil?": { + kind: "fn", + function: "nil?", + inputs: 1 + }, + "true?": { + kind: "fn", + function: "true?", + inputs: 1 + }, + "false?": { + kind: "fn", + function: "false?", + inputs: 1 + }, + "symbol": { + kind: "fn", + function: "symbol", + inputs: 1 + }, + "symbol?": { + kind: "fn", + function: "symbol?", + inputs: 1 + }, + "keyword": { + kind: "fn", + function: "keyword", + inputs: 1 + }, + "keyword?": { + kind: "fn", + function: "keyword?", + inputs: 1 + }, + "vector": { + kind: "fn", + function: "vector", + inputs: -1 + }, + "vector?": { + kind: "fn", + function: "vector?", + inputs: 1 + }, + "sequential?": { + kind: "fn", + function: "sequential?", + inputs: 1 + }, + "hash-map": { + kind: "fn", + function: "hash-map", + inputs: -1 + }, + "map?": { + kind: "fn", + function: "map?", + inputs: 1 + }, + "assoc": { + kind: "fn", + function: "assoc", + inputs: -2 + }, + "dissoc": { + kind: "fn", + function: "dissoc", + inputs: -2 + }, + "get": { + kind: "fn", + function: "get", + inputs: 2 + }, + "contains?": { + kind: "fn", + function: "contains?", + inputs: 2 + }, + "keys": { + kind: "fn", + function: "keys", + inputs: 1 + }, + "vals": { + kind: "fn", + function: "vals", + inputs: 1 + }, + "string?": { + kind: "fn", + function: "string?", + inputs: 1 + }, + "fn?": { + kind: "fn", + function: "fn?", + inputs: 1 + }, + "number?": { + kind: "fn", + function: "number?", + inputs: 1 + }, + "macro?": { + kind: "fn", + function: "macro?", + inputs: 1 + }, + "readline": { + kind: "fn", + function: "readline", + inputs: 1 + }, + "time-ms": { + kind: "fn", + function: "time-ms", + inputs: 0 + }, + "meta": { + kind: "fn", + function: "meta", + inputs: 1 + }, + "with-meta": { + kind: "fn", + function: "with-meta", + inputs: 2 + }, + "seq": { + kind: "fn", + function: "seq", + inputs: 1 + }, + "conj": { + kind: "fn", + function: "conj", + inputs: -3 + } + }; + +def vec2list(obj): + if obj.kind == "list" then + obj.value | map(vec2list(.)) | wrap("list") + else + if obj.kind == "vector" then + obj.value | map(vec2list(.)) | wrap("list") + else + if obj.kind == "hashmap" then + obj.value | map_values(.value |= vec2list(.)) | wrap("hashmap") + else + obj + end + end + end; + +def make_sequence: + . as $dot + | if .value|length == 0 then null | wrap("nil") else + ( + select(.kind == "string") | .value | split("") | map(wrap("string")) + ) // ( + select(.kind == "list" or .kind == "vector") | .value + ) // jqmal_error("cannot make sequence from \(.kind)") | wrap("list") + end; + +def core_interp(arguments; env): + ( + select(.function == "number_add") | + arguments | map(.value) | .[0] + .[1] | wrap("number") + ) // ( + select(.function == "number_sub") | + arguments | map(.value) | .[0] - .[1] | wrap("number") + ) // ( + select(.function == "number_mul") | + arguments | map(.value) | .[0] * .[1] | wrap("number") + ) // ( + select(.function == "number_div") | + arguments | map(.value) | .[0] / .[1] | wrap("number") + ) // ( + select(.function == "env") | + env | tojson | wrap("string") + ) // ( + select(.function == "prn") | + arguments | map(pr_str(env; {readable: true})) | join(" ") | _display | null | wrap("nil") + ) // ( + select(.function == "pr-str") | + arguments | map(pr_str(env; {readable: true})) | join(" ") | wrap("string") + ) // ( + select(.function == "str") | + arguments | map(pr_str(env; {readable: false})) | join("") | wrap("string") + ) // ( + select(.function == "println") | + arguments | map(pr_str(env; {readable: false})) | join(" ") | _display | null | wrap("nil") + ) // ( + select(.function == "list") | + arguments | wrap("list") + ) // ( + select(.function == "list?") | null | wrap(arguments | first.kind == "list" | tostring) + ) // ( + select(.function == "empty?") | null | wrap(arguments|first.value | length == 0 | tostring) + ) // ( + select(.function == "count") | arguments|first.value | length | wrap("number") + ) // ( + select(.function == "=") | null | wrap(vec2list(arguments[0]) == vec2list(arguments[1]) | tostring) + ) // ( + select(.function == "<") | null | wrap(arguments[0].value < arguments[1].value | tostring) + ) // ( + select(.function == "<=") | null | wrap(arguments[0].value <= arguments[1].value | tostring) + ) // ( + select(.function == ">") | null | wrap(arguments[0].value > arguments[1].value | tostring) + ) // ( + select(.function == ">=") | null | wrap(arguments[0].value >= arguments[1].value | tostring) + ) // ( + select(.function == "slurp") | arguments | map(.value) | issue_extern("read") | wrap("string") + ) // ( + select(.function == "read-string") | arguments | first.value | read_str | read_form.value + ) // ( + select(.function == "atom?") | null | wrap(arguments | first.kind == "atom" | tostring) + ) // ( + select(.function == "cons") | ([arguments[0]] + arguments[1].value) | wrap("list") + ) // ( + select(.function == "concat") | arguments | map(.value) | (add//[]) | wrap("list") + ) // ( + select(.function == "nth") + | _debug(arguments) + | arguments[0].value as $lst + | arguments[1].value as $idx + | if ($lst|length < $idx) or ($idx < 0) then + jqmal_error("index out of range") + else + $lst[$idx] + end + ) // ( + select(.function == "first") | arguments[0].value | first // {kind:"nil"} + ) // ( + select(.function == "rest") | arguments[0]?.value?[1:]? // [] | wrap("list") + ) // ( + select(.function == "throw") | jqmal_error(arguments[0] | tojson) + ) // ( + select(.function == "nil?") | null | wrap((arguments[0].kind == "nil") | tostring) + ) // ( + select(.function == "true?") | null | wrap((arguments[0].kind == "true") | tostring) + ) // ( + select(.function == "false?") | null | wrap((arguments[0].kind == "false") | tostring) + ) // ( + select(.function == "symbol?") | null | wrap((arguments[0].kind == "symbol") | tostring) + ) // ( + select(.function == "symbol") | arguments[0].value | wrap("symbol") + ) // ( + select(.function == "keyword") | arguments[0].value | wrap("keyword") + ) // ( + select(.function == "keyword?") | null | wrap((arguments[0].kind == "keyword") | tostring) + ) // ( + select(.function == "vector") | arguments | wrap("vector") + ) // ( + select(.function == "vector?") | null | wrap((arguments[0].kind == "vector") | tostring) + ) // ( + select(.function == "sequential?") | null | wrap((arguments[0].kind == "vector" or arguments[0].kind == "list") | tostring) + ) // ( + select(.function == "hash-map") | + if (arguments|length) % 2 == 1 then + jqmal_error("Odd number of arguments to hash-map") + else + [ arguments | + nwise(2) | + try { + key: (.[0] | extract_string), + value: { + kkind: .[0].kind, + value: .[1] + } + } + ] | from_entries | wrap("hashmap") + end + ) // ( + select(.function == "map?") | null | wrap((arguments[0].kind == "hashmap") | tostring) + ) // ( + select(.function == "assoc") | + if (arguments|length) % 2 == 0 then + jqmal_error("Odd number of key-values to assoc") + else + arguments[0].value + ([ arguments[1:] | + nwise(2) | + try { + key: (.[0] | extract_string), + value: { + kkind: .[0].kind, + value: .[1] + } + } + ] | from_entries) | wrap("hashmap") + end + ) // ( + select(.function == "dissoc") | + arguments[1:] | map(.value) as $keynames | + arguments[0].value | with_entries(select(.key as $k | $keynames | contains([$k]) | not)) | wrap("hashmap") + ) // ( + select(.function == "get") | arguments[0].value[arguments[1].value].value // {kind:"nil"} + ) // ( + select(.function == "contains?") | null | wrap((arguments[0].value | has(arguments[1].value)) | tostring) + ) // ( + select(.function == "keys") | arguments[0].value | with_entries(.value as $v | .key as $k | {key: $k, value: {value: $k, kind: $v.kkind}}) | to_entries | map(.value) | wrap("list") + ) // ( + select(.function == "vals") | arguments[0].value | map(.value) | to_entries | map(.value) | wrap("list") + ) // ( + select(.function == "string?") | null | wrap((arguments[0].kind == "string") | tostring) + ) // ( + select(.function == "fn?") | null | wrap((arguments[0].kind == "fn" or (arguments[0].kind == "function" and (arguments[0].is_macro|not))) | tostring) + ) // ( + select(.function == "number?") | null | wrap((arguments[0].kind == "number") | tostring) + ) // ( + select(.function == "macro?") | null | wrap((arguments[0].is_macro == true) | tostring) + ) // ( + select(.function == "readline") | arguments[0].value | __readline | wrap("string") + ) // ( + select(.function == "time-ms") | now * 1000 | wrap("number") + ) // ( + select(.function == "meta") | arguments[0].meta // {kind:"nil"} + ) // ( + select(.function == "with-meta") | arguments[0] | .meta |= arguments[1] + ) // ( + select(.function == "seq") | arguments[0] | make_sequence + ) // ( + select(.function == "conj") + | arguments[0] as $orig + | arguments[1:] as $stuff + | if $orig.kind == "list" then + [ $stuff|reverse[], $orig.value[] ] | wrap("list") + else + [ $orig.value[], $stuff[] ] | wrap("vector") + end + ) // jqmal_error("Unknown native function \(.function)"); diff --git a/jq/env.jq b/jq/env.jq new file mode 100644 index 00000000..bea8eb1b --- /dev/null +++ b/jq/env.jq @@ -0,0 +1,284 @@ +include "utils"; + +def childEnv(binds; exprs): + { + parent: ., + fallback: null, + environment: [binds, exprs] | transpose | ( + . as $dot | reduce .[] as $item ( + { value: [], seen: false, name: null, idx: 0 }; + if $item[1] != null then + if .seen then + { + value: (.value[1:-1] + (.value|last[1].value += [$item[1]])), + seen: true, + name: .name + } + else + if $item[0] == "&" then + $dot[.idx+1][0] as $name | { + value: (.value + [[$name, {kind:"list", value: [$item[1]]}]]), + seen: true, + name: $name + } + else + { + value: (.value + [$item]), + seen: false, + name: null + } + end + end | (.idx |= .idx + 1) + else + if $item[0] == "&" then + $dot[.idx+1][0] as $name | { + value: (.value + [[$name, {kind:"list", value: []}]]), + seen: true, + name: $name + } + else . end + end + ) + ) | .value | map({(.[0]): .[1]}) | add + }; + +def pureChildEnv: + { + parent: ., + environment: {}, + fallback: null + }; + +def rootEnv: + { + parent: null, + fallback: null, + environment: {} + }; + +def inform_function(name): + (.names += [name]) | (.names |= unique); + +def inform_function_multi(names): + . as $dot | reduce names[] as $name( + $dot; + inform_function($name) + ); + +def env_multiset(keys; value): + (if value.kind == "function" then # multiset not allowed on atoms + value | inform_function_multi(keys) + else + value + end) as $value | { + parent: .parent, + environment: ( + .environment + (reduce keys[] as $key(.environment; .[$key] |= value)) + ), + fallback: .fallback + }; + +def env_multiset(env; keys; value): + env | env_multiset(keys; value); + +def env_set($key; $value): + (if $value.kind == "function" or $value.kind == "atom" then + # inform the function/atom of its names + ($value | + if $value.kind == "atom" then + # check if the one we have is newer + env_req(env; key) as $ours | + if $ours.last_modified > $value.last_modified then + $ours + else + # update modification timestamp + $value | .last_modified |= now + end + else + . + end) | inform_function($key) + else + $value + end) as $value | { + parent: .parent, + environment: (.environment + (.environment | .[$key] |= $value)), # merge together, as .environment[key] |= value does not work + fallback: .fallback + }; + +def env_dump_keys: + def _dump1: + .environment // {} | keys; + if . == null then [] else + if .parent == null then + ( + _dump1 + + (.fallback | env_dump_keys) + ) + else + ( + _dump1 + + (.parent | env_dump_keys) + + (.fallback | env_dump_keys) + ) + end | unique + end; + +def env_find(env): + if env.environment[.] == null then + if env.parent then + env_find(env.parent) // if env.fallback then env_find(env.fallback) else null end + else + null + end + else + env + end; + +def env_get(env): + . as $key | $key | env_find(env).environment[$key] as $value | + if $value == null then + jqmal_error("'\($key)' not found") + else + if $value.kind == "atom" then + $value.identity as $id | + $key | env_find(env.parent).environment[$key] as $possibly_newer | + if $possibly_newer.identity == $id and $possibly_newer.last_modified > $value.last_modified then + $possibly_newer + else + $value + end + else + $value + end + end; + +def env_get(env; key): + key | env_get(env); + +def env_req(env; key): + key as $key | key | env_find(env).environment[$key] as $value | + if $value == null then + null + else + if $value.kind == "atom" then + $value.identity as $id | + $key | env_find(env.parent).environment[$key] as $possibly_newer | + if $possibly_newer.identity == $id and $possibly_newer.last_modified > $value.last_modified then + $possibly_newer + else + $value + end + else + $value + end + end; + +def env_set(env; $key; $value): + (if $value.kind == "function" or $value.kind == "atom" then + # inform the function/atom of its names + $value | (.names += [$key]) | (.names |= unique) | + if $value.kind == "atom" then + # check if the one we have is newer + env_req(env; $key) as $ours | + if $ours.last_modified > $value.last_modified then + $ours + else + # update modification timestamp + $value | .last_modified |= now + end + else + . + end + else + $value + end) as $value | { + parent: env.parent, + environment: ((env.environment // jqmal_error("Environment empty in \(env | keys)")) + (env.environment | .[$key] |= $value)), # merge together, as env.environment[key] |= value does not work + fallback: env.fallback + }; + +def env_setfallback(env; fallback): + { + parent: env.parent, + fallback: fallback, + environment: env.environment + }; + +def addEnv(env): + { + expr: ., + env: env + }; + +def addToEnv(env; name; expr): + { + expr: expr, + env: env_set(env; name; expr) + }; + + +def wrapEnv(atoms): + { + replEnv: ., + currentEnv: ., + atoms: atoms, + isReplEnv: true + }; + +def wrapEnv(replEnv; atoms): + { + replEnv: replEnv, + currentEnv: ., + atoms: atoms, # id -> value + isReplEnv: (replEnv == .) # should we allow separate copies? + }; + +def unwrapReplEnv: + .replEnv; + +def unwrapCurrentEnv: + .currentEnv; + +def env_set6(env; key; value): + if env.isReplEnv then + env_set(env.currentEnv; key; value) | wrapEnv(env.atoms) + else + env_set(env.currentEnv; key; value) | wrapEnv(env.replEnv; env.atoms) + end; + +def env_set_(env; key; value): + if env.currentEnv != null then + env_set6(env; key; value) + else + env_set(env; key; value) + end; + +def addToEnv(envexp; name): + envexp.expr as $value + | envexp.env as $rawEnv + | (if $rawEnv.isReplEnv then + env_set_($rawEnv.currentEnv; name; $value) | wrapEnv($rawEnv.atoms) + else + env_set_($rawEnv.currentEnv; name; $value) | wrapEnv($rawEnv.replEnv; $rawEnv.atoms) + end) as $newEnv + | { + expr: $value, + env: $newEnv + }; + +def _env_remove_references(refs): + if . != null then + { + environment: (.environment | to_entries | map(select(.key as $key | refs | contains([$key]) | not)) | from_entries), + parent: (.parent | _env_remove_references(refs)), + fallback: (.fallback | _env_remove_references(refs)) + } + else . end; + +def env_remove_references(refs): + . as $env + | if has("replEnv") then + .currentEnv |= _env_remove_references(refs) + else + _env_remove_references(refs) + end; \ No newline at end of file diff --git a/jq/interp.jq b/jq/interp.jq new file mode 100644 index 00000000..a60693f4 --- /dev/null +++ b/jq/interp.jq @@ -0,0 +1,178 @@ +include "utils"; +include "core"; +include "env"; +include "printer"; + +def arg_check(args): + if .inputs < 0 then + if (abs(.inputs) - 1) > (args | length) then + jqmal_error("Invalid number of arguments (expected at least \(abs(.inputs) - 1), got \(args|length))") + else + . + end + else if .inputs != (args|length) then + jqmal_error("Invalid number of arguments (expected \(.inputs), got \(args|length))") + else + . + end end; + +def extractReplEnv(env): + env | .replEnv // .; + +def extractEnv(env): + env | .currentEnv // .; + +def updateReplEnv(renv): + def findpath: + if .env.parent then + .path += ["parent"] | + .env |= .parent | + findpath + else + .path + end; + ({ env: ., path: [] } | findpath) as $path | + setpath($path; renv); + +def extractCurrentReplEnv(env): + def findpath: + if .env.parent then + .path += ["parent"] | + .env |= .parent | + findpath + else + .path + end; + if env.currentEnv != null then + ({ env: env.currentEnv, path: [] } | findpath) as $path | + env.currentEnv | getpath($path) + else + env + end; + +def extractAtoms(env): + env.atoms // {}; + +def addFrees(newEnv; frees): + . as $env + | reduce frees[] as $free ( + $env; + . as $dot + | extractEnv(newEnv) as $env + | env_req($env; $free) as $lookup + | if $lookup != null then + env_set_(.; $free; $lookup) + else + . + end) + | . as $env + | $env; + +def interpret(arguments; env; _eval): + extractReplEnv(env) as $replEnv | + extractAtoms(env) as $envAtoms | + (if $DEBUG then _debug("INTERP: \(. | pr_str(env))") else . end) | + (select(.kind == "fn") | + arg_check(arguments) | + (select(.function == "eval") | + # special function + { expr: arguments[0], env: $replEnv|wrapEnv($replEnv; $envAtoms) } + | _eval + | .env as $xenv + | extractReplEnv($xenv) as $xreplenv + | setpath( + ["env", "currentEnv"]; + extractEnv(env) | updateReplEnv($xreplenv)) + ) // + (select(.function == "reset!") | + # env modifying function + arguments[0].identity as $id | + ($envAtoms | setpath([$id]; arguments[1])) as $envAtoms | + arguments[1] | addEnv(env | setpath(["atoms"]; $envAtoms)) + ) // + (select(.function == "swap!") | + # env modifying function + arguments[0].identity as $id | + $envAtoms[$id] as $initValue | + arguments[1] as $function | + ([$initValue] + arguments[2:]) as $args | + ($function | interpret($args; env; _eval)) as $newEnvValue | + ($envAtoms | setpath([$id]; $newEnvValue.expr)) as $envAtoms | + $newEnvValue.expr | addEnv(env | setpath(["atoms"]; $envAtoms)) + ) // (select(.function == "atom") | + (now|tostring) as $id | + {kind: "atom", identity: $id} as $value | + ($envAtoms | setpath([$id]; arguments[0])) as $envAtoms | + $value | addEnv(env | setpath(["atoms"]; $envAtoms)) + ) // (select(.function == "deref") | + $envAtoms[arguments[0].identity] | addEnv(env) + ) // + (select(.function == "apply") | + # (apply F ...T A) -> (F ...T ...A) + arguments as $args + | ($args|first) as $F + | ($args|last.value) as $A + | $args[1:-1] as $T + | $F | interpret([$T[], $A[]]; env; _eval) + ) // + (select(.function == "map") | + arguments + | first as $F + | last.value as $L + | (reduce $L[] as $elem ( + {env: env, val: []}; + . as $dot | + ($F | interpret([$elem]; $dot.env; _eval)) as $val | + { + val: (.val + [$val.expr]), + env: (.env | setpath(["atoms"]; $val.env.atoms)) + } + )) as $ex + | $ex.val | wrap("list") | addEnv($ex.env) + ) // + (core_interp(arguments; env) | addEnv(env)) + ) // + (select(.kind == "function") as $fn | + # todo: arg_check + (.body | pr_str(env)) as $src | + # _debug("INTERP " + $src) | + # _debug("FREES " + ($fn.free_referencess | tostring)) | + env_setfallback(extractEnv(.env | addFrees(env; $fn.free_referencess)); extractEnv(env)) | childEnv($fn.binds; arguments) as $fnEnv | + # tell it about its surroundings + (reduce $fn.free_referencess[] as $name ( + $fnEnv; + . as $env | try env_set_( + .; + $name; + $name | env_get(env) | . as $xvalue + | if $xvalue.kind == "function" then + setpath(["free_referencess"]; $fn.free_referencess) + else + $xvalue + end + ) catch $env)) as $fnEnv | + # tell it about itself + env_multiset($fnEnv; $fn.names; $fn) as $fnEnv | + { + env: env_multiset($fnEnv; $fn.names; $fn) + | wrapEnv($replEnv; $envAtoms), + expr: $fn.body + } + | . as $dot + # | _debug("FNEXEC " + (.expr | pr_str) + " " + (env_req($dot.env; $fn.binds[0]) | pr_str)) + | _eval + | . as $envexp + | (extractReplEnv($envexp.env)) as $xreplenv + | + { + expr: .expr, + env: extractEnv(env) + | updateReplEnv($xreplenv) + | wrapEnv($xreplenv; $envexp.env.atoms) + } + # | . as $dot + # | _debug("FNPOST " + (.expr | pr_str) + " " + (env_req($dot.expr.env; $fn.binds[0]) | pr_str)) + # | _debug("INTERP " + $src + " = " + (.expr|pr_str)) + ) // + jqmal_error("Unsupported function kind \(.kind)"); + \ No newline at end of file diff --git a/jq/printer.jq b/jq/printer.jq new file mode 100644 index 00000000..703eb650 --- /dev/null +++ b/jq/printer.jq @@ -0,0 +1,29 @@ +# {key: string, value: {kkind: kind, value: value}} -> [{kind: value.kkind, value: key}, value.value] +def _reconstruct_hash: + map([{ + kind: .value.kkind, + value: .key + }, + .value.value]); + +def pr_str(env; opt): + (select(.kind == "symbol") | .value) // + (select(.kind == "string") | .value | if opt.readable then tojson else . end) // + (select(.kind == "keyword") | ":\(.value)") // + (select(.kind == "number") | .value | tostring) // + (select(.kind == "list") | .value | map(pr_str(env; opt)) | join(" ") | "(\(.))") // + (select(.kind == "vector") | .value | map(pr_str(env; opt)) | join(" ") | "[\(.)]") // + (select(.kind == "hashmap") | .value | to_entries | _reconstruct_hash | add // [] | map(pr_str(env; opt)) | join(" ") | "{\(.)}") // + (select(.kind == "nil") | "nil") // + (select(.kind == "true") | "true") // + (select(.kind == "false") | "false") // + (select(.kind == "fn") | "#") // + (select(.kind == "function")| "#") // + (select(.kind == "atom") | "(atom \(env.atoms[.identity] | pr_str(env; opt)))") // + "#"; + +def pr_str(env): + pr_str(env; {readable: true}); + +def pr_str: + pr_str(null); # for stepX where X<6 \ No newline at end of file diff --git a/jq/reader.jq b/jq/reader.jq new file mode 100644 index 00000000..d8c98198 --- /dev/null +++ b/jq/reader.jq @@ -0,0 +1,311 @@ +include "utils"; + +def tokenize: + [ . | scan("[\\s,]*(~@|[\\[\\]{}()'`~^@]|\"(?:\\\\.|[^\\\\\"])*\"?|;.*|[^\\s\\[\\]{}('\"`,;)]*)") | select(.|length > 0)[0] | select(.[0:1] != ";") ]; + +def read_str: + tokenize; + +def escape_control: + (select(. == "\u0000") | "\\u0000") // + (select(. == "\u0001") | "\\u0001") // + (select(. == "\u0002") | "\\u0002") // + (select(. == "\u0003") | "\\u0003") // + (select(. == "\u0004") | "\\u0004") // + (select(. == "\u0005") | "\\u0005") // + (select(. == "\u0006") | "\\u0006") // + (select(. == "\u0007") | "\\u0007") // + (select(. == "\u0008") | "\\u0008") // + (select(. == "\u0009") | "\\u0009") // + (select(. == "\u0010") | "\\u0010") // + (select(. == "\u0011") | "\\u0011") // + (select(. == "\u0012") | "\\u0012") // + (select(. == "\u0013") | "\\u0013") // + (select(. == "\u0014") | "\\u0014") // + (select(. == "\u0015") | "\\u0015") // + (select(. == "\u0016") | "\\u0016") // + (select(. == "\u0017") | "\\u0017") // + (select(. == "\u0018") | "\\u0018") // + (select(. == "\u0019") | "\\u0019") // + (select(. == "\u0020") | "\\u0020") // + (select(. == "\u0021") | "\\u0021") // + (select(. == "\u0022") | "\\u0022") // + (select(. == "\u0023") | "\\u0023") // + (select(. == "\u0024") | "\\u0024") // + (select(. == "\u0025") | "\\u0025") // + (select(. == "\u0026") | "\\u0026") // + (select(. == "\u0027") | "\\u0027") // + (select(. == "\u0028") | "\\u0028") // + (select(. == "\u0029") | "\\u0029") // + (select(. == "\u0030") | "\\u0030") // + (select(. == "\u0031") | "\\u0031") // + (select(. == "\n") | "\\n") // + .; + +def read_string: + gsub("(?[\u0000-\u001f])"; "\(.z | escape_control)") | fromjson; + +def extract_string: + . as $val | if ["keyword", "symbol", "string"] | contains([$val.kind]) then + $val.value + else + jqmal_error("assoc called with non-string key of type \($val.kind)") + end; + +# stuff comes in as {tokens: [...], } +def read_atom: + (.tokens | first) as $lookahead | . | ( + if $lookahead == "nil" then + { + tokens: .tokens[1:], + value: { + kind: "nil" + } + } + else if $lookahead == "true" then + { + tokens: .tokens[1:], + value: { + kind: "true" + } + } + else if $lookahead == "false" then + { + tokens: .tokens[1:], + value: { + kind: "false" + } + } + else if $lookahead | test("^\"") then + if $lookahead | test("^\"(?:\\\\.|[^\\\\\"])*\"$") then + { + tokens: .tokens[1:], + value: { + kind: "string", + value: $lookahead | read_string + } + } + else + jqmal_error("EOF while reading string") + end + else if $lookahead | test("^:") then + { + tokens: .tokens[1:], + value: { + kind: "keyword", + value: $lookahead[1:] + } + } + else if $lookahead | test("^-?[0-9]+(?:\\.[0-9]+)?$") then + { + tokens: .tokens[1:], + value: { + kind: "number", + value: $lookahead | tonumber + } + } + else if [")", "]", "}"] | contains([$lookahead]) then # this isn't our business + empty + else + { + tokens: .tokens[1:], + value: { + kind: "symbol", + value: $lookahead + } + } + end end end end end end end + ); + +def read_form_(depth): + (.tokens | first) as $lookahead | . | ( + if $lookahead == null then + null + # read_list + else + if $lookahead | test("^\\(") then + [ (.tokens |= .[1:]) | {tokens: .tokens, value: [], finish: false} | (until(.finish; + if try (.tokens | first | test("^\\)")) catch true then + .finish |= true + else + . as $orig | read_form_(depth+1) as $res | { + tokens: $res.tokens, + value: ($orig.value + [$res.value]), + finish: $orig.finish + } + end)) ] | map(select(.tokens)) | last as $result | + if $result.tokens | first != ")" then + jqmal_error("unbalanced parentheses in \($result.tokens)") + else + { + tokens: $result.tokens[1:], + value: { + kind: "list", + value: $result.value + }, + } + end + # read_list '[' + else if $lookahead | test("^\\[") then + [ (.tokens |= .[1:]) | {tokens: .tokens, value: [], finish: false} | (until(.finish; + if try (.tokens | first | test("^\\]")) catch true then + .finish |= true + else + . as $orig | read_form_(depth+1) as $res | { + tokens: $res.tokens, + value: ($orig.value + [$res.value]), + finish: $orig.finish + } + end)) ] | map(select(.tokens)) | last as $result | + if $result.tokens | first != "]" then + jqmal_error("unbalanced brackets in \($result.tokens)") + else + { + tokens: $result.tokens[1:], + value: { + kind: "vector", + value: $result.value + }, + } + end + # read_list '{' + else if $lookahead | test("^\\{") then + [ (.tokens |= .[1:]) | {tokens: .tokens, value: [], finish: false} | (until(.finish; + if try (.tokens | first | test("^\\}")) catch true then + .finish |= true + else + . as $orig | read_form_(depth+1) as $res | { + tokens: $res.tokens, + value: ($orig.value + [$res.value]), + finish: $orig.finish + } + end)) ] | map(select(.tokens)) | last as $result | + if $result.tokens | first != "}" then + jqmal_error("unbalanced braces in \($result.tokens)") + else + if $result.value | length % 2 == 1 then + # odd number of elements not allowed + jqmal_error("Odd number of parameters to assoc") + else + { + tokens: $result.tokens[1:], + value: { + kind: "hashmap", + value: + [ $result.value | + nwise(2) | + try { + key: (.[0] | extract_string), + value: { + kkind: .[0].kind, + value: .[1] + } + } + ] | from_entries + } + } + end + end + # quote + else if $lookahead == "'" then + (.tokens |= .[1:]) | read_form_(depth+1) | ( + { + tokens: .tokens, + value: { + kind: "list", + value: [ + { + kind: "symbol", + value: "quote" + }, + .value + ] + } + }) + # quasiquote + else if $lookahead == "`" then + (.tokens |= .[1:]) | read_form_(depth+1) | ( + { + tokens: .tokens, + value: { + kind: "list", + value: [ + { + kind: "symbol", + value: "quasiquote" + }, + .value + ] + } + }) + # unquote + else if $lookahead == "~" then + (.tokens |= .[1:]) | read_form_(depth+1) | ( + { + tokens: .tokens, + value: { + kind: "list", + value: [ + { + kind: "symbol", + value: "unquote" + }, + .value + ] + } + }) + # split-unquote + else if $lookahead == "~@" then + (.tokens |= .[1:]) | read_form_(depth+1) | ( + { + tokens: .tokens, + value: { + kind: "list", + value: [ + { + kind: "symbol", + value: "splice-unquote" + }, + .value + ] + } + }) + # deref + else if $lookahead == "@" then + (.tokens |= .[1:]) | read_form_(depth+1) | ( + { + tokens: .tokens, + value: { + kind: "list", + value: [ + { + kind: "symbol", + value: "deref" + }, + .value + ] + } + }) + # with-meta + else if $lookahead == "^" then + (.tokens |= .[1:]) | read_form_(depth+1) as $meta | $meta | read_form_(depth+1) as $value | ( + { + tokens: $value.tokens, + value: { + kind: "list", + value: [ + { + kind: "symbol", + value: "with-meta" + }, + $value.value, + $meta.value + ] + } + }) + else + . as $prev | read_atom + end end end end end end end end end end); + +def read_form: + {tokens: .} | read_form_(0); diff --git a/jq/rts.py b/jq/rts.py new file mode 100644 index 00000000..853c3fe7 --- /dev/null +++ b/jq/rts.py @@ -0,0 +1,112 @@ +import os +from os import fork, execv, pipe, close, dup2, kill, read, write +from select import select +import json +from os.path import dirname, realpath +from os import environ +import signal +from sys import argv +import fcntl + +DEBUG = False +HALT = False + +# Bestow IO upon jq + +def _read(fname, out=None): + with open(fname, "r") as f: + data = json.dumps(f.read()) + "\n" + # print("data =", data) + write(out, bytes(data, 'utf-8')) + +def _readline(prompt="", out=None): + data = json.dumps(input(prompt)) + "\n" + # print("data =", data) + write(out, bytes(data, 'utf-8')) + +def _fwrite(fname, data, out=None): + return + +def _halt(out=None): + global HALT + HALT = True + +def stub(*args, out=None): + raise Exception("command not understood") + +rts = { + "read": _read, + "readline": _readline, + "fwrite": _fwrite, + "halt": _halt, +} + +def process(cmd, fout): + if type(cmd) == str: + print(cmd, end="") + elif type(cmd) == dict: + cmd = cmd['command'] + command = cmd['cmd'] + args = cmd['args'] + fn = rts.get(command, stub) + fn(*args, out=fout) + +def get_one(fd): + s = b"" + while True: + x = read(fd, 1) + if x == b'\n': + break + if x == b'': + break + s += x + if s == "": + return None + return s.decode('utf-8') + + +def main(args): + args = [ + "jq", "--argjson", "DEBUG", json.dumps(DEBUG), "-nrRM", + "-f", + dirname(realpath(__file__)) + "/" + environ.get("STEP", "stepA_mal") + ".jq", + "--args", + *args + ] + # print(args) + sin_pipe = pipe() + sout_pipe = pipe() + + pid = fork() + if pid == 0: + # jq + close(sin_pipe[1]) + close(sout_pipe[0]) + + dup2(sin_pipe[0], 0) + dup2(sout_pipe[1], 2) # bind to stderr, as we write there + dup2(sout_pipe[1], 1) + + execv("/usr/bin/jq", args) + else: + close(sin_pipe[0]) + close(sout_pipe[1]) + + msout = sin_pipe[1] + msin = sout_pipe[0] + + while True: + try: + if HALT: + break + cmd = get_one(msin) + # print(cmd) + if cmd: + process(json.loads(cmd)[1], msout) + except KeyboardInterrupt: + exit() + except Exception as e: + print("RTS Error:", e) + + +main(argv[1:]) \ No newline at end of file diff --git a/jq/run b/jq/run new file mode 100755 index 00000000..02e476e4 --- /dev/null +++ b/jq/run @@ -0,0 +1,3 @@ +#!/bin/sh + +exec python rts.py "${@}" diff --git a/jq/step0_repl.jq b/jq/step0_repl.jq new file mode 100644 index 00000000..46c5a5ea --- /dev/null +++ b/jq/step0_repl.jq @@ -0,0 +1,27 @@ +include "utils"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + .; + +def EVAL: + .; + +def PRINT: + .; + +def rep: + READ | EVAL | PRINT | _display; + +def repl_: + ("user> " | _print) | + (read_line | rep); + +def repl: + while(true; repl_); + +repl diff --git a/jq/step1_read_print.jq b/jq/step1_read_print.jq new file mode 100644 index 00000000..d0069c27 --- /dev/null +++ b/jq/step1_read_print.jq @@ -0,0 +1,42 @@ +include "reader"; +include "printer"; +include "utils"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +def EVAL: + .; + +def PRINT: + pr_str; + +def rep: + READ | EVAL | + if . != null then + PRINT + else + null + end; + +def repl_: + ("user> " | _print) | + (read_line | rep); + +def repl: + {continue: true} | while( + .continue; + try {value: repl_, continue: true} + catch + if is_jqmal_error then + {value: "Error: \(.)", continue: true} + else + {value: ., continue: false} + end) | if .value then .value|_display else empty end; + +repl diff --git a/jq/step2_eval.jq b/jq/step2_eval.jq new file mode 100644 index 00000000..e04ce81d --- /dev/null +++ b/jq/step2_eval.jq @@ -0,0 +1,121 @@ +include "reader"; +include "printer"; +include "utils"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +def lookup(env): + env[.] // + jqmal_error("'\(.)' not found"); + +def arg_check(args): + if .inputs != (args|length) then + jqmal_error("Invalid number of arguments (expected \(.inputs), got \(args|length))") + else + . + end; + +def interpret(arguments; env): + (select(.kind == "fn") | + arg_check(arguments) | + ( + select(.function == "number_add") | + arguments | map(.value) | .[0] + .[1] | wrap("number") + ) // ( + select(.function == "number_sub") | + arguments | map(.value) | .[0] - .[1] | wrap("number") + ) // ( + select(.function == "number_mul") | + arguments | map(.value) | .[0] * .[1] | wrap("number") + ) // ( + select(.function == "number_div") | + arguments | map(.value) | .[0] / .[1] | wrap("number") + ) + ) // + jqmal_error("Unsupported native function kind \(.kind)"); + +def EVAL(env): + def eval_ast: + (select(.kind == "symbol") | .value | lookup(env)) // + (select(.kind == "list") | { + kind: "list", + value: .value | map(EVAL(env)) + }) // .; + (select(.kind == "list") | + if .value | length == 0 then + . + else + eval_ast|.value as $evald | $evald | first | interpret($evald[1:]; env) + end + ) // + (select(.kind == "vector") | + { + kind: "vector", + value: .value|map(EVAL(env)) + } + ) // + (select(.kind == "hashmap") | + { + kind: "hashmap", + value: .value|map_values(.value |= EVAL(env)) + } + ) // eval_ast; + +def PRINT: + pr_str; + +def rep(env): + READ | EVAL(env) | + if . != null then + PRINT + else + null + end; + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + }; + +def repl(env): + {continue: true} | while( + .continue; + try {value: repl_(env), continue: true} + catch + if is_jqmal_error then + {value: "Error: \(.)", continue: true} + else + {value: ., continue: false} + end) | if .value then .value|_display else empty end; + +repl(replEnv) \ No newline at end of file diff --git a/jq/step3_env.jq b/jq/step3_env.jq new file mode 100644 index 00000000..49ae998e --- /dev/null +++ b/jq/step3_env.jq @@ -0,0 +1,218 @@ +include "reader"; +include "printer"; +include "utils"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +# Environment functions + +def pureChildEnv: + { + parent: ., + environment: {} + }; + +def env_set(env; $key; $value): + { + parent: env.parent, + environment: (env.environment + (env.environment | .[$key] |= $value)) # merge together, as .environment[key] |= value does not work + }; + +def env_find(env): + if env.environment[.] == null then + if env.parent then + env_find(env.parent) + else + null + end + else + env + end; + +def addToEnv(envexp; name): + { + expr: envexp.expr, + env: env_set(envexp.env; name; envexp.expr) + }; + +def env_get(env): + . as $key | $key | env_find(env).environment[$key] as $value | + if $value == null then + jqmal_error("'\($key)' not found") + else + $value + end; + +def addEnv(env): + { + expr: ., + env: env + }; + +# Evaluation + +def arg_check(args): + if .inputs != (args|length) then + jqmal_error("Invalid number of arguments (expected \(.inputs), got \(args|length))") + else + . + end; + +def interpret(arguments; env): + (select(.kind == "fn") | + arg_check(arguments) | + ( + select(.function == "number_add") | + arguments | map(.value) | .[0] + .[1] | wrap("number") + ) // ( + select(.function == "number_sub") | + arguments | map(.value) | .[0] - .[1] | wrap("number") + ) // ( + select(.function == "number_mul") | + arguments | map(.value) | .[0] * .[1] | wrap("number") + ) // ( + select(.function == "number_div") | + arguments | map(.value) | .[0] / .[1] | wrap("number") + ) + ) | addEnv(env) // + jqmal_error("Unsupported native function kind \(.kind)"); + +def EVAL(env): + def hmap_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem[1] | EVAL($env) as $resv | + { value: [$elem[0], $resv.expr], env: env }, + ({env: $resv.env, list: $rest} | hmap_with_env) + end; + def map_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem | EVAL($env) as $resv | + { value: $resv.expr, env: env }, + ({env: $resv.env, list: $rest} | map_with_env) + end; + (select(.kind == "list") | + if .value | length == 0 then + . + else + ( + ( + .value | select(.[0].value == "def!") as $value | + ($value[2] | EVAL(env)) as $evval | + addToEnv($evval; $value[1].value) + ) // + ( + .value | select(.[0].value == "let*") as $value | + (env | pureChildEnv) as $subenv | + (reduce ($value[1].value | nwise(2)) as $xvalue ( + $subenv; + . as $env | $xvalue[1] | EVAL($env) as $expenv | + env_set($expenv.env; $xvalue[0].value; $expenv.expr))) as $env + | $value[2] | { expr: EVAL($env).expr, env: env } + ) // + ( + reduce .value[] as $elem ( + []; + . as $dot | $elem | EVAL(env) as $eval_env | + ($dot + [$eval_env.expr]) + ) | { expr: ., env: env } as $ev + | $ev.expr | first | + interpret($ev.expr[1:]; $ev.env) + ) // + addEnv(env) + ) + end + ) // + (select(.kind == "vector") | + [ { env: env, list: .value } | map_with_env ] as $res | + { + kind: "vector", + value: $res | map(.value) + } | addEnv($res | last.env) + ) // + (select(.kind == "hashmap") | + [ { env: env, list: .value | to_entries } | hmap_with_env ] as $res | + { + kind: "hashmap", + value: $res | map(.value) | from_entries + } | addEnv($res | last.env) + ) // + (select(.kind == "symbol") | + .value | env_get(env) | addEnv(env) + ) // addEnv(env); + +def PRINT: + pr_str; + +def rep(env): + READ | EVAL(env) as $expenv | + if $expenv.expr != null then + $expenv.expr | PRINT + else + null + end | addEnv($expenv.env); + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +def childEnv(binds; value): + { + parent: ., + environment: [binds, value] | transpose | map({(.[0]): .[1]}) | from_entries + }; + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + parent: null, + environment: { + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + } + }; + +def repl(env): + def xrepl: + (.env as $env | try repl_($env) catch addEnv($env)) as $expenv | + { + value: $expenv.expr, + stop: false, + env: ($expenv.env // .env) + } | ., xrepl; + {stop: false, env: env} | xrepl | if .value then (.value | _display) else empty end; + +repl(replEnv) \ No newline at end of file diff --git a/jq/step4_if_fn_do.jq b/jq/step4_if_fn_do.jq new file mode 100644 index 00000000..3fcfcfe0 --- /dev/null +++ b/jq/step4_if_fn_do.jq @@ -0,0 +1,566 @@ +include "reader"; +include "printer"; +include "utils"; +include "core"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +# Environment Functions + +def childEnv(binds; exprs): + { + parent: ., + fallback: null, + environment: [binds, exprs] | transpose | ( + . as $dot | reduce .[] as $item ( + { value: [], seen: false, name: null, idx: 0 }; + if $item[1] != null then + if .seen then + { + value: (.value[1:-1] + (.value|last[1].value += [$item[1]])), + seen: true, + name: .name + } + else + if $item[0] == "&" then + $dot[.idx+1][0] as $name | { + value: (.value + [[$name, {kind:"list", value: [$item[1]]}]]), + seen: true, + name: $name + } + else + { + value: (.value + [$item]), + seen: false, + name: null + } + end + end | (.idx |= .idx + 1) + else + if $item[0] == "&" then + $dot[.idx+1][0] as $name | { + value: (.value + [[$name, {kind:"list", value: []}]]), + seen: true, + name: $name + } + else . end + end + ) + ) | .value | map({(.[0]): .[1]}) | add + }; + +def pureChildEnv: + { + parent: ., + environment: {}, + fallback: null + }; + +def rootEnv: + { + parent: null, + fallback: null, + environment: {} + }; + +def inform_function(name): + (.names += [name]) | (.names |= unique); + +def inform_function_multi(names): + . as $dot | reduce names[] as $name( + $dot; + inform_function($name) + ); + +def env_multiset(keys; value): + (if value.kind == "function" then # multiset not allowed on atoms + value | inform_function_multi(keys) + else + value + end) as $value | { + parent: .parent, + environment: ( + .environment + (reduce keys[] as $key(.environment; .[$key] |= value)) + ), + fallback: .fallback + }; + +def env_multiset(env; keys; value): + env | env_multiset(keys; value); + +def env_set($key; $value): + (if $value.kind == "function" or $value.kind == "atom" then + # inform the function/atom of its names + ($value | + if $value.kind == "atom" then + # check if the one we have is newer + env_req(env; key) as $ours | + if $ours.last_modified > $value.last_modified then + $ours + else + # update modification timestamp + $value | .last_modified |= now + end + else + . + end) | inform_function($key) + else + $value + end) as $value | { + parent: .parent, + environment: (.environment + (.environment | .[$key] |= $value)), # merge together, as .environment[key] |= value does not work + fallback: .fallback + }; + +def env_dump_keys: + def _dump1: + .environment // {} | keys; + if . == null then [] else + if .parent == null then + ( + _dump1 + + (.fallback | env_dump_keys) + ) + else + ( + _dump1 + + (.parent | env_dump_keys) + + (.fallback | env_dump_keys) + ) + end | unique + end; + +def env_find(env): + if env.environment[.] == null then + if env.parent then + env_find(env.parent) // if env.fallback then env_find(env.fallback) else null end + else + null + end + else + env + end; + +def env_get(env): + . as $key | $key | env_find(env).environment[$key] as $value | + if $value == null then + jqmal_error("'\($key)' not found") + else + if $value.kind == "atom" then + $value.identity as $id | + $key | env_find(env.parent).environment[$key] as $possibly_newer | + if $possibly_newer.identity == $id and $possibly_newer.last_modified > $value.last_modified then + $possibly_newer + else + $value + end + else + $value + end + end; + +def env_get(env; key): + key | env_get(env); + +def env_req(env; key): + key as $key | key | env_find(env).environment[$key] as $value | + if $value == null then + null + else + if $value.kind == "atom" then + $value.identity as $id | + $key | env_find(env.parent).environment[$key] as $possibly_newer | + if $possibly_newer.identity == $id and $possibly_newer.last_modified > $value.last_modified then + $possibly_newer + else + $value + end + else + $value + end + end; + +def env_set(env; $key; $value): + (if $value.kind == "function" or $value.kind == "atom" then + # inform the function/atom of its names + $value | (.names += [$key]) | (.names |= unique) | + if $value.kind == "atom" then + # check if the one we have is newer + env_req(env; $key) as $ours | + if $ours.last_modified > $value.last_modified then + $ours + else + # update modification timestamp + $value | .last_modified |= now + end + else + . + end + else + $value + end) as $value | { + parent: env.parent, + environment: ((env.environment // jqmal_error("Environment empty in \(env | keys)")) + (env.environment | .[$key] |= $value)), # merge together, as env.environment[key] |= value does not work + fallback: env.fallback + }; + +def env_setfallback(env; fallback): + { + parent: env.parent, + fallback: fallback, + environment: env.environment + }; + +def addEnv(env): + { + expr: ., + env: env + }; + +def addToEnv(env; name; expr): + { + expr: expr, + env: env_set(env; name; expr) + }; + + +def wrapEnv(atoms): + { + replEnv: ., + currentEnv: ., + atoms: atoms, + isReplEnv: true + }; + +def wrapEnv(replEnv; atoms): + { + replEnv: replEnv, + currentEnv: ., + atoms: atoms, # id -> value + isReplEnv: (replEnv == .) # should we allow separate copies? + }; + +def unwrapReplEnv: + .replEnv; + +def unwrapCurrentEnv: + .currentEnv; + +def env_set6(env; key; value): + if env.isReplEnv then + env_set(env.currentEnv; key; value) | wrapEnv(env.atoms) + else + env_set(env.currentEnv; key; value) | wrapEnv(env.replEnv; env.atoms) + end; + +def env_set_(env; key; value): + if env.currentEnv != null then + env_set6(env; key; value) + else + env_set(env; key; value) + end; + +def addToEnv6(envexp; name): + envexp.expr as $value + | envexp.env as $rawEnv + | (if $rawEnv.isReplEnv then + env_set_($rawEnv.currentEnv; name; $value) | wrapEnv($rawEnv.atoms) + else + env_set_($rawEnv.currentEnv; name; $value) | wrapEnv($rawEnv.replEnv; $rawEnv.atoms) + end) as $newEnv + | { + expr: $value, + env: $newEnv + }; + +def addToEnv(envexp; name): + if envexp.env.replEnv != null then + addToEnv6(envexp; name) + else { + expr: envexp.expr, + env: env_set_(envexp.env; name; envexp.expr) + } end; + +def _env_remove_references(refs): + if . != null then + { + environment: (.environment | to_entries | map(select(.key as $key | refs | contains([$key]) | not)) | from_entries), + parent: (.parent | _env_remove_references(refs)), + fallback: (.fallback | _env_remove_references(refs)) + } + else . end; + +def env_remove_references(refs): + . as $env + | if has("replEnv") then + .currentEnv |= _env_remove_references(refs) + else + _env_remove_references(refs) + end; + +# Evaluation + +def arg_check(args): + if .inputs < 0 then + if (abs(.inputs) - 1) > (args | length) then + jqmal_error("Invalid number of arguments (expected at least \(abs(.inputs) - 1), got \(args|length))") + else + . + end + else if .inputs != (args|length) then + jqmal_error("Invalid number of arguments (expected \(.inputs), got \(args|length))") + else + . + end end; + +def addFrees(newEnv; frees): + . as $env + | reduce frees[] as $free ( + $env; + . as $dot + | env_req(newEnv; $free) as $lookup + | if $lookup != null then + env_set_(.; $free; $lookup) + else + . + end) + | . as $env + | $env; + +def interpret(arguments; env; _eval): + (if $DEBUG then _debug("INTERP: \(. | pr_str(env))") else . end) | + (select(.kind == "fn") | + arg_check(arguments) | + (core_interp(arguments; env) | addEnv(env)) + ) // + (select(.kind == "function") as $fn | + # todo: arg_check + (.body | pr_str(env)) as $src | + # _debug("INTERP " + $src) | + # _debug("FREES " + ($fn.free_referencess | tostring)) | + env_setfallback((.env | addFrees(env; $fn.free_referencess)); env) | childEnv($fn.binds; arguments) as $fnEnv | + # tell it about its surroundings + (reduce $fn.free_referencess[] as $name ( + $fnEnv; + . as $env | try env_set( + .; + $name; + $name | env_get(env) | . as $xvalue + | if $xvalue.kind == "function" then + setpath(["free_referencess"]; $fn.free_referencess) + else + $xvalue + end + ) catch $env)) as $fnEnv | + # tell it about itself + env_multiset($fnEnv; $fn.names; $fn) as $fnEnv | + { + env: env_multiset($fnEnv; $fn.names; $fn), + expr: $fn.body + } + | . as $dot + # | _debug("FNEXEC " + (.expr | pr_str) + " " + (env_req($dot.env; $fn.binds[0]) | pr_str)) + | _eval + | . as $envexp + | + { + expr: .expr, + env: env + } + # | . as $dot + # | _debug("FNPOST " + (.expr | pr_str) + " " + (env_req($dot.expr.env; $fn.binds[0]) | pr_str)) + # | _debug("INTERP " + $src + " = " + (.expr|pr_str)) + ) // + jqmal_error("Unsupported function kind \(.kind)"); + +def recurseflip(x; y): + recurse(y; x); + +def TCOWrap(env; retenv; continue): + { + ast: ., + env: env, + ret_env: retenv, + finish: (continue | not), + cont: true # set inside + }; + +def EVAL(env): + def _eval_here: + .env as $env | .expr | EVAL($env); + + def hmap_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem[1] | EVAL($env) as $resv | + { value: [$elem[0], $resv.expr], env: env }, + ({env: $resv.env, list: $rest} | hmap_with_env) + end; + def map_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem | EVAL($env) as $resv | + { value: $resv.expr, env: env }, + ({env: $resv.env, list: $rest} | map_with_env) + end; + (select(.kind == "list") | + if .value | length == 0 then + . + else + ( + ( + .value | select(.[0].value == "def!") as $value | + ($value[2] | EVAL(env)) as $evval | + addToEnv($evval; $value[1].value) + ) // + ( + .value | select(.[0].value == "let*") as $value | + (env | pureChildEnv) as $subenv | + (reduce ($value[1].value | nwise(2)) as $xvalue ( + $subenv; + . as $env | $xvalue[1] | EVAL($env) as $expenv | + env_set($expenv.env; $xvalue[0].value; $expenv.expr))) as $env + | $value[2] | { expr: EVAL($env).expr, env: env } + ) // + ( + .value | select(.[0].value == "do") as $value | + (reduce ($value[1:][]) as $xvalue ( + { env: env, expr: {kind:"nil"} }; + .env as $env | $xvalue | EVAL($env) + )) + ) // + ( + .value | select(.[0].value == "if") as $value | + $value[1] | EVAL(env) as $condenv | + if (["false", "nil"] | contains([$condenv.expr.kind])) then + ($value[3] // {kind:"nil"}) | EVAL($condenv.env) + else + $value[2] | EVAL($condenv.env) + end + ) // + ( + .value | select(.[0].value == "fn*") as $value | + # we can't do what the guide says, so we'll skip over this + # and ues the later implementation + # (fn* args body) + $value[1].value | map(.value) as $binds | { + kind: "function", + binds: $binds, + env: env, + body: $value[2], + names: [], # we can't do that circular reference thing + free_referencess: $value[2] | find_free_references(env | env_dump_keys + $binds) # for dynamically scoped variables + } | addEnv(env) + ) // + ( + reduce .value[] as $elem ( + []; + . as $dot | $elem | EVAL(env) as $eval_env | + ($dot + [$eval_env.expr]) + ) | { expr: ., env: env } as $ev + | $ev.expr | first | + interpret($ev.expr[1:]; $ev.env; _eval_here) + ) // + addEnv(env) + ) + end + ) // + (select(.kind == "vector") | + if .value|length == 0 then + { + kind: "vector", + value: [] + } | addEnv(env) + else + [ { env: env, list: .value } | map_with_env ] as $res | + { + kind: "vector", + value: $res | map(.value) + } | addEnv($res | last.env) + end + ) // + (select(.kind == "hashmap") | + [ { env: env, list: .value | to_entries } | hmap_with_env ] as $res | + { + kind: "hashmap", + value: $res | map(.value) | from_entries + } | addEnv($res | last.env) + ) // + (select(.kind == "function") | + . | addEnv(env) # return this unchanged, since it can only be applied to + ) // + (select(.kind == "symbol") | + .value | env_get(env) | addEnv(env) + ) // addEnv(env); + +def PRINT: + pr_str; + +def rep(env): + READ | EVAL(env) as $expenv | + if $expenv.expr != null then + $expenv.expr | PRINT + else + null + end | addEnv($expenv.env); + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + parent: null, + environment: ({ + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + } + core_identify), + fallback: null + }; + +def repl(env): + def xrepl: + (.env as $env | try repl_($env) catch addEnv($env)) as $expenv | + { + value: $expenv.expr, + stop: false, + env: ($expenv.env // .env) + } | ., xrepl; + {stop: false, env: env} | xrepl | if .value then (.value | _display) else empty end; + +repl( + "(def! not (fn* (a) (if a false true)))" | rep(replEnv) | .env +) diff --git a/jq/step5_tco.jq b/jq/step5_tco.jq new file mode 100644 index 00000000..c2053e30 --- /dev/null +++ b/jq/step5_tco.jq @@ -0,0 +1,582 @@ +include "reader"; +include "printer"; +include "utils"; +include "core"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +# Environment Functions + +def childEnv(binds; exprs): + { + parent: ., + fallback: null, + environment: [binds, exprs] | transpose | ( + . as $dot | reduce .[] as $item ( + { value: [], seen: false, name: null, idx: 0 }; + if $item[1] != null then + if .seen then + { + value: (.value[1:-1] + (.value|last[1].value += [$item[1]])), + seen: true, + name: .name + } + else + if $item[0] == "&" then + $dot[.idx+1][0] as $name | { + value: (.value + [[$name, {kind:"list", value: [$item[1]]}]]), + seen: true, + name: $name + } + else + { + value: (.value + [$item]), + seen: false, + name: null + } + end + end | (.idx |= .idx + 1) + else + if $item[0] == "&" then + $dot[.idx+1][0] as $name | { + value: (.value + [[$name, {kind:"list", value: []}]]), + seen: true, + name: $name + } + else . end + end + ) + ) | .value | map({(.[0]): .[1]}) | add + }; + +def pureChildEnv: + { + parent: ., + environment: {}, + fallback: null + }; + +def rootEnv: + { + parent: null, + fallback: null, + environment: {} + }; + +def inform_function(name): + (.names += [name]) | (.names |= unique); + +def inform_function_multi(names): + . as $dot | reduce names[] as $name( + $dot; + inform_function($name) + ); + +def env_multiset(keys; value): + (if value.kind == "function" then # multiset not allowed on atoms + value | inform_function_multi(keys) + else + value + end) as $value | { + parent: .parent, + environment: ( + .environment + (reduce keys[] as $key(.environment; .[$key] |= value)) + ), + fallback: .fallback + }; + +def env_multiset(env; keys; value): + env | env_multiset(keys; value); + +def env_set($key; $value): + (if $value.kind == "function" or $value.kind == "atom" then + # inform the function/atom of its names + ($value | + if $value.kind == "atom" then + # check if the one we have is newer + env_req(env; key) as $ours | + if $ours.last_modified > $value.last_modified then + $ours + else + # update modification timestamp + $value | .last_modified |= now + end + else + . + end) | inform_function($key) + else + $value + end) as $value | { + parent: .parent, + environment: (.environment + (.environment | .[$key] |= $value)), # merge together, as .environment[key] |= value does not work + fallback: .fallback + }; + +def env_dump_keys: + def _dump1: + .environment // {} | keys; + if . == null then [] else + if .parent == null then + ( + _dump1 + + (.fallback | env_dump_keys) + ) + else + ( + _dump1 + + (.parent | env_dump_keys) + + (.fallback | env_dump_keys) + ) + end | unique + end; + +def env_find(env): + if env.environment[.] == null then + if env.parent then + env_find(env.parent) // if env.fallback then env_find(env.fallback) else null end + else + null + end + else + env + end; + +def env_get(env): + . as $key | $key | env_find(env).environment[$key] as $value | + if $value == null then + jqmal_error("'\($key)' not found") + else + if $value.kind == "atom" then + $value.identity as $id | + $key | env_find(env.parent).environment[$key] as $possibly_newer | + if $possibly_newer.identity == $id and $possibly_newer.last_modified > $value.last_modified then + $possibly_newer + else + $value + end + else + $value + end + end; + +def env_get(env; key): + key | env_get(env); + +def env_req(env; key): + key as $key | key | env_find(env).environment[$key] as $value | + if $value == null then + null + else + if $value.kind == "atom" then + $value.identity as $id | + $key | env_find(env.parent).environment[$key] as $possibly_newer | + if $possibly_newer.identity == $id and $possibly_newer.last_modified > $value.last_modified then + $possibly_newer + else + $value + end + else + $value + end + end; + +def env_set(env; $key; $value): + (if $value.kind == "function" or $value.kind == "atom" then + # inform the function/atom of its names + $value | (.names += [$key]) | (.names |= unique) | + if $value.kind == "atom" then + # check if the one we have is newer + env_req(env; $key) as $ours | + if $ours.last_modified > $value.last_modified then + $ours + else + # update modification timestamp + $value | .last_modified |= now + end + else + . + end + else + $value + end) as $value | { + parent: env.parent, + environment: ((env.environment // jqmal_error("Environment empty in \(env | keys)")) + (env.environment | .[$key] |= $value)), # merge together, as env.environment[key] |= value does not work + fallback: env.fallback + }; + +def env_setfallback(env; fallback): + { + parent: env.parent, + fallback: fallback, + environment: env.environment + }; + +def addEnv(env): + { + expr: ., + env: env + }; + +def addToEnv(env; name; expr): + { + expr: expr, + env: env_set(env; name; expr) + }; + + +def wrapEnv(atoms): + { + replEnv: ., + currentEnv: ., + atoms: atoms, + isReplEnv: true + }; + +def wrapEnv(replEnv; atoms): + { + replEnv: replEnv, + currentEnv: ., + atoms: atoms, # id -> value + isReplEnv: (replEnv == .) # should we allow separate copies? + }; + +def unwrapReplEnv: + .replEnv; + +def unwrapCurrentEnv: + .currentEnv; + +def env_set6(env; key; value): + if env.isReplEnv then + env_set(env.currentEnv; key; value) | wrapEnv(env.atoms) + else + env_set(env.currentEnv; key; value) | wrapEnv(env.replEnv; env.atoms) + end; + +def env_set_(env; key; value): + if env.currentEnv != null then + env_set6(env; key; value) + else + env_set(env; key; value) + end; + +def addToEnv6(envexp; name): + envexp.expr as $value + | envexp.env as $rawEnv + | (if $rawEnv.isReplEnv then + env_set_($rawEnv.currentEnv; name; $value) | wrapEnv($rawEnv.atoms) + else + env_set_($rawEnv.currentEnv; name; $value) | wrapEnv($rawEnv.replEnv; $rawEnv.atoms) + end) as $newEnv + | { + expr: $value, + env: $newEnv + }; + +def addToEnv(envexp; name): + if envexp.env.replEnv != null then + addToEnv6(envexp; name) + else { + expr: envexp.expr, + env: env_set_(envexp.env; name; envexp.expr) + } end; + +def _env_remove_references(refs): + if . != null then + { + environment: (.environment | to_entries | map(select(.key as $key | refs | contains([$key]) | not)) | from_entries), + parent: (.parent | _env_remove_references(refs)), + fallback: (.fallback | _env_remove_references(refs)) + } + else . end; + +def env_remove_references(refs): + . as $env + | if has("replEnv") then + .currentEnv |= _env_remove_references(refs) + else + _env_remove_references(refs) + end; + +# Evaluation + +def arg_check(args): + if .inputs < 0 then + if (abs(.inputs) - 1) > (args | length) then + jqmal_error("Invalid number of arguments (expected at least \(abs(.inputs) - 1), got \(args|length))") + else + . + end + else if .inputs != (args|length) then + jqmal_error("Invalid number of arguments (expected \(.inputs), got \(args|length))") + else + . + end end; + +def addFrees(newEnv; frees): + . as $env + | reduce frees[] as $free ( + $env; + . as $dot + | env_req(newEnv; $free) as $lookup + | if $lookup != null then + env_set_(.; $free; $lookup) + else + . + end) + | . as $env + | $env; + +def interpret(arguments; env; _eval): + (if $DEBUG then _debug("INTERP: \(. | pr_str(env))") else . end) | + (select(.kind == "fn") | + arg_check(arguments) | + (core_interp(arguments; env) | addEnv(env)) + ) // + (select(.kind == "function") as $fn | + # todo: arg_check + (.body | pr_str(env)) as $src | + # _debug("INTERP " + $src) | + # _debug("FREES " + ($fn.free_referencess | tostring)) | + env_setfallback((.env | addFrees(env; $fn.free_referencess)); env) | childEnv($fn.binds; arguments) as $fnEnv | + # tell it about its surroundings + (reduce $fn.free_referencess[] as $name ( + $fnEnv; + . as $env | try env_set( + .; + $name; + $name | env_get(env) | . as $xvalue + | if $xvalue.kind == "function" then + setpath(["free_referencess"]; $fn.free_referencess) + else + $xvalue + end + ) catch $env)) as $fnEnv | + # tell it about itself + env_multiset($fnEnv; $fn.names; $fn) as $fnEnv | + { + env: env_multiset($fnEnv; $fn.names; $fn), + expr: $fn.body + } + | . as $dot + # | _debug("FNEXEC " + (.expr | pr_str) + " " + (env_req($dot.env; $fn.binds[0]) | pr_str)) + | _eval + | . as $envexp + | + { + expr: .expr, + env: env + } + # | . as $dot + # | _debug("FNPOST " + (.expr | pr_str) + " " + (env_req($dot.expr.env; $fn.binds[0]) | pr_str)) + # | _debug("INTERP " + $src + " = " + (.expr|pr_str)) + ) // + jqmal_error("Unsupported function kind \(.kind)"); + +def recurseflip(x; y): + recurse(y; x); + +def TCOWrap(env; retenv; continue): + { + ast: ., + env: env, + ret_env: retenv, + finish: (continue | not), + cont: true # set inside + }; + +def EVAL(env): + def _eval_here: + .env as $env | .expr | EVAL($env); + + def hmap_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem[1] | EVAL($env) as $resv | + { value: [$elem[0], $resv.expr], env: env }, + ({env: $resv.env, list: $rest} | hmap_with_env) + end; + def map_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem | EVAL($env) as $resv | + { value: $resv.expr, env: env }, + ({env: $resv.env, list: $rest} | map_with_env) + end; + . as $ast + | { env: env, ast: ., cont: true, finish: false, ret_env: null } + | [ recurseflip(.cont; + .env as $_menv + | if .finish then + .cont |= false + else + (.ret_env//.env) as $_retenv + | .ret_env as $_orig_retenv + | .ast + | + (select(.kind == "list") | + if .value | length == 0 then + . | TCOWrap($_menv; $_orig_retenv; false) + else + ( + ( + .value | select(.[0].value == "def!") as $value | + ($value[2] | EVAL($_menv)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "let*") as $value | + ($_menv | pureChildEnv) as $subenv | + (reduce ($value[1].value | nwise(2)) as $xvalue ( + $subenv; + . as $env | $xvalue[1] | EVAL($env) as $expenv | + env_set($expenv.env; $xvalue[0].value; $expenv.expr))) as $env + | $value[2] | TCOWrap($env; $_retenv; true) + ) // + ( + .value | select(.[0].value == "do") as $value | + (reduce ($value[1:][]) as $xvalue ( + { env: $_menv, expr: {kind:"nil"} }; + .env as $env | $xvalue | EVAL($env) + )) | . as $ex | .expr | TCOWrap($ex.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "if") as $value | + $value[1] | EVAL(env) as $condenv | + (if (["false", "nil"] | contains([$condenv.expr.kind])) then + ($value[3] // {kind:"nil"}) + else + $value[2] + end) | TCOWrap($condenv.env; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "fn*") as $value | + # (fn* args body) + $value[1].value | map(.value) as $binds | { + kind: "function", + binds: $binds, + env: $_menv, + body: $value[2], + names: [], # we can't do that circular reference thing + free_referencess: $value[2] | find_free_references($_menv | env_dump_keys + $binds) # for dynamically scoped variables + } | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + reduce .value[] as $elem ( + []; + . as $dot | $elem | EVAL($_menv) as $eval_env | + ($dot + [$eval_env.expr]) + ) | . as $expr | first | + interpret($expr[1:]; $_menv; _eval_here) as $exprenv | + $exprenv.expr | TCOWrap($exprenv.env; $_orig_retenv; false) + ) // + TCOWrap($_menv; $_orig_retenv; false) + ) + end + ) // + (select(.kind == "vector") | + if .value|length == 0 then + { + kind: "vector", + value: [] + } | TCOWrap($_menv; $_orig_retenv; false) + else + [ { env: $_menv, list: .value } | map_with_env ] as $res | + { + kind: "vector", + value: $res | map(.value) + } | TCOWrap($res | last.env; $_orig_retenv; false) + end + ) // + (select(.kind == "hashmap") | + [ { env: $_menv, list: .value | to_entries } | hmap_with_env ] as $res | + { + kind: "hashmap", + value: $res | map(.value) | from_entries + } | TCOWrap($res | last.env; $_orig_retenv; false) + ) // + (select(.kind == "function") | + . | TCOWrap($_menv; $_orig_retenv; false) # return this unchanged, since it can only be applied to + ) // + (select(.kind == "symbol") | + .value | env_get($_menv) | TCOWrap($_menv; null; false) + ) // TCOWrap($_menv; $_orig_retenv; false) + end + ) ] + | last as $result + | ($result.ret_env // $result.env) as $env + | $result.ast + | addEnv($env); + +def PRINT: + pr_str; + +def rep(env): + READ | EVAL(env) as $expenv | + if $expenv.expr != null then + $expenv.expr | PRINT + else + null + end | addEnv($expenv.env); + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + parent: null, + environment: ({ + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + } + core_identify), + fallback: null + }; + +def repl(env): + def xrepl: + (.env as $env | try repl_($env) catch addEnv($env)) as $expenv | + { + value: $expenv.expr, + stop: false, + env: ($expenv.env // .env) + } | ., xrepl; + {stop: false, env: env} | xrepl | if .value then (.value | _display) else empty end; + +repl( + "(def! not (fn* (a) (if a false true)))" | rep(replEnv) | .env +) diff --git a/jq/step6_file.jq b/jq/step6_file.jq new file mode 100644 index 00000000..f58e5931 --- /dev/null +++ b/jq/step6_file.jq @@ -0,0 +1,253 @@ +include "reader"; +include "printer"; +include "utils"; +include "interp"; +include "env"; +include "core"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +def recurseflip(x; y): + recurse(y; x); + +def TCOWrap(env; retenv; continue): + { + ast: ., + env: env, + ret_env: (if retenv != null then (retenv | setpath(["atoms"]; env.atoms)) else retenv end), + finish: (continue | not), + cont: true # set inside + }; + +def EVAL(env): + def _eval_here: + .env as $env | .expr | EVAL($env); + + def hmap_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem.value.value | EVAL($env) as $resv | + { + value: { + key: $elem.key, + value: { kkind: $elem.value.kkind, value: $resv.expr } + }, + env: env + }, + ({env: $resv.env, list: $rest} | hmap_with_env) + end; + def map_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem | EVAL($env) as $resv | + { value: $resv.expr, env: env }, + ({env: $resv.env, list: $rest} | map_with_env) + end; + . as $ast + | { env: env, ast: ., cont: true, finish: false, ret_env: null } + | [ recurseflip(.cont; + .env as $_menv + | if .finish then + .cont |= false + else + (.ret_env//.env) as $_retenv + | .ret_env as $_orig_retenv + | .ast + | . as $init + | $_menv | unwrapCurrentEnv as $currentEnv # unwrap env "package" + | $_menv | unwrapReplEnv as $replEnv # - + | $init + | + (select(.kind == "list") | + if .value | length == 0 then + . | TCOWrap($_menv; $_orig_retenv; false) + else + ( + ( + .value | select(.[0].value == "def!") as $value | + ($value[2] | EVAL($_menv)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "let*") as $value | + ($currentEnv | pureChildEnv | wrapEnv($replEnv; $_menv.atoms)) as $subenv | + (reduce ($value[1].value | nwise(2)) as $xvalue ( + $subenv; + . as $env | $xvalue[1] | EVAL($env) as $expenv | + env_set_($expenv.env; $xvalue[0].value; $expenv.expr))) | . as $env + | $value[2] | TCOWrap($env; $_retenv; true) + ) // + ( + .value | select(.[0].value == "do") as $value | + (reduce ($value[1:][]) as $xvalue ( + { env: $_menv, expr: {kind:"nil"} }; + .env as $env | $xvalue | EVAL($env) + )) | . as $ex | .expr | TCOWrap($ex.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "if") as $value | + $value[1] | EVAL($_menv) as $condenv | + (if (["false", "nil"] | contains([$condenv.expr.kind])) then + ($value[3] // {kind:"nil"}) + else + $value[2] + end) | TCOWrap($condenv.env; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "fn*") as $value | + # (fn* args body) + $value[1].value | map(.value) as $binds | + ($value[2] | find_free_references($currentEnv | env_dump_keys + $binds)) as $free_referencess | { + kind: "function", + binds: $binds, + env: (env | env_remove_references($free_referencess)), + body: $value[2], + names: [], # we can't do that circular reference thing + free_referencess: $free_referencess # for dynamically scoped variables + } | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + reduce .value[] as $elem ( + {env: $_menv, val: []}; + . as $dot | $elem | EVAL($dot.env) as $eval_env | + ($dot.env | setpath(["atoms"]; $eval_env.env.atoms)) as $_menv | + {env: $_menv, val: ($dot.val + [$eval_env.expr])} + ) | . as $expr | $expr.val | first | + interpret($expr.val[1:]; $expr.env; _eval_here) as $exprenv | + $exprenv.expr | TCOWrap($exprenv.env; $_orig_retenv; false) + ) // + TCOWrap($_menv; $_orig_retenv; false) + ) + end + ) // + (select(.kind == "vector") | + if .value|length == 0 then + { + kind: "vector", + value: [] + } | TCOWrap($_menv; $_orig_retenv; false) + else + [ { env: env, list: .value } | map_with_env ] as $res | + { + kind: "vector", + value: $res | map(.value) + } | TCOWrap($res | last.env; $_orig_retenv; false) + end + ) // + (select(.kind == "hashmap") | + [ { env: env, list: (.value | to_entries) } | hmap_with_env ] as $res | + { + kind: "hashmap", + value: $res | map(.value) | from_entries + } | TCOWrap($res | last.env; $_orig_retenv; false) + ) // + (select(.kind == "function") | + . | TCOWrap($_menv; $_orig_retenv; false) # return this unchanged, since it can only be applied to + ) // + (select(.kind == "symbol") | + .value | env_get($currentEnv) | TCOWrap($_menv; null; false) + ) // TCOWrap($_menv; $_orig_retenv; false) + end + ) ] + | last as $result + | ($result.ret_env // $result.env) as $env + | $result.ast + | addEnv($env); + +def PRINT(env): + pr_str(env); + +def rep(env): + READ | EVAL(env) as $expenv | + if $expenv.expr != null then + $expenv.expr | PRINT($expenv.env) + else + null + end | addEnv($expenv.env); + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + parent: null, + environment: ({ + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + "eval": { + kind: "fn", + inputs: 1, + function: "eval" + } + } + core_identify), + fallback: null, + }; + +def repl(env): + def xrepl: + (.env as $env | try repl_($env) catch addEnv($env)) as $expenv | + { + value: $expenv.expr, + stop: false, + env: ($expenv.env // .env) + } | ., xrepl; + {stop: false, env: env} | xrepl | if .value then (.value | _display) else empty end; + +def eval_ign(expr): + . as $env | expr | rep($env) | .env; + +def eval_val(expr): + . as $env | expr | rep($env) | .expr; + +def getEnv: + replEnv + | wrapEnv({}) + | eval_ign("(def! not (fn* (a) (if a false true)))") + | eval_ign("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\\nnil)\")))))))"); + +def main: + if $ARGS.positional|length > 0 then + getEnv as $env | + env_set_($env; "*ARGV*"; $ARGS.positional[1:] | map(wrap("string")) | wrap("list")) | + eval_val("(load-file \($ARGS.positional[0] | tojson))") | + "" + else + repl( getEnv as $env | env_set_($env; "*ARGV*"; [] | wrap("list")) ) + end; + +[ main ] | _halt diff --git a/jq/step7_quote.jq b/jq/step7_quote.jq new file mode 100644 index 00000000..123f2cc5 --- /dev/null +++ b/jq/step7_quote.jq @@ -0,0 +1,295 @@ +include "reader"; +include "printer"; +include "utils"; +include "interp"; +include "env"; +include "core"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +def recurseflip(x; y): + recurse(y; x); + +def TCOWrap(env; retenv; continue): + { + ast: ., + env: env, + ret_env: (if retenv != null then (retenv | setpath(["atoms"]; env.atoms)) else retenv end), + finish: (continue | not), + cont: true # set inside + }; + +def _symbol(name): + { + kind: "symbol", + value: name + }; + +def _symbol_v(name): + if .kind == "symbol" then + .value == name + else + false + end; + +def quasiquote: + if isPair then + .value as $value | null | + if ($value[0] | _symbol_v("unquote")) then + $value[1] + else + if isPair($value[0]) and ($value[0].value[0] | _symbol_v("splice-unquote")) then + [_symbol("concat")] + + [$value[0].value[1]] + + [($value[1:] | wrap("list") | quasiquote)] | wrap("list") + else + [_symbol("cons")] + + [($value[0] | quasiquote)] + + [($value[1:] | wrap("list") | quasiquote)] | wrap("list") + end + end + else + [_symbol("quote")] + + [.] | wrap("list") + end; + +def EVAL(env): + def _eval_here: + .env as $env | .expr | EVAL($env); + + def hmap_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem.value.value | EVAL($env) as $resv | + { + value: { + key: $elem.key, + value: { kkind: $elem.value.kkind, value: $resv.expr } + }, + env: env + }, + ({env: $resv.env, list: $rest} | hmap_with_env) + end; + def map_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem | EVAL($env) as $resv | + { value: $resv.expr, env: env }, + ({env: $resv.env, list: $rest} | map_with_env) + end; + . as $ast + | { env: env, ast: ., cont: true, finish: false, ret_env: null } + | [ recurseflip(.cont; + .env as $_menv + | if .finish then + .cont |= false + else + (.ret_env//.env) as $_retenv + | .ret_env as $_orig_retenv + | .ast + | . as $init + | $_menv | unwrapCurrentEnv as $currentEnv # unwrap env "package" + | $_menv | unwrapReplEnv as $replEnv # - + | $init + | + (select(.kind == "list") | + if .value | length == 0 then + . | TCOWrap($_menv; $_orig_retenv; false) + else + ( + ( + .value | select(.[0].value == "def!") as $value | + ($value[2] | EVAL($_menv)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "let*") as $value | + ($currentEnv | pureChildEnv | wrapEnv($replEnv; $_menv.atoms)) as $_menv | + (reduce ($value[1].value | nwise(2)) as $xvalue ( + $_menv; + . as $env | $xvalue[1] | EVAL($env) as $expenv | + env_set_($expenv.env; $xvalue[0].value; $expenv.expr))) as $env + | $value[2] | TCOWrap($env; $_retenv; true) + ) // + ( + .value | select(.[0].value == "do") as $value | + (reduce ($value[1:][]) as $xvalue ( + { env: $_menv, expr: {kind:"nil"} }; + .env as $env | $xvalue | EVAL($env) + )) | . as $ex | .expr | TCOWrap($ex.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "if") as $value | + $value[1] | EVAL($_menv) as $condenv | + (if (["false", "nil"] | contains([$condenv.expr.kind])) then + ($value[3] // {kind:"nil"}) + else + $value[2] + end) | TCOWrap($condenv.env; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "fn*") as $value | + # (fn* args body) + $value[1].value | map(.value) as $binds | + ($value[2] | find_free_references($currentEnv | env_dump_keys + $binds)) as $free_referencess | { + kind: "function", + binds: $binds, + env: (env | env_remove_references($free_referencess)), + body: $value[2], + names: [], # we can't do that circular reference thing + free_referencess: $free_referencess # for dynamically scoped variables + } | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "quote") as $value | + $value[1] | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "quasiquote") as $value | + $value[1] | quasiquote | TCOWrap($_menv; $_orig_retenv; true) + ) // + ( + reduce .value[] as $elem ( + {env: $_menv, val: []}; + . as $dot | $elem | EVAL($dot.env) as $eval_env | + ($dot.env | setpath(["atoms"]; $eval_env.env.atoms)) as $_menv | + {env: $_menv, val: ($dot.val + [$eval_env.expr])} + ) | . as $expr | $expr.val | first | + interpret($expr.val[1:]; $expr.env; _eval_here) as $exprenv | + $exprenv.expr | TCOWrap($exprenv.env; $_orig_retenv; false) + ) // + TCOWrap($_menv; $_orig_retenv; false) + ) + end + ) // + (select(.kind == "vector") | + if .value|length == 0 then + { + kind: "vector", + value: [] + } | TCOWrap($_menv; $_orig_retenv; false) + else + [ { env: env, list: .value } | map_with_env ] as $res | + { + kind: "vector", + value: $res | map(.value) + } | TCOWrap($res | last.env; $_orig_retenv; false) + end + ) // + (select(.kind == "hashmap") | + [ { env: env, list: (.value | to_entries) } | hmap_with_env ] as $res | + { + kind: "hashmap", + value: $res | map(.value) | from_entries + } | TCOWrap($res | last.env; $_orig_retenv; false) + ) // + (select(.kind == "function") | + . | TCOWrap($_menv; $_orig_retenv; false) # return this unchanged, since it can only be applied to + ) // + (select(.kind == "symbol") | + .value | env_get($currentEnv) | TCOWrap($_menv; null; false) + ) // TCOWrap($_menv; $_orig_retenv; false) + end + ) ] + | last as $result + | ($result.ret_env // $result.env) as $env + | $result.ast + | addEnv($env); + +def PRINT(env): + pr_str(env); + +def rep(env): + READ | EVAL(env) as $expenv | + if $expenv.expr != null then + $expenv.expr | PRINT($expenv.env) + else + null + end | addEnv($expenv.env); + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + parent: null, + environment: ({ + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + "eval": { + kind: "fn", + inputs: 1, + function: "eval" + } + } + core_identify), + fallback: null + }; + +def repl(env): + def xrepl: + (.env as $env | try repl_($env) catch addEnv($env)) as $expenv | + { + value: $expenv.expr, + stop: false, + env: ($expenv.env // .env) + } | ., xrepl; + {stop: false, env: env} | xrepl | if .value then (.value | _display) else empty end; + +def eval_ign(expr): + . as $env | expr | rep($env) | .env; + +def eval_val(expr): + . as $env | expr | rep($env) | .expr; + +def getEnv: + replEnv + | wrapEnv({}) + | eval_ign("(def! not (fn* (a) (if a false true)))") + | eval_ign("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\\nnil)\")))))))"); + +def main: + if $ARGS.positional|length > 0 then + getEnv as $env | + env_set_($env; "*ARGV*"; $ARGS.positional[1:] | map(wrap("string")) | wrap("list")) | + eval_val("(load-file \($ARGS.positional[0] | tojson))") | + "" + else + repl( getEnv as $env | env_set_($env; "*ARGV*"; [] | wrap("list")) ) + end; + +[ main ] | _halt diff --git a/jq/step8_macros.jq b/jq/step8_macros.jq new file mode 100644 index 00000000..46031c93 --- /dev/null +++ b/jq/step8_macros.jq @@ -0,0 +1,363 @@ +include "reader"; +include "printer"; +include "utils"; +include "interp"; +include "env"; +include "core"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +def recurseflip(x; y): + recurse(y; x); + +def TCOWrap(env; retenv; continue): + { + ast: ., + env: env, + ret_env: (if retenv != null then (retenv | setpath(["atoms"]; env.atoms)) else retenv end), + finish: (continue | not), + cont: true # set inside + }; + +def _symbol(name): + { + kind: "symbol", + value: name + }; + +def _symbol_v(name): + if .kind == "symbol" then + .value == name + else + false + end; + +def quasiquote: + if isPair then + .value as $value | null | + if ($value[0] | _symbol_v("unquote")) then + $value[1] + else + if isPair($value[0]) and ($value[0].value[0] | _symbol_v("splice-unquote")) then + [_symbol("concat")] + + [$value[0].value[1]] + + [($value[1:] | wrap("list") | quasiquote)] | wrap("list") + else + [_symbol("cons")] + + [($value[0] | quasiquote)] + + [($value[1:] | wrap("list") | quasiquote)] | wrap("list") + end + end + else + [_symbol("quote")] + + [.] | wrap("list") + end; + +def set_macro_function: + if .kind != "function" then + jqmal_error("expected a function to be defined by defmacro!") + else + .is_macro |= true + end; + +def is_macro_call(env): + if .kind != "list" then + false + else + if (.value|first.kind == "symbol") then + env_req(env; .value|first.value) + | if .kind != "function" then + false + else + .is_macro + end + else + false + end + end; + +def EVAL(env): + def _eval_here: + .env as $env | .expr | EVAL($env); + + def _interpret($_menv): + reduce .value[] as $elem ( + {env: $_menv, val: []}; + . as $dot | $elem | EVAL($dot.env) as $eval_env | + ($dot.env | setpath(["atoms"]; $eval_env.env.atoms)) as $_menv | + {env: $_menv, val: ($dot.val + [$eval_env.expr])} + ) | . as $expr | $expr.val | first | + interpret($expr.val[1:]; $expr.env; _eval_here); + + def macroexpand(env): + . as $dot | + $dot | + [ while(is_macro_call(env | unwrapCurrentEnv); + . as $dot + | ($dot.value[0] | EVAL(env).expr) as $fn + | $dot.value[1:] as $args + | $fn + | interpret($args; env; _eval_here).expr) // . ] + | last + | if is_macro_call(env | unwrapCurrentEnv) then + . as $dot + | ($dot.value[0] | EVAL(env).expr) as $fn + | $dot.value[1:] as $args + | $fn + | interpret($args; env; _eval_here).expr + else + . + end + ; + + def hmap_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem.value.value | EVAL($env) as $resv | + { + value: { + key: $elem.key, + value: { kkind: $elem.value.kkind, value: $resv.expr } + }, + env: env + }, + ({env: $resv.env, list: $rest} | hmap_with_env) + end; + def map_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem | EVAL($env) as $resv | + { value: $resv.expr, env: env }, + ({env: $resv.env, list: $rest} | map_with_env) + end; + def eval_ast(env): + (select(.kind == "vector") | + if .value|length == 0 then + { + kind: "vector", + value: [] + } + else + [ { env: env, list: .value } | map_with_env ] as $res | + { + kind: "vector", + value: $res | map(.value) + } + end + ) // + (select(.kind == "hashmap") | + [ { env: env, list: (.value | to_entries) } | hmap_with_env ] as $res | + { + kind: "hashmap", + value: $res | map(.value) | from_entries + } + ) // + (select(.kind == "function") | + .# return this unchanged, since it can only be applied to + ) // + (select(.kind == "symbol") | + .value | env_get(env | unwrapCurrentEnv) + ) // .; + + . as $ast + | { env: env, ast: ., cont: true, finish: false, ret_env: null } + | [ recurseflip(.cont; + .env as $_menv + | if .finish then + .cont |= false + else + (.ret_env//.env) as $_retenv + | .ret_env as $_orig_retenv + | .ast + | . as $init + | $_menv | unwrapCurrentEnv as $currentEnv # unwrap env "package" + | $_menv | unwrapReplEnv as $replEnv # - + | $init + | + (select(.kind == "list") | + macroexpand($_menv) | + if .kind != "list" then + eval_ast($_menv) | TCOWrap($_menv; $_orig_retenv; false) + else + if .value | length == 0 then + . | TCOWrap($_menv; $_orig_retenv; false) + else + ( + ( + .value | select(.[0].value == "def!") as $value | + ($value[2] | EVAL($_menv)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "defmacro!") as $value | + ($value[2] | EVAL($_menv) | (.expr |= set_macro_function)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "let*") as $value | + ($currentEnv | pureChildEnv | wrapEnv($replEnv; $_menv.atoms)) as $_menv | + (reduce ($value[1].value | nwise(2)) as $xvalue ( + $_menv; + . as $env | $xvalue[1] | EVAL($env) as $expenv | + env_set_($expenv.env; $xvalue[0].value; $expenv.expr))) as $env + | $value[2] | TCOWrap($env; $_retenv; true) + ) // + ( + .value | select(.[0].value == "do") as $value | + (reduce ($value[1:][]) as $xvalue ( + { env: $_menv, expr: {kind:"nil"} }; + .env as $env | $xvalue | EVAL($env) + )) | . as $ex | .expr | TCOWrap($ex.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "if") as $value | + $value[1] | EVAL($_menv) as $condenv | + (if (["false", "nil"] | contains([$condenv.expr.kind])) then + ($value[3] // {kind:"nil"}) + else + $value[2] + end) | TCOWrap($condenv.env; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "fn*") as $value | + # (fn* args body) + $value[1].value | map(.value) as $binds | + ($value[2] | find_free_references($currentEnv | env_dump_keys + $binds)) as $free_referencess | { + kind: "function", + binds: $binds, + env: (env | env_remove_references($free_referencess)), + body: $value[2], + names: [], # we can't do that circular reference thing + free_referencess: $free_referencess, # for dynamically scoped variables + is_macro: false + } | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "quote") as $value | + $value[1] | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "quasiquote") as $value | + $value[1] | quasiquote | TCOWrap($_menv; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "macroexpand") as $value | + $value[1] | macroexpand(env) | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + . as $dot | _interpret($_menv) as $exprenv | + $exprenv.expr | TCOWrap($exprenv.env; $_orig_retenv; false) + ) // + TCOWrap($_menv; $_orig_retenv; false) + ) + end + end + ) // + (eval_ast($_menv) | TCOWrap($_menv; $_orig_retenv; false)) + end + ) ] + | last as $result + | ($result.ret_env // $result.env) as $env + | $result.ast + | addEnv($env); + +def PRINT(env): + pr_str(env); + +def rep(env): + READ | EVAL(env) as $expenv | + if $expenv.expr != null then + $expenv.expr | PRINT($expenv.env) + else + null + end | addEnv($expenv.env); + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + parent: null, + environment: ({ + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + "eval": { + kind: "fn", + inputs: 1, + function: "eval" + } + } + core_identify), + fallback: null + }; + +def repl(env): + def xrepl: + (.env as $env | try repl_($env) catch addEnv($env)) as $expenv | + { + value: $expenv.expr, + stop: false, + env: ($expenv.env // .env) + } | ., xrepl; + {stop: false, env: env} | xrepl | if .value then (.value | _display) else empty end; + +def eval_ign(expr): + . as $env | expr | rep($env) | .env; + +def eval_val(expr): + . as $env | expr | rep($env) | .expr; + +def getEnv: + replEnv + | wrapEnv({}) + | eval_ign("(def! not (fn* (a) (if a false true)))") + | eval_ign("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\\nnil)\")))))))") + | eval_ign("(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)))))))") + ; + +def main: + if $ARGS.positional|length > 0 then + getEnv as $env | + env_set_($env; "*ARGV*"; $ARGS.positional[1:] | map(wrap("string")) | wrap("list")) | + eval_val("(load-file \($ARGS.positional[0] | tojson))") | + "" + else + repl( getEnv as $env | env_set_($env; "*ARGV*"; [] | wrap("list")) ) + end; + +[ main ] | _halt diff --git a/jq/step9_try.jq b/jq/step9_try.jq new file mode 100644 index 00000000..6d1c509a --- /dev/null +++ b/jq/step9_try.jq @@ -0,0 +1,392 @@ +include "reader"; +include "printer"; +include "utils"; +include "interp"; +include "env"; +include "core"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +def recurseflip(x; y): + recurse(y; x); + +def TCOWrap(env; retenv; continue): + { + ast: ., + env: env, + ret_env: (if retenv != null then (retenv | setpath(["atoms"]; env.atoms)) else retenv end), + finish: (continue | not), + cont: true # set inside + }; + +def _symbol(name): + { + kind: "symbol", + value: name + }; + +def _symbol_v(name): + if .kind == "symbol" then + .value == name + else + false + end; + +def quasiquote: + if isPair then + .value as $value | null | + if ($value[0] | _symbol_v("unquote")) then + $value[1] + else + if isPair($value[0]) and ($value[0].value[0] | _symbol_v("splice-unquote")) then + [_symbol("concat")] + + [$value[0].value[1]] + + [($value[1:] | wrap("list") | quasiquote)] | wrap("list") + else + [_symbol("cons")] + + [($value[0] | quasiquote)] + + [($value[1:] | wrap("list") | quasiquote)] | wrap("list") + end + end + else + [_symbol("quote")] + + [.] | wrap("list") + end; + +def set_macro_function: + if .kind != "function" then + jqmal_error("expected a function to be defined by defmacro!") + else + .is_macro |= true + end; + +def is_macro_call(env): + if .kind != "list" then + false + else + if (.value|first.kind == "symbol") then + env_req(env; .value|first.value) + | if .kind != "function" then + false + else + .is_macro + end + else + false + end + end; + +def EVAL(env): + def _eval_here: + .env as $env | .expr | EVAL($env); + + def _interpret($_menv): + reduce .value[] as $elem ( + {env: $_menv, val: []}; + . as $dot | $elem | EVAL($dot.env) as $eval_env | + ($dot.env | setpath(["atoms"]; $eval_env.env.atoms)) as $_menv | + {env: $_menv, val: ($dot.val + [$eval_env.expr])} + ) | . as $expr | $expr.val | first | + interpret($expr.val[1:]; $expr.env; _eval_here); + + def macroexpand(env): + . as $dot | + $dot | + [ while(is_macro_call(env | unwrapCurrentEnv); + . as $dot + | ($dot.value[0] | EVAL(env).expr) as $fn + | $dot.value[1:] as $args + | $fn + | interpret($args; env; _eval_here).expr) // . ] + | last + | if is_macro_call(env | unwrapCurrentEnv) then + . as $dot + | ($dot.value[0] | EVAL(env).expr) as $fn + | $dot.value[1:] as $args + | $fn + | interpret($args; env; _eval_here).expr + else + . + end + ; + + def hmap_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem.value.value | EVAL($env) as $resv | + { + value: { + key: $elem.key, + value: { kkind: $elem.value.kkind, value: $resv.expr } + }, + env: env + }, + ({env: $resv.env, list: $rest} | hmap_with_env) + end; + def map_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem | EVAL($env) as $resv | + { value: $resv.expr, env: env }, + ({env: $resv.env, list: $rest} | map_with_env) + end; + def eval_ast(env): + (select(.kind == "vector") | + if .value|length == 0 then + { + kind: "vector", + value: [] + } + else + [ { env: env, list: .value } | map_with_env ] as $res | + { + kind: "vector", + value: $res | map(.value) + } + end + ) // + (select(.kind == "hashmap") | + [ { env: env, list: (.value | to_entries) } | hmap_with_env ] as $res | + { + kind: "hashmap", + value: $res | map(.value) | from_entries + } + ) // + (select(.kind == "function") | + .# return this unchanged, since it can only be applied to + ) // + (select(.kind == "symbol") | + .value | env_get(env | unwrapCurrentEnv) + ) // .; + + . as $ast + | { env: env, ast: ., cont: true, finish: false, ret_env: null } + | [ recurseflip(.cont; + .env as $_menv + | if .finish then + .cont |= false + else + (.ret_env//.env) as $_retenv + | .ret_env as $_orig_retenv + | .ast + | . as $init + | $_menv | unwrapCurrentEnv as $currentEnv # unwrap env "package" + | $_menv | unwrapReplEnv as $replEnv # - + | $init + | + (select(.kind == "list") | + macroexpand($_menv) | + if .kind != "list" then + eval_ast($_menv) | TCOWrap($_menv; $_orig_retenv; false) + else + if .value | length == 0 then + . | TCOWrap($_menv; $_orig_retenv; false) + else + ( + ( + .value | select(.[0].value == "def!") as $value | + ($value[2] | EVAL($_menv)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "defmacro!") as $value | + ($value[2] | EVAL($_menv) | (.expr |= set_macro_function)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "let*") as $value | + ($currentEnv | pureChildEnv | wrapEnv($replEnv; $_menv.atoms)) as $_menv | + (reduce ($value[1].value | nwise(2)) as $xvalue ( + $_menv; + . as $env | $xvalue[1] | EVAL($env) as $expenv | + env_set_($expenv.env; $xvalue[0].value; $expenv.expr))) as $env + | $value[2] | TCOWrap($env; $_retenv; true) + ) // + ( + .value | select(.[0].value == "do") as $value | + (reduce ($value[1:][]) as $xvalue ( + { env: $_menv, expr: {kind:"nil"} }; + .env as $env | $xvalue | EVAL($env) + )) | . as $ex | .expr | TCOWrap($ex.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "try*") as $value | + try ( + $value[1] | EVAL($_menv) as $exp | $exp.expr | TCOWrap($exp.env; $_orig_retenv; false) + ) catch ( . as $exc | + if $value[2] then + if ($value[2].value[0] | _symbol_v("catch*")) then + (if ($exc | is_jqmal_error) then + $exc[19:] as $ex | + try ( + $ex + | fromjson + ) catch ( + $ex | + wrap("string") + ) + else + $exc|wrap("string") + end) as $exc | + $value[2].value[2] | EVAL($currentEnv | childEnv([$value[2].value[1].value]; [$exc]) | wrapEnv($replEnv; $_menv.atoms)) as $ex | + $ex.expr | TCOWrap($ex.env; $_retenv; false) + else + error($exc) + end + else + error($exc) + end + ) + ) // + ( + .value | select(.[0].value == "if") as $value | + $value[1] | EVAL($_menv) as $condenv | + (if (["false", "nil"] | contains([$condenv.expr.kind])) then + ($value[3] // {kind:"nil"}) + else + $value[2] + end) | TCOWrap($condenv.env; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "fn*") as $value | + # (fn* args body) + $value[1].value | map(.value) as $binds | + ($value[2] | find_free_references($currentEnv | env_dump_keys + $binds)) as $free_referencess | { + kind: "function", + binds: $binds, + env: (env | env_remove_references($free_referencess)), + body: $value[2], + names: [], # we can't do that circular reference thing + free_referencess: $free_referencess, # for dynamically scoped variables + is_macro: false + } | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "quote") as $value | + $value[1] | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "quasiquote") as $value | + $value[1] | quasiquote | TCOWrap($_menv; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "macroexpand") as $value | + $value[1] | macroexpand(env) | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + . as $dot | _interpret($_menv) as $exprenv | + $exprenv.expr | TCOWrap($exprenv.env; $_orig_retenv; false) + ) // + TCOWrap($_menv; $_orig_retenv; false) + ) + end + end + ) // + (eval_ast($_menv) | TCOWrap($_menv; $_orig_retenv; false)) + end + ) ] + | last as $result + | ($result.ret_env // $result.env) as $env + | $result.ast + | addEnv($env); + +def PRINT(env): + pr_str(env); + +def rep(env): + READ | EVAL(env) as $expenv | + if $expenv.expr != null then + $expenv.expr | PRINT($expenv.env) + else + null + end | addEnv($expenv.env); + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + parent: null, + environment: ({ + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + "eval": { + kind: "fn", + inputs: 1, + function: "eval" + } + } + core_identify), + fallback: null + }; + +def repl(env): + def xrepl: + (.env as $env | try repl_($env) catch addEnv($env)) as $expenv | + { + value: $expenv.expr, + stop: false, + env: ($expenv.env // .env) + } | ., xrepl; + {stop: false, env: env} | xrepl | if .value then (.value | _display) else empty end; + +def eval_ign(expr): + . as $env | expr | rep($env) | .env; + +def eval_val(expr): + . as $env | expr | rep($env) | .expr; + +def getEnv: + replEnv + | wrapEnv({}) + | eval_ign("(def! not (fn* (a) (if a false true)))") + | eval_ign("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\\nnil)\")))))))") + | eval_ign("(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)))))))") + ; + +def main: + if $ARGS.positional|length > 0 then + getEnv as $env | + env_set_($env; "*ARGV*"; $ARGS.positional[1:] | map(wrap("string")) | wrap("list")) | + eval_val("(load-file \($ARGS.positional[0] | tojson))") | + "" + else + repl( getEnv as $env | env_set_($env; "*ARGV*"; [] | wrap("list")) ) + end; + +[ main ] | _halt diff --git a/jq/stepA_mal.jq b/jq/stepA_mal.jq new file mode 100644 index 00000000..c39c787a --- /dev/null +++ b/jq/stepA_mal.jq @@ -0,0 +1,404 @@ +include "reader"; +include "printer"; +include "utils"; +include "interp"; +include "env"; +include "core"; + +def read_line: + . as $in + | label $top + | _readline; + +def READ: + read_str | read_form | .value; + +def recurseflip(x; y): + recurse(y; x); + +def TCOWrap(env; retenv; continue): + { + ast: ., + env: env, + ret_env: (if retenv != null then (retenv | setpath(["atoms"]; env.atoms)) else retenv end), + finish: (continue | not), + cont: true # set inside + }; + +def _symbol(name): + { + kind: "symbol", + value: name + }; + +def _symbol_v(name): + if .kind == "symbol" then + .value == name + else + false + end; + +def quasiquote: + if isPair then + .value as $value | null | + if ($value[0] | _symbol_v("unquote")) then + $value[1] + else + if isPair($value[0]) and ($value[0].value[0] | _symbol_v("splice-unquote")) then + [_symbol("concat")] + + [$value[0].value[1]] + + [($value[1:] | wrap("list") | quasiquote)] | wrap("list") + else + [_symbol("cons")] + + [($value[0] | quasiquote)] + + [($value[1:] | wrap("list") | quasiquote)] | wrap("list") + end + end + else + [_symbol("quote")] + + [.] | wrap("list") + end; + +def set_macro_function: + if .kind != "function" then + jqmal_error("expected a function to be defined by defmacro!") + else + .is_macro |= true + end; + +def is_macro_call(env): + if .kind != "list" then + false + else + if (.value|first.kind == "symbol") then + env_req(env; .value|first.value) + | if .kind != "function" then + false + else + .is_macro + end + else + false + end + end; + +def EVAL(env): + def _eval_here: + .env as $env | .expr | EVAL($env); + + def _interpret($_menv): + reduce .value[] as $elem ( + {env: $_menv, val: []}; + . as $dot | $elem | EVAL($dot.env) as $eval_env | + ($dot.env | setpath(["atoms"]; $eval_env.env.atoms)) as $_menv | + {env: $_menv, val: ($dot.val + [$eval_env.expr])} + ) | . as $expr | $expr.val | first | + interpret($expr.val[1:]; $expr.env; _eval_here); + + def macroexpand(env): + . as $dot | + $dot | + [ while(is_macro_call(env | unwrapCurrentEnv); + . as $dot + | ($dot.value[0] | EVAL(env).expr) as $fn + | $dot.value[1:] as $args + | $fn + | interpret($args; env; _eval_here).expr) // . ] + | last + | if is_macro_call(env | unwrapCurrentEnv) then + . as $dot + | ($dot.value[0] | EVAL(env).expr) as $fn + | $dot.value[1:] as $args + | $fn + | interpret($args; env; _eval_here).expr + else + . + end + ; + + def hmap_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem.value.value | EVAL($env) as $resv | + { + value: { + key: $elem.key, + value: { kkind: $elem.value.kkind, value: $resv.expr } + }, + env: env + }, + ({env: $resv.env, list: $rest} | hmap_with_env) + end; + def map_with_env: + .env as $env | .list as $list | + if $list|length == 0 then + empty + else + $list[0] as $elem | + $list[1:] as $rest | + $elem | EVAL($env) as $resv | + { value: $resv.expr, env: env }, + ({env: $resv.env, list: $rest} | map_with_env) + end; + def eval_ast(env): + (select(.kind == "vector") | + if .value|length == 0 then + { + kind: "vector", + value: [] + } + else + [ { env: env, list: .value } | map_with_env ] as $res | + { + kind: "vector", + value: $res | map(.value) + } + end + ) // + (select(.kind == "hashmap") | + [ { env: env, list: (.value | to_entries) } | hmap_with_env ] as $res | + { + kind: "hashmap", + value: $res | map(.value) | from_entries + } + ) // + (select(.kind == "function") | + .# return this unchanged, since it can only be applied to + ) // + (select(.kind == "symbol") | + .value | env_get(env | unwrapCurrentEnv) + ) // .; + + . as $ast + | { env: env, ast: ., cont: true, finish: false, ret_env: null } + | [ recurseflip(.cont; + .env as $_menv + | (if $DEBUG then _debug("EVAL: \($ast | pr_str($_menv))") else . end) + | (if $DEBUG then _debug("ATOMS: \($_menv.atoms)") else . end) + | if .finish then + .cont |= false + else + (.ret_env//.env) as $_retenv + | .ret_env as $_orig_retenv + | .ast + | . as $init + | $_menv | unwrapCurrentEnv as $currentEnv # unwrap env "package" + | $_menv | unwrapReplEnv as $replEnv # - + | $init + | + (select(.kind == "list") | + macroexpand($_menv) | + if .kind != "list" then + eval_ast($_menv) | TCOWrap($_menv; $_orig_retenv; false) + else + if .value | length == 0 then + . | TCOWrap($_menv; $_orig_retenv; false) + else + ( + ( + .value | select(.[0].value == "atoms??") as $value | + $_menv.atoms | keys | map(wrap("string")) | wrap("list") | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "def!") as $value | + ($value[2] | EVAL($_menv)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "defmacro!") as $value | + ($value[2] | EVAL($_menv) | (.expr |= set_macro_function)) as $evval | + addToEnv($evval; $value[1].value) as $val | + $val.expr | TCOWrap($val.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "let*") as $value | + ($currentEnv | pureChildEnv | wrapEnv($replEnv; $_menv.atoms)) as $_menv | + (reduce ($value[1].value | nwise(2)) as $xvalue ( + $_menv; + . as $env | $xvalue[1] | EVAL($env) as $expenv | + env_set_($expenv.env; $xvalue[0].value; $expenv.expr))) as $env + | $value[2] | TCOWrap($env; $_retenv; true) + ) // + ( + .value | select(.[0].value == "do") as $value | + (reduce ($value[1:][]) as $xvalue ( + { env: $_menv, expr: {kind:"nil"} }; + .env as $env | $xvalue | EVAL($env) + )) | . as $ex | .expr | TCOWrap($ex.env; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "try*") as $value | + try ( + $value[1] | EVAL($_menv) as $exp | $exp.expr | TCOWrap($exp.env; $_orig_retenv; false) + ) catch ( . as $exc | + if $value[2] then + if ($value[2].value[0] | _symbol_v("catch*")) then + (if ($exc | is_jqmal_error) then + $exc[19:] as $ex | + try ( + $ex + | fromjson + ) catch ( + $ex | + wrap("string") + ) + else + $exc|wrap("string") + end) as $exc | + $value[2].value[2] | EVAL($currentEnv | childEnv([$value[2].value[1].value]; [$exc]) | wrapEnv($replEnv; $_menv.atoms)) as $ex | + $ex.expr | TCOWrap($ex.env; $_retenv; false) + else + error($exc) + end + else + error($exc) + end + ) + ) // + ( + .value | select(.[0].value == "if") as $value | + $value[1] | EVAL($_menv) as $condenv | + (if (["false", "nil"] | contains([$condenv.expr.kind])) then + ($value[3] // {kind:"nil"}) + else + $value[2] + end) | TCOWrap($condenv.env; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "fn*") as $value | + # (fn* args body) + $value[1].value | map(.value) as $binds | + ($value[2] | find_free_references($currentEnv | env_dump_keys + $binds)) as $free_referencess | { + kind: "function", + binds: $binds, + env: $_menv, + body: $value[2], + names: [], # we can't do that circular reference thing + free_referencess: $free_referencess, # for dynamically scoped variables + is_macro: false + } | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "quote") as $value | + $value[1] | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + .value | select(.[0].value == "quasiquote") as $value | + $value[1] | quasiquote | TCOWrap($_menv; $_orig_retenv; true) + ) // + ( + .value | select(.[0].value == "macroexpand") as $value | + $value[1] | macroexpand($_menv) | TCOWrap($_menv; $_orig_retenv; false) + ) // + ( + . as $dot | _interpret($_menv) as $exprenv | + $exprenv.expr | TCOWrap($exprenv.env; $_orig_retenv; false) + ) // + TCOWrap($_menv; $_orig_retenv; false) + ) + end + end + ) // + (eval_ast($_menv) | TCOWrap($_menv; $_orig_retenv; false)) + end + | (if $DEBUG then _debug("POSTEVAL: \($ast | pr_str($_menv)) = \(.ast | pr_str($_menv))") else . end) + ) ] + | last as $result + | ($result.ret_env // $result.env) as $env + | $result.ast + | addEnv($env); + +def PRINT(env): + pr_str(env); + +def rep(env): + READ | EVAL(env) as $expenv | + if $expenv.expr != null then + $expenv.expr | PRINT($expenv.env) + else + null + end | addEnv($expenv.env); + +def repl_(env): + ("user> " | _print) | + (read_line | rep(env)); + +# we don't have no indirect functions, so we'll have to interpret the old way +def replEnv: + { + parent: null, + environment: ({ + "+": { + kind: "fn", # native function + inputs: 2, + function: "number_add" + }, + "-": { + kind: "fn", # native function + inputs: 2, + function: "number_sub" + }, + "*": { + kind: "fn", # native function + inputs: 2, + function: "number_mul" + }, + "/": { + kind: "fn", # native function + inputs: 2, + function: "number_div" + }, + "eval": { + kind: "fn", + inputs: 1, + function: "eval" + } + } + core_identify), + fallback: null + }; + +def repl(env): + def xrepl: + (.env as $env | try repl_($env) catch addEnv($env)) as $expenv | + { + value: $expenv.expr, + stop: false, + env: ($expenv.env // .env) + } | ., xrepl; + {stop: false, env: env} | xrepl | if .value then (.value | _display) else empty end; + +def eval_ign(expr): + . as $env | expr | rep($env) | .env; + +def eval_val(expr): + . as $env | expr | rep($env) | .expr; + +def getEnv: + replEnv + | wrapEnv({}) + | eval_ign("(def! *host-language* \"jq\")") + | eval_ign("(def! not (fn* (a) (if a false true)))") + | eval_ign("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\\nnil)\")))))))") + | eval_ign("(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)))))))") + ; + +def main: + if $ARGS.positional|length > 0 then + try ( + getEnv as $env | + env_set_($env; "*ARGV*"; $ARGS.positional[1:] | map(wrap("string")) | wrap("list")) | + eval_val("(load-file \($ARGS.positional[0] | tojson))") | + "" + ) catch ( + _print + ) + else + repl( getEnv as $env | env_set_($env; "*ARGV*"; [] | wrap("list")) ) + end; + +[ main ] | _halt diff --git a/jq/utils.jq b/jq/utils.jq new file mode 100644 index 00000000..c55a07e9 --- /dev/null +++ b/jq/utils.jq @@ -0,0 +1,160 @@ +def _debug(ex): + . as $top + | ex + | debug + | $top; + +def _print: + tostring; + +def nwise(n): + def _nwise: + if length <= n then + . + else + .[0:n], (.[n:] | _nwise) + end; + _nwise; + +def abs(x): + if x < 0 then 0 - x else x end; + +def jqmal_error(e): + error("JqMAL Exception :: " + e); + +def is_jqmal_error: + startswith("JqMAL Exception :: "); + +def wrap(kind): + { + kind: kind, + value: . + }; + +def wrap2(kind; opts): + opts + { + kind: kind, + value: . + }; + +def isPair: + if (.kind == "list" or .kind == "vector") then + .value | length > 0 + else + false + end; + +def isPair(x): + x | isPair; + +def find_free_references(keys): + def _refs: + if . == null then [] else + . as $dot + | if .kind == "symbol" then + if keys | contains([$dot.value]) then [] else [$dot.value] end + else if "list" == $dot.kind then + # if - scan args + # def! - scan body + # let* - add keys sequentially, scan body + # fn* - add keys, scan body + # quote - [] + # quasiquote - ??? + $dot.value[0] as $head + | if $head.kind == "symbol" then + ( + select($head.value == "if") | $dot.value[1:] | map(_refs) | reduce .[] as $x ([]; . + $x) + ) // ( + select($head.value == "def!") | $dot.value[2] | _refs + ) // ( + select($head.value == "let*") | $dot.value[2] | find_free_references(($dot.value[1].value as $value | ([ range(0; $value|length; 2) ] | map(select(. % 2 == 0) | $value[.].value))) + keys) + ) // ( + select($head.value == "fn*") | $dot.value[2] | find_free_references(($dot.value[1].value | map(.value)) + keys) + ) // ( + select($head.value == "quote") | [] + ) // ( + select($head.value == "quasiquote") | [] + ) // ($dot.value | map(_refs) | reduce .[] as $x ([]; . + $x)) + else + [ $dot.values[1:][] | _refs ] + end + else if "vector" == $dot.kind then + ($dot.value | map(_refs) | reduce .[] as $x ([]; . + $x)) + else if "hashmap" == $dot.kind then + ([$dot.value | to_entries[] | ({kind: .value.kkind, value: .key}, .value.value) ] | map(_refs) | reduce .[] as $x ([]; . + $x)) + else + [] + end end end end + end; + _refs | unique; + +def tomal: + ( + select(type == "array") | ( + map(tomal) | wrap("list") + ) + ) // ( + select(type == "string") | ( + if startswith("sym/") then + .[4:] | wrap("symbol") + else + wrap("string") + end + ) + ) // ( + select(type == "number") | ( + wrap("number") + ) + ); + +def _extern(options): + {command: .} + | debug + | if (options.nowait | not) then + input | fromjson + else + null + end; + +def issue_extern(cmd; options): + {cmd: cmd, args: .} + | _extern(options); + +def issue_extern(cmd): + issue_extern(cmd; {}); + +def _readline: + [.] + | issue_extern("readline"; {nowait: false}) + ; + +def __readline(prompt): + . as $top + | prompt + | _readline; + +def __readline: + __readline(.); + +def _display: + tostring | .+"\n" | debug; + +def _write_to_file(name): + . as $value + | [(name|tojson), (.|tojson), (false|tojson)] + | issue_extern("fwrite"; {nowait: true}) + | $value; + +def _append_to_file(name): + . as $value + | [(name|tojson), (.|tojson), (true|tojson)] + | issue_extern("fwrite"; {nowait: true}) + | $value; + +def _halt: + [] + | issue_extern("halt"; {nowait: true}) + | halt; + +def trap: + _write_to_file("trap_reason.json") | jqmal_error("trap");