From dbda7a9ba915528a0d9c25a310a3263d70b45053 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Tue, 29 Dec 2020 11:21:43 +0200 Subject: [PATCH 01/20] chore: add deno into the setup --- Makefile | 3 ++- impls/deno/run | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100755 impls/deno/run diff --git a/Makefile b/Makefile index c42c5ca62f..27651dafac 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,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 \ + deno elisp elixir elm erlang es6 factor fantom forth fsharp go groovy gnu-smalltalk \ 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 \ @@ -201,6 +201,7 @@ crystal_STEP_TO_PROG = impls/crystal/$($(1)) cs_STEP_TO_PROG = impls/cs/$($(1)).exe d_STEP_TO_PROG = impls/d/$($(1)) dart_STEP_TO_PROG = impls/dart/$($(1)).dart +deno_STEP_TO_PROG = impls/deno/$($(1)).ts elisp_STEP_TO_PROG = impls/elisp/$($(1)).el elixir_STEP_TO_PROG = impls/elixir/lib/mix/tasks/$($(1)).ex elm_STEP_TO_PROG = impls/elm/$($(1)).js diff --git a/impls/deno/run b/impls/deno/run new file mode 100755 index 0000000000..575e70f93a --- /dev/null +++ b/impls/deno/run @@ -0,0 +1,2 @@ +#!/bin/bash +exec deno run --allow-all $(dirname $0)/${STEP:-stepA_mal}.ts "${@}" From d92cbb1533fed863e6166d51506bdcaa178e1e1e Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Tue, 29 Dec 2020 11:22:03 +0200 Subject: [PATCH 02/20] feature: step 0 - the REPL --- impls/deno/step0_repl.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 impls/deno/step0_repl.ts diff --git a/impls/deno/step0_repl.ts b/impls/deno/step0_repl.ts new file mode 100644 index 0000000000..ad4fa1dcf7 --- /dev/null +++ b/impls/deno/step0_repl.ts @@ -0,0 +1,17 @@ +const read = (str: string): any => str; + +const eval_ = (ast: any): any => ast; + +const print = (exp: any): string => exp.toString(); + +const rep = (str: string): string => print(eval_(read(str))); + +while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + console.log(rep(value)); +} From ca20b4a6dfc437275f1a0c538a0d4076afa13865 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Wed, 30 Dec 2020 07:48:26 +0200 Subject: [PATCH 03/20] feature: step 2 read and print --- impls/deno/printer.ts | 44 ++++++++ impls/deno/reader.ts | 184 +++++++++++++++++++++++++++++++++ impls/deno/step1_read_print.ts | 25 +++++ impls/deno/types.ts | 70 +++++++++++++ 4 files changed, 323 insertions(+) create mode 100644 impls/deno/printer.ts create mode 100644 impls/deno/reader.ts create mode 100644 impls/deno/step1_read_print.ts create mode 100644 impls/deno/types.ts diff --git a/impls/deno/printer.ts b/impls/deno/printer.ts new file mode 100644 index 0000000000..60f4fdfc88 --- /dev/null +++ b/impls/deno/printer.ts @@ -0,0 +1,44 @@ +import { MalType } from "./types.ts"; + +export const prStr = ( + v: MalType, + printReabably: boolean = true, +): string => { + const prStrReadably = (v: MalType): string => { + switch (v.tag) { + case "MalList": + return `(${v.items.map(prStrReadably).join(" ")})`; + case "MalVector": + return `[${v.items.map(prStrReadably).join(" ")}]`; + case "MalMap": + return `{${ + [...v.items].map(([k, v]) => + `${ + k.startsWith("s") + ? prStrReadably({ tag: "MalString", value: k.substr(1) }) + : prStrReadably({ tag: "MalSymbol", name: k.substr(1) }) + } ${prStrReadably(v)}` + ).join(" ") + }}`; + case "MalNil": + return "nil"; + case "MalString": + return printReabably + ? `"${ + v.value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll( + "\n", + "\\n", + ) + }"` + : v.value; + case "MalBoolean": + case "MalNumber": + case "MalAtom": + return `${v.value}`; + case "MalSymbol": + return `${v.name}`; + } + }; + + return prStrReadably(v); +}; diff --git a/impls/deno/reader.ts b/impls/deno/reader.ts new file mode 100644 index 0000000000..65a6cbb14c --- /dev/null +++ b/impls/deno/reader.ts @@ -0,0 +1,184 @@ +import { + MalAtom, + MalList, + MalNumber, + MalType, + MalVector, + mkMalMap, +} from "./types.ts"; + +interface Reader { + position: number; + peek: () => string; + next: () => string; +} + +const newReader = (tokens: Array): Reader => ({ + position: 0, + + peek: function (): string { + return tokens[this.position]; + }, + + next: function (): string { + const current = this.peek(); + this.position += 1; + return current; + }, +}); + +export const readStr = (input: string): MalType => + readForm(newReader(tokenize(input))); + +const tokenize = (input: string): Array => { + const tokenPattern = + /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)/; + + const regex = new RegExp(tokenPattern, "g"); + + const tokens = []; + while (true) { + const tokenResults = regex.exec(input); + if (tokenResults === null || tokenResults[0] === "") { + break; + } else if (tokenResults[1] !== ";") { + tokens.push(tokenResults[1]); + } + } + + return tokens; +}; + +const readForm = (reader: Reader): MalType => { + const token = reader.peek(); + + switch (token) { + case "(": + return readList(reader); + case "[": + return readVector(reader); + case "{": + return readMap(reader); + case "'": + return readSymbol(reader, "quote"); + case "`": + return readSymbol(reader, "quasiquote"); + case "~": + return readSymbol(reader, "unquote"); + case "~@": + return readSymbol(reader, "splice-unquote"); + case "@": + return readSymbol(reader, "deref"); + case "^": + return readMetaData(reader); + default: + return readAtom(reader); + } +}; + +const readList = (reader: Reader): MalType => + readCollection( + reader, + ")", + (items: Array): MalType => ({ tag: "MalList", items }), + ); + +const readVector = (reader: Reader): MalType => + readCollection( + reader, + "]", + (items: Array): MalType => ({ tag: "MalVector", items }), + ); + +const readMap = ( + reader: Reader, +): MalType => { + const buildMap = (items: Array): MalType => { + const args: Array<[MalType, MalType]> = []; + + if (items.length % 2 === 1) { + throw new Error(`Syntax Error: Odd number of map arguments`); + } + for (let lp = 0; lp < items.length; lp += 2) { + args.push([items[lp], items[lp + 1]]); + } + + return mkMalMap(args); + }; + + return readCollection(reader, "}", buildMap); +}; + +const readCollection = ( + reader: Reader, + close: string, + mk: (a: Array) => MalType, +): MalType => { + reader.next(); + + const items: Array = []; + + while (true) { + const token = reader.peek(); + + if (token === undefined) { + throw new Error(`Syntax Error: EOF whilst expecting '${close}'`); + } else if (token === close) { + reader.next(); + return mk(items); + } else { + items.push(readForm(reader)); + } + } +}; + +const readSymbol = (reader: Reader, name: string): MalType => { + reader.next(); + + return { + tag: "MalList", + items: [{ tag: "MalSymbol", name }, readForm(reader)], + }; +}; + +const readMetaData = (reader: Reader): MalType => { + reader.next(); + + const v1 = readForm(reader); + const v2 = readForm(reader); + + return { + tag: "MalList", + items: [{ tag: "MalSymbol", name: "with-meta" }, v2, v1], + }; +}; + +const readAtom = (reader: Reader): MalType => { + const token = reader.next(); + + if (token === undefined) { + throw new Error(`Syntax Error: Unexpected EOF`); + } else if (token.match(/^[0-9]+$/)) { + return { tag: "MalNumber", value: parseInt(token) }; + } else if (token == "false") { + return { tag: "MalBoolean", value: false }; + } else if (token == "true") { + return { tag: "MalBoolean", value: true }; + } else if (token == "nil") { + return { tag: "MalNil" }; + } else if (token[0] === '"') { + if (token.match(/^"(?:\\.|[^\\"])*"$/)) { + return { + tag: "MalString", + value: token.substr(1, token.length - 2).replaceAll("\\\\", "\\") + .replaceAll('\\"', '"').replaceAll("\\n", "\n"), + }; + } else { + throw new Error(`Syntax Error: EOF whilst expecting '"': ${token}`); + } + } else if (token[0] === ":") { + return { tag: "MalSymbol", name: token }; + } else { + return { tag: "MalAtom", value: token }; + } +}; diff --git a/impls/deno/step1_read_print.ts b/impls/deno/step1_read_print.ts new file mode 100644 index 0000000000..05c96570c0 --- /dev/null +++ b/impls/deno/step1_read_print.ts @@ -0,0 +1,25 @@ +import * as Reader from "./reader.ts"; +import * as Printer from "./printer.ts"; +import { MalType } from "./types.ts"; + +const read = (str: string): MalType => Reader.readStr(str); + +const eval_ = (ast: MalType): MalType => ast; + +const print = (exp: MalType): string => Printer.prStr(exp); + +const rep = (str: string): string => print(eval_(read(str))); + +while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value)); + } catch (e) { + console.error(e.message); + } +} diff --git a/impls/deno/types.ts b/impls/deno/types.ts new file mode 100644 index 0000000000..32e241e72e --- /dev/null +++ b/impls/deno/types.ts @@ -0,0 +1,70 @@ +export type MalType = + | MalList + | MalVector + | MalMap + | MalAtom + | MalNumber + | MalString + | MalBoolean + | MalNil + | MalSymbol; + +export type MalList = { + tag: "MalList"; + items: Array; +}; + +export type MalVector = { + tag: "MalVector"; + items: Array; +}; + +export type MalMap = { + tag: "MalMap"; + items: Map; +}; + +export const mkMalMap = (values: Array<[MalType, MalType]>): MalMap => { + const items: Array<[string, MalType]> = []; + + values.forEach(([k, v]) => { + if (k.tag === "MalString") { + items.push([`s${k.value}`, v]); + } else if (k.tag === "MalSymbol") { + items.push([`t${k.name}`, v]); + } else { + throw new Error(`Precondition Error: Unable to use ${JSON.stringify(k)} as a map key.`); + } + }); + + return { tag: "MalMap", items: new Map(items) }; +}; + +export type MalAtom = { + tag: "MalAtom"; + value: string; +}; + +export type MalNumber = { + tag: "MalNumber"; + value: number; +}; + +export type MalString = { + tag: "MalString"; + value: string; +}; + +export type MalBoolean = { + tag: "MalBoolean"; + value: boolean; +}; + +export type MalNil = { + tag: "MalNil"; +}; + +export type MalSymbol = { + tag: "MalSymbol"; + name: string; +}; From 679bc226295d1367aecc2e531f3f31906ccbae60 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Wed, 30 Dec 2020 07:52:37 +0200 Subject: [PATCH 04/20] feature: report error if closing brace is received --- impls/deno/reader.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/impls/deno/reader.ts b/impls/deno/reader.ts index 65a6cbb14c..bc9ac00d65 100644 --- a/impls/deno/reader.ts +++ b/impls/deno/reader.ts @@ -71,6 +71,10 @@ const readForm = (reader: Reader): MalType => { return readSymbol(reader, "deref"); case "^": return readMetaData(reader); + case ")": + case "]": + case "}": + throw new Error(`Syntax Error: Unexpected '${token}'`); default: return readAtom(reader); } From 3da692b600fb42e84d5015514968ade749883a27 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Wed, 30 Dec 2020 08:02:04 +0200 Subject: [PATCH 05/20] refactor: tidy up hash map --- impls/deno/printer.ts | 12 ++++-------- impls/deno/reader.ts | 4 ++-- impls/deno/types.ts | 21 +++++++++++++++------ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/impls/deno/printer.ts b/impls/deno/printer.ts index 60f4fdfc88..956e2230a5 100644 --- a/impls/deno/printer.ts +++ b/impls/deno/printer.ts @@ -1,4 +1,4 @@ -import { MalType } from "./types.ts"; +import { MalType, mapValues } from "./types.ts"; export const prStr = ( v: MalType, @@ -10,14 +10,10 @@ export const prStr = ( return `(${v.items.map(prStrReadably).join(" ")})`; case "MalVector": return `[${v.items.map(prStrReadably).join(" ")}]`; - case "MalMap": + case "MalHashMap": return `{${ - [...v.items].map(([k, v]) => - `${ - k.startsWith("s") - ? prStrReadably({ tag: "MalString", value: k.substr(1) }) - : prStrReadably({ tag: "MalSymbol", name: k.substr(1) }) - } ${prStrReadably(v)}` + mapValues(v).map(([k, v]) => + `${prStrReadably(k)} ${prStrReadably(v)}` ).join(" ") }}`; case "MalNil": diff --git a/impls/deno/reader.ts b/impls/deno/reader.ts index bc9ac00d65..54f79ee0a3 100644 --- a/impls/deno/reader.ts +++ b/impls/deno/reader.ts @@ -58,7 +58,7 @@ const readForm = (reader: Reader): MalType => { case "[": return readVector(reader); case "{": - return readMap(reader); + return readHashMap(reader); case "'": return readSymbol(reader, "quote"); case "`": @@ -94,7 +94,7 @@ const readVector = (reader: Reader): MalType => (items: Array): MalType => ({ tag: "MalVector", items }), ); -const readMap = ( +const readHashMap = ( reader: Reader, ): MalType => { const buildMap = (items: Array): MalType => { diff --git a/impls/deno/types.ts b/impls/deno/types.ts index 32e241e72e..ef705250c2 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -1,7 +1,7 @@ export type MalType = | MalList | MalVector - | MalMap + | MalHashMap | MalAtom | MalNumber | MalString @@ -19,12 +19,12 @@ export type MalVector = { items: Array; }; -export type MalMap = { - tag: "MalMap"; +export type MalHashMap = { + tag: "MalHashMap"; items: Map; }; -export const mkMalMap = (values: Array<[MalType, MalType]>): MalMap => { +export const mkMalMap = (values: Array<[MalType, MalType]>): MalHashMap => { const items: Array<[string, MalType]> = []; values.forEach(([k, v]) => { @@ -33,13 +33,22 @@ export const mkMalMap = (values: Array<[MalType, MalType]>): MalMap => { } else if (k.tag === "MalSymbol") { items.push([`t${k.name}`, v]); } else { - throw new Error(`Precondition Error: Unable to use ${JSON.stringify(k)} as a map key.`); + throw new Error( + `Precondition Error: Unable to use ${JSON.stringify(k)} as a hashmap key.`, + ); } }); - return { tag: "MalMap", items: new Map(items) }; + return { tag: "MalHashMap", items: new Map(items) }; }; +export const mapValues = (malMap: MalHashMap): Array<[MalType, MalType]> => + [...malMap.items].map(([k, v]) => + k.startsWith("s") + ? [{ tag: "MalString", value: k.substr(1) }, v] + : [{ tag: "MalSymbol", name: k.substr(1) }, v] + ); + export type MalAtom = { tag: "MalAtom"; value: string; From 38a8ad15b302158fcc2c994f612cc39ff8569344 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Wed, 30 Dec 2020 14:08:09 +0200 Subject: [PATCH 06/20] feature: step 2 eval --- impls/deno/env.ts | 6 +++ impls/deno/printer.ts | 2 + impls/deno/reader.ts | 66 ++++++++++--------------- impls/deno/step2_eval.ts | 104 +++++++++++++++++++++++++++++++++++++++ impls/deno/types.ts | 58 ++++++++++++++++++++-- 5 files changed, 194 insertions(+), 42 deletions(-) create mode 100644 impls/deno/env.ts create mode 100644 impls/deno/step2_eval.ts diff --git a/impls/deno/env.ts b/impls/deno/env.ts new file mode 100644 index 0000000000..b10dbf8c13 --- /dev/null +++ b/impls/deno/env.ts @@ -0,0 +1,6 @@ +import * as MalType from "./types.ts"; + +export type Env = Map; + +export const lookup = (name: string, env: Env): MalType.MalType | undefined => + env.get(name); diff --git a/impls/deno/printer.ts b/impls/deno/printer.ts index 956e2230a5..5079f5e98a 100644 --- a/impls/deno/printer.ts +++ b/impls/deno/printer.ts @@ -33,6 +33,8 @@ export const prStr = ( return `${v.value}`; case "MalSymbol": return `${v.name}`; + case "MalFunction": + return "#"; } }; diff --git a/impls/deno/reader.ts b/impls/deno/reader.ts index 54f79ee0a3..10d4c0acc9 100644 --- a/impls/deno/reader.ts +++ b/impls/deno/reader.ts @@ -1,10 +1,15 @@ import { - MalAtom, - MalList, MalNumber, MalType, MalVector, - mkMalMap, + mkBoolean, + mkHashMap, + mkList, + mkNumber, + mkString, + mkSymbol, + mkVector, + nil, } from "./types.ts"; interface Reader { @@ -31,10 +36,8 @@ export const readStr = (input: string): MalType => readForm(newReader(tokenize(input))); const tokenize = (input: string): Array => { - const tokenPattern = - /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)/; - - const regex = new RegExp(tokenPattern, "g"); + const regex = + /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)/g; const tokens = []; while (true) { @@ -81,18 +84,10 @@ const readForm = (reader: Reader): MalType => { }; const readList = (reader: Reader): MalType => - readCollection( - reader, - ")", - (items: Array): MalType => ({ tag: "MalList", items }), - ); + readCollection(reader, ")", mkList); const readVector = (reader: Reader): MalType => - readCollection( - reader, - "]", - (items: Array): MalType => ({ tag: "MalVector", items }), - ); + readCollection(reader, "]", mkVector); const readHashMap = ( reader: Reader, @@ -107,7 +102,7 @@ const readHashMap = ( args.push([items[lp], items[lp + 1]]); } - return mkMalMap(args); + return mkHashMap(args); }; return readCollection(reader, "}", buildMap); @@ -139,10 +134,7 @@ const readCollection = ( const readSymbol = (reader: Reader, name: string): MalType => { reader.next(); - return { - tag: "MalList", - items: [{ tag: "MalSymbol", name }, readForm(reader)], - }; + return mkList([mkSymbol(name), readForm(reader)]); }; const readMetaData = (reader: Reader): MalType => { @@ -151,10 +143,7 @@ const readMetaData = (reader: Reader): MalType => { const v1 = readForm(reader); const v2 = readForm(reader); - return { - tag: "MalList", - items: [{ tag: "MalSymbol", name: "with-meta" }, v2, v1], - }; + return mkList([mkSymbol("with-meta"), v2, v1]); }; const readAtom = (reader: Reader): MalType => { @@ -162,27 +151,26 @@ const readAtom = (reader: Reader): MalType => { if (token === undefined) { throw new Error(`Syntax Error: Unexpected EOF`); - } else if (token.match(/^[0-9]+$/)) { - return { tag: "MalNumber", value: parseInt(token) }; + } else if (token.match(/^-?[0-9]+$/)) { + return mkNumber(parseInt(token)); } else if (token == "false") { - return { tag: "MalBoolean", value: false }; + return mkBoolean(false); } else if (token == "true") { - return { tag: "MalBoolean", value: true }; + return mkBoolean(true); } else if (token == "nil") { - return { tag: "MalNil" }; + return nil; } else if (token[0] === '"') { if (token.match(/^"(?:\\.|[^\\"])*"$/)) { - return { - tag: "MalString", - value: token.substr(1, token.length - 2).replaceAll("\\\\", "\\") - .replaceAll('\\"', '"').replaceAll("\\n", "\n"), - }; + return mkString( + token.substr(1, token.length - 2).replaceAll("\\\\", "\\").replaceAll( + '\\"', + '"', + ).replaceAll("\\n", "\n"), + ); } else { throw new Error(`Syntax Error: EOF whilst expecting '"': ${token}`); } - } else if (token[0] === ":") { - return { tag: "MalSymbol", name: token }; } else { - return { tag: "MalAtom", value: token }; + return mkSymbol(token); } }; diff --git a/impls/deno/step2_eval.ts b/impls/deno/step2_eval.ts new file mode 100644 index 0000000000..84f83c6ad4 --- /dev/null +++ b/impls/deno/step2_eval.ts @@ -0,0 +1,104 @@ +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; + +const initialEnv: Env.Env = new Map([ + [ + "+", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) + ), + ], + [ + "-", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) + ), + ], + [ + "*", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) + ), + ], + [ + "/", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) + ), + ], +]); + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + if (ast.tag === "MalSymbol") { + const binding = Env.lookup(ast.name, env); + + if (binding === undefined) { + throw new Error(`Unknown Symbol: ${ast.name}`); + } else { + return binding; + } + } else if (ast.tag === "MalList") { + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalVector") { + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalHashMap") { + return MalType.mkHashMap( + MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + } else { + return ast; + } +}; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } else { + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalFunction") { + return callerItem.f(callerArgs); + } else if (callerItem.tag === "MalSymbol") { + const binding = Env.lookup(callerItem.name, env); + + if (binding !== undefined && binding.tag === "MalFunction") { + return binding.f(callerArgs); + } + } + } + } + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); + } + } else { + return evaluate_ast(ast, env); + } +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string): string => print(evaluate(read(str), initialEnv)); + +type FunctionType = (a: MalType.MalType, b: MalType.MalType) => MalType.MalType; + +while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value)); + } catch (e) { + console.error(e.message); + } +} diff --git a/impls/deno/types.ts b/impls/deno/types.ts index ef705250c2..f8bd53202e 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -7,24 +7,35 @@ export type MalType = | MalString | MalBoolean | MalNil - | MalSymbol; + | MalSymbol + | MalFunction; export type MalList = { tag: "MalList"; items: Array; }; +export const mkList = (items: Array): MalList => ({ + tag: "MalList", + items, +}); + export type MalVector = { tag: "MalVector"; items: Array; }; +export const mkVector = (items: Array): MalVector => ({ + tag: "MalVector", + items, +}); + export type MalHashMap = { tag: "MalHashMap"; items: Map; }; -export const mkMalMap = (values: Array<[MalType, MalType]>): MalHashMap => { +export const mkHashMap = (values: Array<[MalType, MalType]>): MalHashMap => { const items: Array<[string, MalType]> = []; values.forEach(([k, v]) => { @@ -34,7 +45,9 @@ export const mkMalMap = (values: Array<[MalType, MalType]>): MalHashMap => { items.push([`t${k.name}`, v]); } else { throw new Error( - `Precondition Error: Unable to use ${JSON.stringify(k)} as a hashmap key.`, + `Precondition Error: Unable to use ${ + JSON.stringify(k) + } as a hashmap key.`, ); } }); @@ -59,21 +72,60 @@ export type MalNumber = { value: number; }; +export const mkNumber = (value: number): MalNumber => ({ + tag: "MalNumber", + value, +}); + +export const asNumber = (v: MalType): number => { + if (v.tag === "MalNumber") { + return v.value; + } else { + throw new Error(`Precondition Error: ${JSON.stringify(v)} is not a number`); + } +}; + export type MalString = { tag: "MalString"; value: string; }; +export const mkString = (value: string): MalString => ({ + tag: "MalString", + value, +}); + export type MalBoolean = { tag: "MalBoolean"; value: boolean; }; +export const mkBoolean = (value: boolean): MalBoolean => ({ + tag: "MalBoolean", + value, +}); + export type MalNil = { tag: "MalNil"; }; +export const nil: MalNil = ({ tag: "MalNil" }); + export type MalSymbol = { tag: "MalSymbol"; name: string; }; + +export const mkSymbol = (name: string): MalSymbol => ({ + tag: "MalSymbol", + name, +}); + +export type MalFunction = { + tag: "MalFunction"; + f: (args: Array) => MalType; +}; + +export const mkFunction = ( + f: (args: Array) => MalType, +): MalFunction => ({ tag: "MalFunction", f }); From ffa79f0ef8e4928df797f9652809dcc39d3bfa4e Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Wed, 30 Dec 2020 14:25:34 +0200 Subject: [PATCH 07/20] feature: introduce a keyword --- impls/deno/printer.ts | 2 +- impls/deno/reader.ts | 3 +++ impls/deno/types.ts | 23 +++++++++++++---------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/impls/deno/printer.ts b/impls/deno/printer.ts index 5079f5e98a..1db5f3ece2 100644 --- a/impls/deno/printer.ts +++ b/impls/deno/printer.ts @@ -29,8 +29,8 @@ export const prStr = ( : v.value; case "MalBoolean": case "MalNumber": - case "MalAtom": return `${v.value}`; + case "MalKeyword": case "MalSymbol": return `${v.name}`; case "MalFunction": diff --git a/impls/deno/reader.ts b/impls/deno/reader.ts index 10d4c0acc9..ee7ab3e446 100644 --- a/impls/deno/reader.ts +++ b/impls/deno/reader.ts @@ -4,6 +4,7 @@ import { MalVector, mkBoolean, mkHashMap, + mkKeyword, mkList, mkNumber, mkString, @@ -170,6 +171,8 @@ const readAtom = (reader: Reader): MalType => { } else { throw new Error(`Syntax Error: EOF whilst expecting '"': ${token}`); } + } else if (token[0] === ":") { + return mkKeyword(token); } else { return mkSymbol(token); } diff --git a/impls/deno/types.ts b/impls/deno/types.ts index f8bd53202e..5dfda2c570 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -2,12 +2,12 @@ export type MalType = | MalList | MalVector | MalHashMap - | MalAtom | MalNumber | MalString | MalBoolean | MalNil | MalSymbol + | MalKeyword | MalFunction; export type MalList = { @@ -41,7 +41,7 @@ export const mkHashMap = (values: Array<[MalType, MalType]>): MalHashMap => { values.forEach(([k, v]) => { if (k.tag === "MalString") { items.push([`s${k.value}`, v]); - } else if (k.tag === "MalSymbol") { + } else if (k.tag === "MalKeyword") { items.push([`t${k.name}`, v]); } else { throw new Error( @@ -57,16 +57,9 @@ export const mkHashMap = (values: Array<[MalType, MalType]>): MalHashMap => { export const mapValues = (malMap: MalHashMap): Array<[MalType, MalType]> => [...malMap.items].map(([k, v]) => - k.startsWith("s") - ? [{ tag: "MalString", value: k.substr(1) }, v] - : [{ tag: "MalSymbol", name: k.substr(1) }, v] + k.startsWith("s") ? [mkString(k.substr(1)), v] : [mkKeyword(k.substr(1)), v] ); -export type MalAtom = { - tag: "MalAtom"; - value: string; -}; - export type MalNumber = { tag: "MalNumber"; value: number; @@ -121,6 +114,16 @@ export const mkSymbol = (name: string): MalSymbol => ({ name, }); +export type MalKeyword = { + tag: "MalKeyword"; + name: string; +}; + +export const mkKeyword = (name: string): MalKeyword => ({ + tag: "MalKeyword", + name, +}); + export type MalFunction = { tag: "MalFunction"; f: (args: Array) => MalType; From 46d997e994f06978ae47fde3ce14183700d28554 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Wed, 30 Dec 2020 15:44:03 +0200 Subject: [PATCH 08/20] feature: step 3 environments --- impls/deno/env.ts | 37 +++++++++- impls/deno/step2_eval.ts | 65 ++++++++++-------- impls/deno/step3_env.ts | 144 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 impls/deno/step3_env.ts diff --git a/impls/deno/env.ts b/impls/deno/env.ts index b10dbf8c13..345f0b45dd 100644 --- a/impls/deno/env.ts +++ b/impls/deno/env.ts @@ -1,6 +1,37 @@ import * as MalType from "./types.ts"; -export type Env = Map; +export type Scope = Map; -export const lookup = (name: string, env: Env): MalType.MalType | undefined => - env.get(name); +export type Env = { + outer: Env | undefined; + data: Scope; +}; + +export const mkEnv = (outer: Env | undefined = undefined): Env => ({ + outer, + data: new Map(), +}); + +export const find = (name: string, env: Env): MalType.MalType | undefined => { + const result = env.data.get(name); + + if (result === undefined) { + return env.outer === undefined ? undefined : find(name, env.outer); + } else { + return result; + } +}; + +export const get = (name: string, env: Env): MalType.MalType => { + const result = find(name, env); + + if (result === undefined) { + throw new Error(`Undefined Symbol: ${name} not found`); + } else { + return result; + } +}; + +export const set = (name: string, value: MalType.MalType, env: Env): void => { + env.data.set(name, value); +}; diff --git a/impls/deno/step2_eval.ts b/impls/deno/step2_eval.ts index 84f83c6ad4..3bb83cab7b 100644 --- a/impls/deno/step2_eval.ts +++ b/impls/deno/step2_eval.ts @@ -3,38 +3,45 @@ import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; -const initialEnv: Env.Env = new Map([ - [ - "+", - MalType.mkFunction(([a, b]) => - MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) - ), - ], - [ - "-", - MalType.mkFunction(([a, b]) => - MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) - ), - ], - [ - "*", - MalType.mkFunction(([a, b]) => - MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) - ), - ], - [ - "/", - MalType.mkFunction(([a, b]) => - MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) - ), - ], -]); +const replEnv = Env.mkEnv(); + +Env.set( + "+", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) + ), + replEnv, +); + +Env.set( + "-", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) + ), + replEnv, +); + +Env.set( + "*", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) + ), + replEnv, +); + +Env.set( + "/", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) + ), + replEnv, +); const read = (str: string): MalType.MalType => Reader.readStr(str); const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { if (ast.tag === "MalSymbol") { - const binding = Env.lookup(ast.name, env); + const binding = Env.find(ast.name, env); if (binding === undefined) { throw new Error(`Unknown Symbol: ${ast.name}`); @@ -68,7 +75,7 @@ const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { if (callerItem.tag === "MalFunction") { return callerItem.f(callerArgs); } else if (callerItem.tag === "MalSymbol") { - const binding = Env.lookup(callerItem.name, env); + const binding = Env.find(callerItem.name, env); if (binding !== undefined && binding.tag === "MalFunction") { return binding.f(callerArgs); @@ -85,7 +92,7 @@ const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { const print = (exp: MalType.MalType): string => Printer.prStr(exp); -const rep = (str: string): string => print(evaluate(read(str), initialEnv)); +const rep = (str: string): string => print(evaluate(read(str), replEnv)); type FunctionType = (a: MalType.MalType, b: MalType.MalType) => MalType.MalType; diff --git a/impls/deno/step3_env.ts b/impls/deno/step3_env.ts new file mode 100644 index 0000000000..7fda7fea83 --- /dev/null +++ b/impls/deno/step3_env.ts @@ -0,0 +1,144 @@ +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; + +const replEnv = Env.mkEnv(); + +Env.set( + "+", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) + ), + replEnv, +); + +Env.set( + "-", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) + ), + replEnv, +); + +Env.set( + "*", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) + ), + replEnv, +); + +Env.set( + "/", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) + ), + replEnv, +); + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + if (ast.tag === "MalSymbol") { + return Env.get(ast.name, env); + } else if (ast.tag === "MalList") { + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalVector") { + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalHashMap") { + return MalType.mkHashMap( + MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + } else { + return ast; + } +}; + +const isNamedSymbol = (ast: MalType.MalType, name: string): boolean => + ast.tag === "MalSymbol" && ast.name === name; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } else if (isNamedSymbol(ast.items[0], "def!")) { + if (ast.items[1].tag === "MalSymbol") { + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1].name, result, env); + return result; + } else { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + } else if (isNamedSymbol(ast.items[0], "let*")) { + const innerEnv = Env.mkEnv(env); + + const bindings = ast.items[1]; + if (bindings.tag === "MalList" || bindings.tag === "MalVector") { + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag === "MalSymbol") { + Env.set(name.name, evaluate(value, innerEnv), innerEnv); + } else { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + } + + return evaluate(ast.items[2], innerEnv); + } else { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + } else { + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalFunction") { + return callerItem.f(callerArgs); + } else if (callerItem.tag === "MalSymbol") { + const binding = Env.get(callerItem.name, env); + + if (binding.tag === "MalFunction") { + return binding.f(callerArgs); + } + } + } + } + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); + } + } else { + return evaluate_ast(ast, env); + } +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string): string => print(evaluate(read(str), replEnv)); + +while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value)); + } catch (e) { + console.error(e.message); + } +} From 171f0cf0531b025e26271beb8d0c3c321c776141 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Thu, 31 Dec 2020 14:07:57 +0200 Subject: [PATCH 09/20] feature: step 4 if fn do --- impls/deno/core.ts | 128 ++++++++++++++++++ impls/deno/env.ts | 47 +++++-- impls/deno/reader.ts | 8 +- impls/deno/step2_eval.ts | 12 +- impls/deno/step3_env.ts | 135 +++++++++++-------- impls/deno/step4_if_fn_do.ts | 242 +++++++++++++++++++++++++++++++++++ impls/deno/types.ts | 67 ++++++++++ 7 files changed, 563 insertions(+), 76 deletions(-) create mode 100644 impls/deno/core.ts create mode 100644 impls/deno/step4_if_fn_do.ts diff --git a/impls/deno/core.ts b/impls/deno/core.ts new file mode 100644 index 0000000000..351e7d1564 --- /dev/null +++ b/impls/deno/core.ts @@ -0,0 +1,128 @@ +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; + +export const ns: Array<[string, MalType.MalType]> = [ + [ + "+", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) + ), + ], + [ + "-", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) + ), + ], + [ + "*", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) + ), + ], + [ + "/", + MalType.mkFunction(([a, b]) => + MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) + ), + ], + + [ + "pr_str", + MalType.mkFunction(([v]) => { + console.log(Printer.prStr(v, true)); + return MalType.nil; + }), + ], + [ + "println", + MalType.mkFunction((args) => { + console.log(args.map((v) => Printer.prStr(v, false)).join(" ")); + return MalType.nil; + }), + ], + [ + "prn", + MalType.mkFunction((args) => { + console.log(args.map((v) => Printer.prStr(v, true)).join(" ")); + return MalType.nil; + }), + ], + [ + "pr-str", + MalType.mkFunction((args) => + MalType.mkString(args.map((v) => Printer.prStr(v, true)).join(" ")) + ), + ], + [ + "str", + MalType.mkFunction((args) => + MalType.mkString(args.map((v) => Printer.prStr(v, false)).join("")) + ), + ], + ["list", MalType.mkFunction((v) => MalType.mkList(v))], + [ + "list?", + MalType.mkFunction(([v]) => { + if (v === undefined) { + throw new Error("Illegal Argument: list? requires a parameter"); + } + return MalType.mkBoolean(v.tag === "MalList"); + }), + ], + [ + "empty?", + MalType.mkFunction(([v]) => { + if (v === undefined || v.tag !== "MalList" && v.tag !== "MalVector") { + throw new Error( + "Illegal Argument: empty? requires a list or vector as parameter", + ); + } + return MalType.mkBoolean(v.items.length === 0); + }), + ], + [ + "count", + MalType.mkFunction(([v]) => { + if (v.tag === "MalNil") { + return MalType.mkNumber(0); + } + + if (v.tag !== "MalList" && v.tag !== "MalVector") { + throw new Error( + "Illegal Argument: count requires a list or vector parameter", + ); + } + return MalType.mkNumber(v.items.length); + }), + ], + + [ + "=", + MalType.mkFunction(([a, b]) => MalType.mkBoolean(MalType.equals(a, b))), + ], + [ + "<", + MalType.mkFunction(([a, b]) => + MalType.mkBoolean(MalType.asNumber(a) < MalType.asNumber(b)) + ), + ], + [ + "<=", + MalType.mkFunction(([a, b]) => + MalType.mkBoolean(MalType.asNumber(a) <= MalType.asNumber(b)) + ), + ], + [ + ">", + MalType.mkFunction(([a, b]) => + MalType.mkBoolean(MalType.asNumber(a) > MalType.asNumber(b)) + ), + ], + [ + ">=", + MalType.mkFunction(([a, b]) => + MalType.mkBoolean(MalType.asNumber(a) >= MalType.asNumber(b)) + ), + ], +]; diff --git a/impls/deno/env.ts b/impls/deno/env.ts index 345f0b45dd..c8704b0bbb 100644 --- a/impls/deno/env.ts +++ b/impls/deno/env.ts @@ -7,13 +7,36 @@ export type Env = { data: Scope; }; -export const mkEnv = (outer: Env | undefined = undefined): Env => ({ - outer, - data: new Map(), -}); +export const mkEnv = ( + outer: Env | undefined = undefined, + binds: Array = [], + exprs: Array = [], +): Env => { + const data: Array<[string, MalType.MalType]> = []; -export const find = (name: string, env: Env): MalType.MalType | undefined => { - const result = env.data.get(name); + for (let lp = 0; lp < binds.length; lp += 1) { + if (binds[lp].name === "&") { + if (lp !== binds.length - 2) { + throw new Error( + `Illegal Argument: the '&' symbol must be the second last parameter`, + ); + } else { + data.push([binds[lp + 1].name, MalType.mkList(exprs.slice(lp))]); + break; + } + } else { + data.push([binds[lp].name, exprs[lp]]); + } + } + + return { outer, data: new Map(data) }; +}; + +export const find = ( + name: MalType.MalSymbol, + env: Env, +): MalType.MalType | undefined => { + const result = env.data.get(name.name); if (result === undefined) { return env.outer === undefined ? undefined : find(name, env.outer); @@ -22,16 +45,20 @@ export const find = (name: string, env: Env): MalType.MalType | undefined => { } }; -export const get = (name: string, env: Env): MalType.MalType => { +export const get = (name: MalType.MalSymbol, env: Env): MalType.MalType => { const result = find(name, env); if (result === undefined) { - throw new Error(`Undefined Symbol: ${name} not found`); + throw new Error(`Undefined Symbol: ${name.name} not found`); } else { return result; } }; -export const set = (name: string, value: MalType.MalType, env: Env): void => { - env.data.set(name, value); +export const set = ( + name: MalType.MalSymbol, + value: MalType.MalType, + env: Env, +): void => { + env.data.set(name.name, value); }; diff --git a/impls/deno/reader.ts b/impls/deno/reader.ts index ee7ab3e446..93f76c1aee 100644 --- a/impls/deno/reader.ts +++ b/impls/deno/reader.ts @@ -163,10 +163,10 @@ const readAtom = (reader: Reader): MalType => { } else if (token[0] === '"') { if (token.match(/^"(?:\\.|[^\\"])*"$/)) { return mkString( - token.substr(1, token.length - 2).replaceAll("\\\\", "\\").replaceAll( - '\\"', - '"', - ).replaceAll("\\n", "\n"), + token.substr(1, token.length - 2).replace(/\\(.)/g, (_, c: string) => + c == "n" + ? "\n" + : c), ); } else { throw new Error(`Syntax Error: EOF whilst expecting '"': ${token}`); diff --git a/impls/deno/step2_eval.ts b/impls/deno/step2_eval.ts index 3bb83cab7b..b48f17e4a7 100644 --- a/impls/deno/step2_eval.ts +++ b/impls/deno/step2_eval.ts @@ -6,7 +6,7 @@ import * as Reader from "./reader.ts"; const replEnv = Env.mkEnv(); Env.set( - "+", + MalType.mkSymbol("+"), MalType.mkFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) ), @@ -14,7 +14,7 @@ Env.set( ); Env.set( - "-", + MalType.mkSymbol("-"), MalType.mkFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) ), @@ -22,7 +22,7 @@ Env.set( ); Env.set( - "*", + MalType.mkSymbol("*"), MalType.mkFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) ), @@ -30,7 +30,7 @@ Env.set( ); Env.set( - "/", + MalType.mkSymbol("/"), MalType.mkFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) ), @@ -41,7 +41,7 @@ const read = (str: string): MalType.MalType => Reader.readStr(str); const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { if (ast.tag === "MalSymbol") { - const binding = Env.find(ast.name, env); + const binding = Env.find(ast, env); if (binding === undefined) { throw new Error(`Unknown Symbol: ${ast.name}`); @@ -75,7 +75,7 @@ const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { if (callerItem.tag === "MalFunction") { return callerItem.f(callerArgs); } else if (callerItem.tag === "MalSymbol") { - const binding = Env.find(callerItem.name, env); + const binding = Env.find(callerItem, env); if (binding !== undefined && binding.tag === "MalFunction") { return binding.f(callerArgs); diff --git a/impls/deno/step3_env.ts b/impls/deno/step3_env.ts index 7fda7fea83..52e941cec6 100644 --- a/impls/deno/step3_env.ts +++ b/impls/deno/step3_env.ts @@ -6,7 +6,7 @@ import * as Reader from "./reader.ts"; const replEnv = Env.mkEnv(); Env.set( - "+", + MalType.mkSymbol("+"), MalType.mkFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) ), @@ -14,7 +14,7 @@ Env.set( ); Env.set( - "-", + MalType.mkSymbol("-"), MalType.mkFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) ), @@ -22,7 +22,7 @@ Env.set( ); Env.set( - "*", + MalType.mkSymbol("*"), MalType.mkFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) ), @@ -30,7 +30,7 @@ Env.set( ); Env.set( - "/", + MalType.mkSymbol("/"), MalType.mkFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) ), @@ -41,7 +41,7 @@ const read = (str: string): MalType.MalType => Reader.readStr(str); const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { if (ast.tag === "MalSymbol") { - return Env.get(ast.name, env); + return Env.get(ast, env); } else if (ast.tag === "MalList") { return MalType.mkList(ast.items.map((i) => evaluate(i, env))); } else if (ast.tag === "MalVector") { @@ -63,66 +63,89 @@ const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { if (ast.items.length === 0) { return ast; } else if (isNamedSymbol(ast.items[0], "def!")) { - if (ast.items[1].tag === "MalSymbol") { - const result = evaluate(ast.items[2], env); - Env.set(ast.items[1].name, result, env); - return result; - } else { - throw new Error( - `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, - ); - } + return evaluateDefBang(ast, env); } else if (isNamedSymbol(ast.items[0], "let*")) { - const innerEnv = Env.mkEnv(env); - - const bindings = ast.items[1]; - if (bindings.tag === "MalList" || bindings.tag === "MalVector") { - for (let lp = 0; lp < bindings.items.length; lp += 2) { - const name = bindings.items[lp]; - const value = bindings.items[lp + 1] ?? MalType.nil; - - if (name.tag === "MalSymbol") { - Env.set(name.name, evaluate(value, innerEnv), innerEnv); - } else { - throw new Error( - `Invalid Argument: let* binding requires a symbol name: ${ - JSON.stringify(name) - }`, - ); - } - } - - return evaluate(ast.items[2], innerEnv); - } else { - throw new Error( - `Invalid Argument: let* requires a list of bindings: ${ - JSON.stringify(bindings) - }`, - ); - } + return evaluateLetStar(ast, env); } else { - const evalList = evaluate_ast(ast, env); + return evaluateFunctionInvocation(ast, env); + } + } else { + return evaluate_ast(ast, env); + } +}; + +const evaluateDefBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateLetStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + + Env.set(name, evaluate(value, innerEnv), innerEnv); + } + + return evaluate(ast.items[2], innerEnv); +}; + +const evaluateFunctionInvocation = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const evalList = evaluate_ast(ast, env); - if (evalList.tag === "MalList" || evalList.tag === "MalVector") { - const [callerItem, ...callerArgs] = evalList.items; + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; - if (callerItem !== undefined) { - if (callerItem.tag === "MalFunction") { - return callerItem.f(callerArgs); - } else if (callerItem.tag === "MalSymbol") { - const binding = Env.get(callerItem.name, env); + if (callerItem !== undefined) { + if (callerItem.tag === "MalFunction") { + return callerItem.f(callerArgs); + } else if (callerItem.tag === "MalSymbol") { + const binding = Env.get(callerItem, env); - if (binding.tag === "MalFunction") { - return binding.f(callerArgs); - } - } + if (binding.tag === "MalFunction") { + return binding.f(callerArgs); } } - throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); } - } else { - return evaluate_ast(ast, env); } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); }; const print = (exp: MalType.MalType): string => Printer.prStr(exp); diff --git a/impls/deno/step4_if_fn_do.ts b/impls/deno/step4_if_fn_do.ts new file mode 100644 index 0000000000..41a12e551f --- /dev/null +++ b/impls/deno/step4_if_fn_do.ts @@ -0,0 +1,242 @@ +import * as Core from "./core.ts"; +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + if (ast.tag === "MalSymbol") { + return Env.get(ast, env); + } else if (ast.tag === "MalList") { + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalVector") { + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalHashMap") { + return MalType.mkHashMap( + MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + } else { + return ast; + } +}; + +const isNamedSymbol = (ast: MalType.MalType, name: string): boolean => + ast.tag === "MalSymbol" && ast.name === name; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } else if (isNamedSymbol(ast.items[0], "def!")) { + return evaluateDefBang(ast, env); + } else if (isNamedSymbol(ast.items[0], "do")) { + return evaluateDo(ast, env); + } else if (isNamedSymbol(ast.items[0], "fn*")) { + return evaluateFnStar(ast, env); + } else if (isNamedSymbol(ast.items[0], "if")) { + return evaluateIf(ast, env); + } else if (isNamedSymbol(ast.items[0], "let*")) { + return evaluateLetStar(ast, env); + } else { + return evaluateFunctionInvocation(ast, env); + } + } else { + return evaluate_ast(ast, env); + } +}; + +const evaluateDefBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDo = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const result = evaluate_ast(MalType.mkList(ast.items.slice(1)), env); + + if (result.tag !== "MalList") { + throw new Error( + `Invalid Argument: do expected a list: ${JSON.stringify(result)}`, + ); + } + if (result.items.length === 0) { + throw new Error("Invalid Argument: do expected a non-empty list"); + } + + return result.items[result.items.length - 1]; +}; + +const evaluateFnStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const [_, parameters, body] = ast.items; + + if ( + parameters == undefined || + parameters.tag !== "MalList" && parameters.tag !== "MalVector" + ) { + throw new Error("Invalid Argument: fn* expects a list of parameters"); + } + if (body === undefined) { + throw new Error("Invalid Argument: fn* expects a body"); + } + + const formalParameters: Array = []; + parameters.items.forEach((p, idx) => { + if (p.tag === "MalSymbol") { + formalParameters.push(p); + } else { + throw new Error( + `Invalid Argument: Parameter ${idx + 1} in fn* is not a symbol: ${ + JSON.stringify(p) + }`, + ); + } + }); + + const fn = (args: Array): MalType.MalType => + evaluate(body, Env.mkEnv(env, formalParameters, args)); + + return MalType.mkFunction(fn); +}; + +const evaluateIf = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items.length < 3) { + throw new Error( + `Invalid Argument: if expects at least 3 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + if (ast.items.length > 4) { + throw new Error( + `Invalid Argument: if expects no more than 4 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + + const ifGuard = evaluate(ast.items[1], env); + + if ( + ifGuard.tag === "MalNil" || ifGuard.tag === "MalBoolean" && !ifGuard.value + ) { + return ast.items.length === 4 ? evaluate(ast.items[3], env) : MalType.nil; + } else { + return evaluate(ast.items[2], env); + } +}; + +const evaluateLetStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + + Env.set(name, evaluate(value, innerEnv), innerEnv); + } + + return evaluate(ast.items[2], innerEnv); +}; + +const evaluateFunctionInvocation = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalFunction") { + return callerItem.f(callerArgs); + } else if (callerItem.tag === "MalSymbol") { + const binding = Env.get(callerItem, env); + + if (binding.tag === "MalFunction") { + return binding.f(callerArgs); + } + } + } + } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string, env: Env.Env): string => + print(evaluate(read(str), env)); + +const initReplEnv = () => { + const env = Env.mkEnv( + undefined, + Core.ns.map(([n]) => MalType.mkSymbol(n)), + Core.ns.map(([, t]) => t), + ); + + rep("(def! not (fn* (a) (if a false true)))", env); + + return env; +}; + +const repl = () => { + const env = initReplEnv(); + + while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value, env)); + } catch (e) { + console.error(e.message); + } + } +}; + +repl(); diff --git a/impls/deno/types.ts b/impls/deno/types.ts index 5dfda2c570..1ac91aae02 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -132,3 +132,70 @@ export type MalFunction = { export const mkFunction = ( f: (args: Array) => MalType, ): MalFunction => ({ tag: "MalFunction", f }); + +export const equals = (a: MalType, b: MalType): boolean => { + switch (a.tag) { + case "MalBoolean": + return b.tag === "MalBoolean" && a.value === b.value; + case "MalFunction": + return false; + case "MalHashMap": + return b.tag === "MalHashMap" && hashMapEquals(a, b); + case "MalKeyword": + return b.tag === "MalKeyword" && a.name === b.name; + case "MalList": + case "MalVector": + return (b.tag === "MalList" || b.tag === "MalVector") && seqEquals(a, b); + case "MalNil": + return b.tag === "MalNil"; + case "MalNumber": + return b.tag === "MalNumber" && a.value === b.value; + case "MalString": + return b.tag === "MalString" && a.value === b.value; + case "MalSymbol": + return b.tag === "MalSymbol" && a.name === b.name; + default: + return false; + } +}; + +const hashMapEquals = (a: MalHashMap, b: MalHashMap): boolean => { + const as = a.items; + const bs = b.items; + + if (as.size !== bs.size) { + return false; + } + + as.forEach((value, key) => { + if (!bs.has(key)) return false; + + const bValue = bs.get(key); + + if (bValue === undefined || !equals(value, bValue)) { + return false; + } + }); + + return true; +}; + +const seqEquals = ( + a: MalList | MalVector, + b: MalList | MalVector, +): boolean => { + const as = a.items; + const bs = b.items; + + if (as.length !== bs.length) { + return false; + } + + for (let loop = 0; loop < as.length; loop += 1) { + if (!equals(as[loop], bs[loop])) { + return false; + } + } + + return true; +}; From 481c283077a059a62501385d2ff91a3f4f9a4846 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Thu, 31 Dec 2020 15:48:18 +0200 Subject: [PATCH 10/20] feature: step 5 tail call optimization --- impls/deno/core.ts | 36 +++--- impls/deno/printer.ts | 1 + impls/deno/step2_eval.ts | 23 ++-- impls/deno/step3_env.ts | 23 ++-- impls/deno/step4_if_fn_do.ts | 17 ++- impls/deno/step5_tco.ts | 225 +++++++++++++++++++++++++++++++++++ impls/deno/types.ts | 29 ++++- 7 files changed, 298 insertions(+), 56 deletions(-) create mode 100644 impls/deno/step5_tco.ts diff --git a/impls/deno/core.ts b/impls/deno/core.ts index 351e7d1564..092df25301 100644 --- a/impls/deno/core.ts +++ b/impls/deno/core.ts @@ -4,66 +4,66 @@ import * as Printer from "./printer.ts"; export const ns: Array<[string, MalType.MalType]> = [ [ "+", - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) ), ], [ "-", - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) ), ], [ "*", - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) ), ], [ "/", - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) ), ], [ "pr_str", - MalType.mkFunction(([v]) => { + MalType.mkInternalFunction(([v]) => { console.log(Printer.prStr(v, true)); return MalType.nil; }), ], [ "println", - MalType.mkFunction((args) => { + MalType.mkInternalFunction((args) => { console.log(args.map((v) => Printer.prStr(v, false)).join(" ")); return MalType.nil; }), ], [ "prn", - MalType.mkFunction((args) => { + MalType.mkInternalFunction((args) => { console.log(args.map((v) => Printer.prStr(v, true)).join(" ")); return MalType.nil; }), ], [ "pr-str", - MalType.mkFunction((args) => + MalType.mkInternalFunction((args) => MalType.mkString(args.map((v) => Printer.prStr(v, true)).join(" ")) ), ], [ "str", - MalType.mkFunction((args) => + MalType.mkInternalFunction((args) => MalType.mkString(args.map((v) => Printer.prStr(v, false)).join("")) ), ], - ["list", MalType.mkFunction((v) => MalType.mkList(v))], + ["list", MalType.mkInternalFunction((v) => MalType.mkList(v))], [ "list?", - MalType.mkFunction(([v]) => { + MalType.mkInternalFunction(([v]) => { if (v === undefined) { throw new Error("Illegal Argument: list? requires a parameter"); } @@ -72,7 +72,7 @@ export const ns: Array<[string, MalType.MalType]> = [ ], [ "empty?", - MalType.mkFunction(([v]) => { + MalType.mkInternalFunction(([v]) => { if (v === undefined || v.tag !== "MalList" && v.tag !== "MalVector") { throw new Error( "Illegal Argument: empty? requires a list or vector as parameter", @@ -83,7 +83,7 @@ export const ns: Array<[string, MalType.MalType]> = [ ], [ "count", - MalType.mkFunction(([v]) => { + MalType.mkInternalFunction(([v]) => { if (v.tag === "MalNil") { return MalType.mkNumber(0); } @@ -99,29 +99,29 @@ export const ns: Array<[string, MalType.MalType]> = [ [ "=", - MalType.mkFunction(([a, b]) => MalType.mkBoolean(MalType.equals(a, b))), + MalType.mkInternalFunction(([a, b]) => MalType.mkBoolean(MalType.equals(a, b))), ], [ "<", - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkBoolean(MalType.asNumber(a) < MalType.asNumber(b)) ), ], [ "<=", - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkBoolean(MalType.asNumber(a) <= MalType.asNumber(b)) ), ], [ ">", - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkBoolean(MalType.asNumber(a) > MalType.asNumber(b)) ), ], [ ">=", - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkBoolean(MalType.asNumber(a) >= MalType.asNumber(b)) ), ], diff --git a/impls/deno/printer.ts b/impls/deno/printer.ts index 1db5f3ece2..5c6e59079e 100644 --- a/impls/deno/printer.ts +++ b/impls/deno/printer.ts @@ -33,6 +33,7 @@ export const prStr = ( case "MalKeyword": case "MalSymbol": return `${v.name}`; + case "MalInternalFunction": case "MalFunction": return "#"; } diff --git a/impls/deno/step2_eval.ts b/impls/deno/step2_eval.ts index b48f17e4a7..415871f4aa 100644 --- a/impls/deno/step2_eval.ts +++ b/impls/deno/step2_eval.ts @@ -7,7 +7,7 @@ const replEnv = Env.mkEnv(); Env.set( MalType.mkSymbol("+"), - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) ), replEnv, @@ -15,7 +15,7 @@ Env.set( Env.set( MalType.mkSymbol("-"), - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) ), replEnv, @@ -23,7 +23,7 @@ Env.set( Env.set( MalType.mkSymbol("*"), - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) ), replEnv, @@ -31,7 +31,7 @@ Env.set( Env.set( MalType.mkSymbol("/"), - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) ), replEnv, @@ -72,14 +72,13 @@ const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { const [callerItem, ...callerArgs] = evalList.items; if (callerItem !== undefined) { - if (callerItem.tag === "MalFunction") { - return callerItem.f(callerArgs); - } else if (callerItem.tag === "MalSymbol") { - const binding = Env.find(callerItem, env); - - if (binding !== undefined && binding.tag === "MalFunction") { - return binding.f(callerArgs); - } + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + return evaluate( + callerItem.body, + Env.mkEnv(callerItem.env, callerItem.params, callerArgs), + ); } } } diff --git a/impls/deno/step3_env.ts b/impls/deno/step3_env.ts index 52e941cec6..6e454da564 100644 --- a/impls/deno/step3_env.ts +++ b/impls/deno/step3_env.ts @@ -7,7 +7,7 @@ const replEnv = Env.mkEnv(); Env.set( MalType.mkSymbol("+"), - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) ), replEnv, @@ -15,7 +15,7 @@ Env.set( Env.set( MalType.mkSymbol("-"), - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) ), replEnv, @@ -23,7 +23,7 @@ Env.set( Env.set( MalType.mkSymbol("*"), - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) ), replEnv, @@ -31,7 +31,7 @@ Env.set( Env.set( MalType.mkSymbol("/"), - MalType.mkFunction(([a, b]) => + MalType.mkInternalFunction(([a, b]) => MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) ), replEnv, @@ -133,14 +133,13 @@ const evaluateFunctionInvocation = ( const [callerItem, ...callerArgs] = evalList.items; if (callerItem !== undefined) { - if (callerItem.tag === "MalFunction") { - return callerItem.f(callerArgs); - } else if (callerItem.tag === "MalSymbol") { - const binding = Env.get(callerItem, env); - - if (binding.tag === "MalFunction") { - return binding.f(callerArgs); - } + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + return evaluate( + callerItem.body, + Env.mkEnv(callerItem.env, callerItem.params, callerArgs), + ); } } } diff --git a/impls/deno/step4_if_fn_do.ts b/impls/deno/step4_if_fn_do.ts index 41a12e551f..b53e5a6f63 100644 --- a/impls/deno/step4_if_fn_do.ts +++ b/impls/deno/step4_if_fn_do.ts @@ -113,7 +113,7 @@ const evaluateFnStar = ( const fn = (args: Array): MalType.MalType => evaluate(body, Env.mkEnv(env, formalParameters, args)); - return MalType.mkFunction(fn); + return MalType.mkInternalFunction(fn); }; const evaluateIf = ( @@ -189,14 +189,13 @@ const evaluateFunctionInvocation = ( const [callerItem, ...callerArgs] = evalList.items; if (callerItem !== undefined) { - if (callerItem.tag === "MalFunction") { - return callerItem.f(callerArgs); - } else if (callerItem.tag === "MalSymbol") { - const binding = Env.get(callerItem, env); - - if (binding.tag === "MalFunction") { - return binding.f(callerArgs); - } + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + return evaluate( + callerItem.body, + Env.mkEnv(callerItem.env, callerItem.params, callerArgs), + ); } } } diff --git a/impls/deno/step5_tco.ts b/impls/deno/step5_tco.ts new file mode 100644 index 0000000000..192b6d32f1 --- /dev/null +++ b/impls/deno/step5_tco.ts @@ -0,0 +1,225 @@ +import * as Core from "./core.ts"; +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + if (ast.tag === "MalSymbol") { + return Env.get(ast, env); + } else if (ast.tag === "MalList") { + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalVector") { + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalHashMap") { + return MalType.mkHashMap( + MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + } else { + return ast; + } +}; + +const isNamedSymbol = (ast: MalType.MalType, name: string): boolean => + ast.tag === "MalSymbol" && ast.name === name; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + while (true) { + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } + + if (ast.items[0].tag === "MalSymbol") { + switch (ast.items[0].name) { + case "def!": + return evaluateDefBang(ast, env); + case "do": + ast = evaluateDo(ast, env); + continue; + case "fn*": + return evaluateFnStar(ast, env); + case "if": + ast = evaluateIf(ast, env); + continue; + case "let*": { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + + Env.set(name, evaluate(value, innerEnv), innerEnv); + } + + ast = ast.items[2]; + env = innerEnv; + continue; + } + } + } + + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + ast = callerItem.body; + env = Env.mkEnv(callerItem.env, callerItem.params, callerArgs); + continue; + } + } + } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); + } else { + return evaluate_ast(ast, env); + } + } +}; + +const evaluateDefBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDo = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + evaluate_ast(MalType.mkList(ast.items.slice(1, ast.items.length - 2)), env); + return ast.items[ast.items.length - 1]; +}; + +const evaluateFnStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const [_, parameters, body] = ast.items; + + if ( + parameters == undefined || + parameters.tag !== "MalList" && parameters.tag !== "MalVector" + ) { + throw new Error("Invalid Argument: fn* expects a list of parameters"); + } + if (body === undefined) { + throw new Error("Invalid Argument: fn* expects a body"); + } + + const formalParameters: Array = []; + parameters.items.forEach((p, idx) => { + if (p.tag === "MalSymbol") { + formalParameters.push(p); + } else { + throw new Error( + `Invalid Argument: Parameter ${idx + 1} in fn* is not a symbol: ${ + JSON.stringify(p) + }`, + ); + } + }); + + return MalType.mkFunction(body, formalParameters, env); +}; + +const evaluateIf = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items.length < 3) { + throw new Error( + `Invalid Argument: if expects at least 3 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + if (ast.items.length > 4) { + throw new Error( + `Invalid Argument: if expects no more than 4 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + + const ifGuard = evaluate(ast.items[1], env); + + if ( + ifGuard.tag === "MalNil" || ifGuard.tag === "MalBoolean" && !ifGuard.value + ) { + return ast.items.length === 4 ? ast.items[3] : MalType.nil; + } else { + return ast.items[2]; + } +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string, env: Env.Env): string => + print(evaluate(read(str), env)); + +const initReplEnv = () => { + const env = Env.mkEnv( + undefined, + Core.ns.map(([n]) => MalType.mkSymbol(n)), + Core.ns.map(([, t]) => t), + ); + + rep("(def! not (fn* (a) (if a false true)))", env); + + return env; +}; + +const repl = () => { + const env = initReplEnv(); + + while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value, env)); + } catch (e) { + console.error(e.message); + } + } +}; + +repl(); diff --git a/impls/deno/types.ts b/impls/deno/types.ts index 1ac91aae02..4ba5787baf 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -1,3 +1,5 @@ +import * as Env from "./env.ts"; + export type MalType = | MalList | MalVector @@ -8,6 +10,7 @@ export type MalType = | MalNil | MalSymbol | MalKeyword + | MalInternalFunction | MalFunction; export type MalList = { @@ -124,21 +127,37 @@ export const mkKeyword = (name: string): MalKeyword => ({ name, }); +export type MalInternalFunction = { + tag: "MalInternalFunction"; + fn: (args: Array) => MalType; +}; + +export const mkInternalFunction = ( + fn: (args: Array) => MalType, +): MalInternalFunction => ({ tag: "MalInternalFunction", fn }); + export type MalFunction = { tag: "MalFunction"; - f: (args: Array) => MalType; + body: MalType; + params: Array; + env: Env.Env; }; export const mkFunction = ( - f: (args: Array) => MalType, -): MalFunction => ({ tag: "MalFunction", f }); + body: MalType, + params: Array, + env: Env.Env, +): MalFunction => ({ + tag: "MalFunction", + body, + params, + env, +}); export const equals = (a: MalType, b: MalType): boolean => { switch (a.tag) { case "MalBoolean": return b.tag === "MalBoolean" && a.value === b.value; - case "MalFunction": - return false; case "MalHashMap": return b.tag === "MalHashMap" && hashMapEquals(a, b); case "MalKeyword": From 6870702834657a56bd2bdebafc2c4dfd1425cea3 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Thu, 31 Dec 2020 15:57:57 +0200 Subject: [PATCH 11/20] fix: missed processing the second last do parameter --- impls/deno/step5_tco.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impls/deno/step5_tco.ts b/impls/deno/step5_tco.ts index 192b6d32f1..60e0707eed 100644 --- a/impls/deno/step5_tco.ts +++ b/impls/deno/step5_tco.ts @@ -121,7 +121,7 @@ const evaluateDo = ( ast: MalType.MalList, env: Env.Env, ): MalType.MalType => { - evaluate_ast(MalType.mkList(ast.items.slice(1, ast.items.length - 2)), env); + evaluate_ast(MalType.mkList(ast.items.slice(1, ast.items.length - 1)), env); return ast.items[ast.items.length - 1]; }; From bb1c64cc5c15239dbdf5466ff60f98d1622e67ad Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Fri, 1 Jan 2021 13:25:48 +0200 Subject: [PATCH 12/20] feature: step 6 files, mutation and evil --- impls/deno/core.ts | 129 +++++++++++++++++- impls/deno/printer.ts | 2 + impls/deno/reader.ts | 12 +- impls/deno/step4_if_fn_do.ts | 6 +- impls/deno/step5_tco.ts | 9 +- impls/deno/step6_file.ts | 257 +++++++++++++++++++++++++++++++++++ impls/deno/types.ts | 22 ++- 7 files changed, 416 insertions(+), 21 deletions(-) create mode 100644 impls/deno/step6_file.ts diff --git a/impls/deno/core.ts b/impls/deno/core.ts index 092df25301..ded35dc83e 100644 --- a/impls/deno/core.ts +++ b/impls/deno/core.ts @@ -1,7 +1,11 @@ +import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; -export const ns: Array<[string, MalType.MalType]> = [ +export const ns = ( + evaluate: (ast: MalType.MalType, env: Env.Env) => MalType.MalType, +): Array<[string, MalType.MalType]> => [ [ "+", MalType.mkInternalFunction(([a, b]) => @@ -99,7 +103,9 @@ export const ns: Array<[string, MalType.MalType]> = [ [ "=", - MalType.mkInternalFunction(([a, b]) => MalType.mkBoolean(MalType.equals(a, b))), + MalType.mkInternalFunction(([a, b]) => + MalType.mkBoolean(MalType.equals(a, b)) + ), ], [ "<", @@ -125,4 +131,123 @@ export const ns: Array<[string, MalType.MalType]> = [ MalType.mkBoolean(MalType.asNumber(a) >= MalType.asNumber(b)) ), ], + + [ + "read-string", + MalType.mkInternalFunction(([s]) => { + if (s === undefined) { + return MalType.nil; + } else if (s.tag === "MalString") { + return Reader.readStr(s.value); + } else { + throw new Error( + `Invalid Argument: read-string requires a string argument: ${ + JSON.stringify(s) + }`, + ); + } + }), + ], + [ + "slurp", + MalType.mkInternalFunction(([s]) => { + if (s === undefined) { + throw new Error( + `Invalid Argument: slurp requires a single string argument`, + ); + } else if (s.tag === "MalString") { + return MalType.mkString(Deno.readTextFileSync(s.value)); + } else { + throw new Error( + `Invalid Argument: slurp requires a string argument: ${ + JSON.stringify(s) + }`, + ); + } + }), + ], + + [ + "atom", + MalType.mkInternalFunction(([v]) => { + return MalType.mkAtom(v ?? MalType.nil); + }), + ], + [ + "atom?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean(v !== undefined && v.tag === "MalAtom"); + }), + ], + [ + "deref", + MalType.mkInternalFunction(([v]) => { + if (v === undefined) { + throw new Error( + `Invalid Argument: deref requires a single atom argument`, + ); + } else if (v.tag === "MalAtom") { + return v.value; + } else { + throw new Error( + `Invalid Argument: deref requires an atom argument: ${ + JSON.stringify(v) + }`, + ); + } + }), + ], + [ + "reset!", + MalType.mkInternalFunction(([a, v]) => { + if (a === undefined) { + throw new Error(`Invalid Argument: reset! requires an atom argument`); + } else if (a.tag !== "MalAtom") { + throw new Error( + `Invalid Argument: reset! requires an atom argument: ${ + JSON.stringify(a) + }`, + ); + } + a.value = v ?? MalType.nil; + + return a.value; + }), + ], + [ + "swap!", + MalType.mkInternalFunction(([a, f, ...args]) => { + if (a === undefined) { + throw new Error(`Invalid Argument: swap! requires an atom argument`); + } else if (a.tag !== "MalAtom") { + throw new Error( + `Invalid Argument: swap! requires an atom argument: ${ + JSON.stringify(a) + }`, + ); + } + + if (f === undefined) { + throw new Error( + `Invalid Argument: swap! requires an function argument`, + ); + } else if (f.tag !== "MalFunction" && f.tag !== "MalInternalFunction") { + throw new Error( + `Invalid Argument: swap! requires a function argument: ${ + JSON.stringify(a) + }`, + ); + } + + args = [a.value, ...(args ?? [])]; + + if (f.tag === "MalFunction") { + a.value = evaluate(f.body, Env.mkEnv(f.env, f.params, args)); + } else { + a.value = f.fn(args); + } + + return a.value; + }), + ], ]; diff --git a/impls/deno/printer.ts b/impls/deno/printer.ts index 5c6e59079e..9fff800a58 100644 --- a/impls/deno/printer.ts +++ b/impls/deno/printer.ts @@ -27,6 +27,8 @@ export const prStr = ( ) }"` : v.value; + case "MalAtom": + return `(atom ${prStrReadably(v.value)})`; case "MalBoolean": case "MalNumber": return `${v.value}`; diff --git a/impls/deno/reader.ts b/impls/deno/reader.ts index 93f76c1aee..3f03cff015 100644 --- a/impls/deno/reader.ts +++ b/impls/deno/reader.ts @@ -45,7 +45,7 @@ const tokenize = (input: string): Array => { const tokenResults = regex.exec(input); if (tokenResults === null || tokenResults[0] === "") { break; - } else if (tokenResults[1] !== ";") { + } else if (tokenResults[1][0] !== ";") { tokens.push(tokenResults[1]); } } @@ -57,6 +57,8 @@ const readForm = (reader: Reader): MalType => { const token = reader.peek(); switch (token) { + case undefined: + throw new Error("Reader Error: No input"); case "(": return readList(reader); case "[": @@ -163,10 +165,10 @@ const readAtom = (reader: Reader): MalType => { } else if (token[0] === '"') { if (token.match(/^"(?:\\.|[^\\"])*"$/)) { return mkString( - token.substr(1, token.length - 2).replace(/\\(.)/g, (_, c: string) => - c == "n" - ? "\n" - : c), + token.substr(1, token.length - 2).replace( + /\\(.)/g, + (_, c: string) => c == "n" ? "\n" : c, + ), ); } else { throw new Error(`Syntax Error: EOF whilst expecting '"': ${token}`); diff --git a/impls/deno/step4_if_fn_do.ts b/impls/deno/step4_if_fn_do.ts index b53e5a6f63..bf7ff645de 100644 --- a/impls/deno/step4_if_fn_do.ts +++ b/impls/deno/step4_if_fn_do.ts @@ -209,10 +209,12 @@ const rep = (str: string, env: Env.Env): string => print(evaluate(read(str), env)); const initReplEnv = () => { + const ns = Core.ns(evaluate); + const env = Env.mkEnv( undefined, - Core.ns.map(([n]) => MalType.mkSymbol(n)), - Core.ns.map(([, t]) => t), + ns.map(([n]) => MalType.mkSymbol(n)), + ns.map(([, t]) => t), ); rep("(def! not (fn* (a) (if a false true)))", env); diff --git a/impls/deno/step5_tco.ts b/impls/deno/step5_tco.ts index 60e0707eed..87199e1bbc 100644 --- a/impls/deno/step5_tco.ts +++ b/impls/deno/step5_tco.ts @@ -22,9 +22,6 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { } }; -const isNamedSymbol = (ast: MalType.MalType, name: string): boolean => - ast.tag === "MalSymbol" && ast.name === name; - const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { while (true) { if (ast.tag === "MalList") { @@ -193,10 +190,12 @@ const rep = (str: string, env: Env.Env): string => print(evaluate(read(str), env)); const initReplEnv = () => { + const ns = Core.ns(evaluate); + const env = Env.mkEnv( undefined, - Core.ns.map(([n]) => MalType.mkSymbol(n)), - Core.ns.map(([, t]) => t), + ns.map(([n]) => MalType.mkSymbol(n)), + ns.map(([, t]) => t), ); rep("(def! not (fn* (a) (if a false true)))", env); diff --git a/impls/deno/step6_file.ts b/impls/deno/step6_file.ts new file mode 100644 index 0000000000..6d221045e2 --- /dev/null +++ b/impls/deno/step6_file.ts @@ -0,0 +1,257 @@ +import * as Core from "./core.ts"; +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + if (ast.tag === "MalSymbol") { + return Env.get(ast, env); + } else if (ast.tag === "MalList") { + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalVector") { + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + } else if (ast.tag === "MalHashMap") { + return MalType.mkHashMap( + MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + } else { + return ast; + } +}; + +const isNamedSymbol = (ast: MalType.MalType, name: string): boolean => + ast.tag === "MalSymbol" && ast.name === name; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + while (true) { + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } + + if (ast.items[0].tag === "MalSymbol") { + switch (ast.items[0].name) { + case "def!": + return evaluateDefBang(ast, env); + case "do": + ast = evaluateDo(ast, env); + continue; + case "fn*": + return evaluateFnStar(ast, env); + case "if": + ast = evaluateIf(ast, env); + continue; + case "let*": { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + + Env.set(name, evaluate(value, innerEnv), innerEnv); + } + + ast = ast.items[2]; + env = innerEnv; + continue; + } + } + } + + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + ast = callerItem.body; + env = Env.mkEnv(callerItem.env, callerItem.params, callerArgs); + continue; + } + } + } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); + } else { + return evaluate_ast(ast, env); + } + } +}; + +const evaluateDefBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDo = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + evaluate_ast(MalType.mkList(ast.items.slice(1, ast.items.length - 1)), env); + return ast.items[ast.items.length - 1]; +}; + +const evaluateFnStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const [_, parameters, body] = ast.items; + + if ( + parameters == undefined || + parameters.tag !== "MalList" && parameters.tag !== "MalVector" + ) { + throw new Error("Invalid Argument: fn* expects a list of parameters"); + } + if (body === undefined) { + throw new Error("Invalid Argument: fn* expects a body"); + } + + const formalParameters: Array = []; + parameters.items.forEach((p, idx) => { + if (p.tag === "MalSymbol") { + formalParameters.push(p); + } else { + throw new Error( + `Invalid Argument: Parameter ${idx + 1} in fn* is not a symbol: ${ + JSON.stringify(p) + }`, + ); + } + }); + + return MalType.mkFunction(body, formalParameters, env); +}; + +const evaluateIf = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items.length < 3) { + throw new Error( + `Invalid Argument: if expects at least 3 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + if (ast.items.length > 4) { + throw new Error( + `Invalid Argument: if expects no more than 4 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + + const ifGuard = evaluate(ast.items[1], env); + + if ( + ifGuard.tag === "MalNil" || ifGuard.tag === "MalBoolean" && !ifGuard.value + ) { + return ast.items.length === 4 ? ast.items[3] : MalType.nil; + } else { + return ast.items[2]; + } +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string, env: Env.Env): string => + print(evaluate(read(str), env)); + +const initReplEnv = () => { + const ns = Core.ns(evaluate); + + const env = Env.mkEnv( + undefined, + ns.map(([n]) => MalType.mkSymbol(n)), + ns.map(([, t]) => t), + ); + + Env.set( + MalType.mkSymbol("eval"), + MalType.mkInternalFunction(([a]) => { + if (a === undefined) { + return MalType.nil; + } + return evaluate(a, env); + }), + env, + ); + + rep("(def! not (fn* (a) (if a false true)))", env); + rep( + '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', + env, + ); + + return env; +}; + +const repl = (env: Env.Env) => { + while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value, env)); + } catch (e) { + if (e.message !== "Reader Error: No input") { + console.error(e.message); + } + } + } +}; + +if (Deno.args.length > 0) { + const env = initReplEnv(); + + Env.set( + MalType.mkSymbol("*ARGV*"), + MalType.mkList(Deno.args.slice(1).map(MalType.mkString)), + env, + ); + + rep(`(load-file "${Deno.args[0]}")`, env); +} else { + const env = initReplEnv(); + Env.set(MalType.mkSymbol("*ARGV*"), MalType.mkList([]), env); + + repl(env); +} diff --git a/impls/deno/types.ts b/impls/deno/types.ts index 4ba5787baf..e818eae877 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -1,17 +1,25 @@ import * as Env from "./env.ts"; export type MalType = - | MalList - | MalVector + | MalAtom + | MalBoolean + | MalFunction | MalHashMap + | MalInternalFunction + | MalKeyword + | MalList + | MalNil | MalNumber | MalString - | MalBoolean - | MalNil | MalSymbol - | MalKeyword - | MalInternalFunction - | MalFunction; + | MalVector; + +export type MalAtom = { + tag: "MalAtom"; + value: MalType; +}; + +export const mkAtom = (value: MalType): MalAtom => ({ tag: "MalAtom", value }); export type MalList = { tag: "MalList"; From 88440d4046793bdd9d25cb81a66000677eb0dee3 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Fri, 1 Jan 2021 17:07:19 +0200 Subject: [PATCH 13/20] feature: step 7 quoting --- impls/deno/core.ts | 63 ++++++++ impls/deno/step7_quote.ts | 305 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 impls/deno/step7_quote.ts diff --git a/impls/deno/core.ts b/impls/deno/core.ts index ded35dc83e..cbb2ffbfe4 100644 --- a/impls/deno/core.ts +++ b/impls/deno/core.ts @@ -250,4 +250,67 @@ export const ns = ( return a.value; }), ], + + [ + "cons", + MalType.mkInternalFunction(([a, b]) => { + if (a === undefined || b === undefined) { + throw new Error(`Invalid Argument: cons requires two arguments`); + } + + if (b.tag === "MalNil") { + return MalType.mkList([a]); + } else if (b.tag !== "MalList" && b.tag !== "MalVector") { + throw new Error( + `Invalid Argument: cons second argument must be a list: ${ + JSON.stringify(b) + }`, + ); + } + + return MalType.mkList([a, ...b.items]); + }), + ], + [ + "concat", + MalType.mkInternalFunction((lst) => { + const result: Array = []; + + lst.forEach((i) => { + if (i.tag === "MalNil") { + // do nothing + } else if (i.tag === "MalList" || i.tag === "MalVector") { + i.items.forEach((e) => result.push(e)); + } else { + throw new Error( + `Invalid Argument: concat argument must be a list: ${ + JSON.stringify(i) + }`, + ); + } + }); + + return MalType.mkList(result); + }), + ], + [ + "vec", + MalType.mkInternalFunction(([v]) => { + if (v === undefined) { + throw new Error("Invalid Argument: vec requires a single argument"); + } + + if (v.tag === "MalVector") { + return v; + } else if (v.tag === "MalList") { + return MalType.mkVector(v.items); + } else { + throw new Error( + `Invalid Argument: vec requires a single list or vector argument: ${ + JSON.stringify(v) + }`, + ); + } + }), + ], ]; diff --git a/impls/deno/step7_quote.ts b/impls/deno/step7_quote.ts new file mode 100644 index 0000000000..da16cd9ad8 --- /dev/null +++ b/impls/deno/step7_quote.ts @@ -0,0 +1,305 @@ +import * as Core from "./core.ts"; +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + switch (ast.tag) { + case "MalSymbol": + return Env.get(ast, env); + case "MalList": + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + case "MalVector": + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + case "MalHashMap": + return MalType.mkHashMap( + MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + default: + return ast; + } +}; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + while (true) { + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } + + if (ast.items[0].tag === "MalSymbol") { + switch (ast.items[0].name) { + case "def!": + return evaluateDefBang(ast, env); + case "do": + ast = evaluateDo(ast, env); + continue; + case "fn*": + return evaluateFnStar(ast, env); + case "if": + ast = evaluateIf(ast, env); + continue; + case "let*": { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + + Env.set(name, evaluate(value, innerEnv), innerEnv); + } + + ast = ast.items[2]; + env = innerEnv; + continue; + } + case "quasiquote": + ast = evaluateQuasiQuote(ast.items[1]); + continue; + case "quasiquoteexpand": + return evaluateQuasiQuote(ast.items[1]); + case "quote": + return ast.items[1]; + } + } + + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + ast = callerItem.body; + env = Env.mkEnv(callerItem.env, callerItem.params, callerArgs); + continue; + } + } + } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); + } else { + return evaluate_ast(ast, env); + } + } +}; + +const evaluateDefBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDo = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + evaluate_ast(MalType.mkList(ast.items.slice(1, ast.items.length - 1)), env); + return ast.items[ast.items.length - 1]; +}; + +const evaluateFnStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const [_, parameters, body] = ast.items; + + if ( + parameters == undefined || + parameters.tag !== "MalList" && parameters.tag !== "MalVector" + ) { + throw new Error("Invalid Argument: fn* expects a list of parameters"); + } + if (body === undefined) { + throw new Error("Invalid Argument: fn* expects a body"); + } + + const formalParameters: Array = []; + parameters.items.forEach((p, idx) => { + if (p.tag === "MalSymbol") { + formalParameters.push(p); + } else { + throw new Error( + `Invalid Argument: Parameter ${idx + 1} in fn* is not a symbol: ${ + JSON.stringify(p) + }`, + ); + } + }); + + return MalType.mkFunction(body, formalParameters, env); +}; + +const evaluateIf = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items.length < 3) { + throw new Error( + `Invalid Argument: if expects at least 3 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + if (ast.items.length > 4) { + throw new Error( + `Invalid Argument: if expects no more than 4 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + + const ifGuard = evaluate(ast.items[1], env); + + if ( + ifGuard.tag === "MalNil" || ifGuard.tag === "MalBoolean" && !ifGuard.value + ) { + return ast.items.length === 4 ? ast.items[3] : MalType.nil; + } else { + return ast.items[2]; + } +}; + +const evaluateQuasiQuote = (ast: MalType.MalType): MalType.MalType => { + const startsWith = ( + items: Array, + symbolName: string, + ): boolean => (items.length == 2 && items[0].tag === "MalSymbol" && + items[0].name === symbolName); + + const loop = ( + element: MalType.MalType, + accumulator: MalType.MalList, + ): MalType.MalList => + (element.tag == "MalList" && startsWith(element.items, "splice-unquote")) + ? MalType.mkList( + [MalType.mkSymbol("concat"), element.items[1], accumulator], + ) + : MalType.mkList( + [MalType.mkSymbol("cons"), evaluateQuasiQuote(element), accumulator], + ); + + const foldr = (xs: MalType.MalType[]): MalType.MalList => { + let acc = MalType.mkList([]); + for (let i = xs.length - 1; i >= 0; i -= 1) { + acc = loop(xs[i], acc); + } + return acc; + }; + + switch (ast.tag) { + case "MalSymbol": + return MalType.mkList([MalType.mkSymbol("quote"), ast]); + case "MalHashMap": + return MalType.mkList([MalType.mkSymbol("quote"), ast]); + case "MalList": + return (startsWith(ast.items, "unquote")) + ? ast.items[1] + : foldr(ast.items); + case "MalVector": + return MalType.mkList([MalType.mkSymbol("vec"), foldr(ast.items)]); + default: + return ast; + } +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string, env: Env.Env): string => + print(evaluate(read(str), env)); + +const initReplEnv = () => { + const ns = Core.ns(evaluate); + + const env = Env.mkEnv( + undefined, + ns.map(([n]) => MalType.mkSymbol(n)), + ns.map(([, t]) => t), + ); + + Env.set( + MalType.mkSymbol("eval"), + MalType.mkInternalFunction(([a]) => { + if (a === undefined) { + return MalType.nil; + } + return evaluate(a, env); + }), + env, + ); + + rep("(def! not (fn* (a) (if a false true)))", env); + rep( + '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', + env, + ); + + return env; +}; + +const repl = (env: Env.Env) => { + while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value, env)); + } catch (e) { + if (e.message !== "Reader Error: No input") { + console.error(e.message); + } + } + } +}; + +if (Deno.args.length > 0) { + const env = initReplEnv(); + + Env.set( + MalType.mkSymbol("*ARGV*"), + MalType.mkList(Deno.args.slice(1).map(MalType.mkString)), + env, + ); + + rep(`(load-file "${Deno.args[0]}")`, env); +} else { + const env = initReplEnv(); + Env.set(MalType.mkSymbol("*ARGV*"), MalType.mkList([]), env); + + repl(env); +} From c011f7db685799cac3a2d37b4d075e5e6d946aad Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Sat, 2 Jan 2021 09:34:30 +0200 Subject: [PATCH 14/20] feature: step 8 macros --- impls/deno/core.ts | 62 +++++++ impls/deno/step8_macros.ts | 372 +++++++++++++++++++++++++++++++++++++ impls/deno/types.ts | 3 + 3 files changed, 437 insertions(+) create mode 100644 impls/deno/step8_macros.ts diff --git a/impls/deno/core.ts b/impls/deno/core.ts index cbb2ffbfe4..203d994b75 100644 --- a/impls/deno/core.ts +++ b/impls/deno/core.ts @@ -313,4 +313,66 @@ export const ns = ( } }), ], + [ + "nth", + MalType.mkInternalFunction(([l, i]) => { + if (l === undefined || l.tag !== "MalList" && l.tag !== "MalVector") { + throw new Error( + `Invalid Argument: nth parameter 0: expected list or vector: ${ + JSON.stringify(i) + }`, + ); + } + if (i === undefined || i.tag !== "MalNumber") { + throw new Error( + `Invalid Argument: nth parameter 1: expected number: ${ + JSON.stringify(i) + }`, + ); + } + + const result = l.items[i.value]; + if (result === undefined) { + throw new Error( + `Index Out Of Range: nth: ${i.value} exceeds bounds of ${ + JSON.stringify(l) + }`, + ); + } else { + return result; + } + }), + ], + [ + "first", + MalType.mkInternalFunction(([v]) => { + if ( + v === undefined || + v.tag !== "MalList" && v.tag !== "MalVector" && v.tag !== "MalNil" + ) { + throw new Error( + "Invalid Argument: first parameter 0: expected list, vector or nil", + ); + } + + return v.tag === "MalNil" ? MalType.nil : v.items[0] ?? MalType.nil; + }), + ], + [ + "rest", + MalType.mkInternalFunction(([v]) => { + if ( + v === undefined || + v.tag !== "MalList" && v.tag !== "MalVector" && v.tag !== "MalNil" + ) { + throw new Error( + "Invalid Argument: rest parameter 0: expected list, vector or nil", + ); + } + + return v.tag === "MalNil" + ? MalType.mkList([]) + : MalType.mkList(v.items.slice(1)); + }), + ], ]; diff --git a/impls/deno/step8_macros.ts b/impls/deno/step8_macros.ts new file mode 100644 index 0000000000..a4bc946d27 --- /dev/null +++ b/impls/deno/step8_macros.ts @@ -0,0 +1,372 @@ +import * as Core from "./core.ts"; +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + switch (ast.tag) { + case "MalSymbol": + return Env.get(ast, env); + case "MalList": + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + case "MalVector": + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + case "MalHashMap": + return MalType.mkHashMap( + MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + default: + return ast; + } +}; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + while (true) { + if (ast.tag === "MalList") { + ast = macroExpand(ast, env); + + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } + + if (ast.items[0].tag === "MalSymbol") { + switch (ast.items[0].name) { + case "def!": + return evaluateDefBang(ast, env); + case "defmacro!": + return evaluateDefMacroBang(ast, env); + case "do": + ast = evaluateDo(ast, env); + continue; + case "fn*": + return evaluateFnStar(ast, env); + case "if": + ast = evaluateIf(ast, env); + continue; + case "let*": { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + + Env.set(name, evaluate(value, innerEnv), innerEnv); + } + + ast = ast.items[2]; + env = innerEnv; + continue; + } + case "macroexpand": + return macroExpand(ast.items[1], env); + case "quasiquote": + ast = evaluateQuasiQuote(ast.items[1]); + continue; + case "quasiquoteexpand": + return evaluateQuasiQuote(ast.items[1]); + case "quote": + return ast.items[1]; + } + } + + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + ast = callerItem.body; + env = Env.mkEnv(callerItem.env, callerItem.params, callerArgs); + continue; + } + } + } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); + } else { + return evaluate_ast(ast, env); + } + } else { + return evaluate_ast(ast, env); + } + } +}; + +const macroExpand = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + const macroFunction = ( + ast: MalType.MalType, + env: Env.Env, + ): [MalType.MalFunction, Array] | undefined => { + if ( + (ast.tag === "MalList" || ast.tag === "MalVector") && + ast.items.length > 0 && ast.items[0].tag === "MalSymbol" + ) { + const value = Env.find(ast.items[0], env); + + return value !== undefined && value.tag === "MalFunction" && value.isMacro + ? [value, ast.items.slice(1)] + : undefined; + } else { + return undefined; + } + }; + + while (true) { + const macro = macroFunction(ast, env); + if (macro === undefined) { + return ast; + } else { + ast = evaluate( + macro[0].body, + Env.mkEnv(macro[0].env, macro[0].params, macro[1]), + ); + } + } +}; + +const evaluateDefBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDefMacroBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: defmacro! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + + if (result.tag === "MalFunction") { + result.isMacro = true; + } + + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDo = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + evaluate_ast(MalType.mkList(ast.items.slice(1, ast.items.length - 1)), env); + return ast.items[ast.items.length - 1]; +}; + +const evaluateFnStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const [_, parameters, body] = ast.items; + + if ( + parameters == undefined || + parameters.tag !== "MalList" && parameters.tag !== "MalVector" + ) { + throw new Error("Invalid Argument: fn* expects a list of parameters"); + } + if (body === undefined) { + throw new Error("Invalid Argument: fn* expects a body"); + } + + const formalParameters: Array = []; + parameters.items.forEach((p, idx) => { + if (p.tag === "MalSymbol") { + formalParameters.push(p); + } else { + throw new Error( + `Invalid Argument: Parameter ${idx + 1} in fn* is not a symbol: ${ + JSON.stringify(p) + }`, + ); + } + }); + + return MalType.mkFunction(body, formalParameters, env); +}; + +const evaluateIf = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items.length < 3) { + throw new Error( + `Invalid Argument: if expects at least 3 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + if (ast.items.length > 4) { + throw new Error( + `Invalid Argument: if expects no more than 4 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + + const ifGuard = evaluate(ast.items[1], env); + + if ( + ifGuard.tag === "MalNil" || ifGuard.tag === "MalBoolean" && !ifGuard.value + ) { + return ast.items.length === 4 ? ast.items[3] : MalType.nil; + } else { + return ast.items[2]; + } +}; + +const evaluateQuasiQuote = (ast: MalType.MalType): MalType.MalType => { + const startsWith = ( + items: Array, + symbolName: string, + ): boolean => (items.length == 2 && items[0].tag === "MalSymbol" && + items[0].name === symbolName); + + const loop = ( + element: MalType.MalType, + accumulator: MalType.MalList, + ): MalType.MalList => + (element.tag == "MalList" && startsWith(element.items, "splice-unquote")) + ? MalType.mkList( + [MalType.mkSymbol("concat"), element.items[1], accumulator], + ) + : MalType.mkList( + [MalType.mkSymbol("cons"), evaluateQuasiQuote(element), accumulator], + ); + + const foldr = (xs: MalType.MalType[]): MalType.MalList => { + let acc = MalType.mkList([]); + for (let i = xs.length - 1; i >= 0; i -= 1) { + acc = loop(xs[i], acc); + } + return acc; + }; + + switch (ast.tag) { + case "MalSymbol": + return MalType.mkList([MalType.mkSymbol("quote"), ast]); + case "MalHashMap": + return MalType.mkList([MalType.mkSymbol("quote"), ast]); + case "MalList": + return (startsWith(ast.items, "unquote")) + ? ast.items[1] + : foldr(ast.items); + case "MalVector": + return MalType.mkList([MalType.mkSymbol("vec"), foldr(ast.items)]); + default: + return ast; + } +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string, env: Env.Env): string => + print(evaluate(read(str), env)); + +const initReplEnv = () => { + const ns = Core.ns(evaluate); + + const env = Env.mkEnv( + undefined, + ns.map(([n]) => MalType.mkSymbol(n)), + ns.map(([, t]) => t), + ); + + Env.set( + MalType.mkSymbol("eval"), + MalType.mkInternalFunction(([a]) => { + if (a === undefined) { + return MalType.nil; + } + return evaluate(a, env); + }), + env, + ); + + rep("(def! not (fn* (a) (if a false true)))", env); + rep( + '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', + env, + ); + rep( + "(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)))))))", + env, + ); + + return env; +}; + +const repl = (env: Env.Env) => { + while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value, env)); + } catch (e) { + if (e.message !== "Reader Error: No input") { + console.error(e.message); + } + } + } +}; + +if (Deno.args.length > 0) { + const env = initReplEnv(); + + Env.set( + MalType.mkSymbol("*ARGV*"), + MalType.mkList(Deno.args.slice(1).map(MalType.mkString)), + env, + ); + + rep(`(load-file "${Deno.args[0]}")`, env); +} else { + const env = initReplEnv(); + Env.set(MalType.mkSymbol("*ARGV*"), MalType.mkList([]), env); + + repl(env); +} diff --git a/impls/deno/types.ts b/impls/deno/types.ts index e818eae877..846becc846 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -149,17 +149,20 @@ export type MalFunction = { body: MalType; params: Array; env: Env.Env; + isMacro: boolean; }; export const mkFunction = ( body: MalType, params: Array, env: Env.Env, + isMacro: boolean = false, ): MalFunction => ({ tag: "MalFunction", body, params, env, + isMacro, }); export const equals = (a: MalType, b: MalType): boolean => { From bb3a096283f106ccb28e5de47dbd01c56475b23a Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Sat, 2 Jan 2021 09:46:57 +0200 Subject: [PATCH 15/20] refactor: simplify the macro expansion --- impls/deno/step8_macros.ts | 131 ++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 68 deletions(-) diff --git a/impls/deno/step8_macros.ts b/impls/deno/step8_macros.ts index a4bc946d27..b34b974ec8 100644 --- a/impls/deno/step8_macros.ts +++ b/impls/deno/step8_macros.ts @@ -25,91 +25,86 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { while (true) { + ast = macroExpand(ast, env); if (ast.tag === "MalList") { - ast = macroExpand(ast, env); + if (ast.items.length === 0) { + return ast; + } - if (ast.tag === "MalList") { - if (ast.items.length === 0) { - return ast; - } + if (ast.items[0].tag === "MalSymbol") { + switch (ast.items[0].name) { + case "def!": + return evaluateDefBang(ast, env); + case "defmacro!": + return evaluateDefMacroBang(ast, env); + case "do": + ast = evaluateDo(ast, env); + continue; + case "fn*": + return evaluateFnStar(ast, env); + case "if": + ast = evaluateIf(ast, env); + continue; + case "let*": { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; - if (ast.items[0].tag === "MalSymbol") { - switch (ast.items[0].name) { - case "def!": - return evaluateDefBang(ast, env); - case "defmacro!": - return evaluateDefMacroBang(ast, env); - case "do": - ast = evaluateDo(ast, env); - continue; - case "fn*": - return evaluateFnStar(ast, env); - case "if": - ast = evaluateIf(ast, env); - continue; - case "let*": { - const bindings = ast.items[1]; - - if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + if (name.tag !== "MalSymbol") { throw new Error( - `Invalid Argument: let* requires a list of bindings: ${ - JSON.stringify(bindings) + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) }`, ); } - const innerEnv = Env.mkEnv(env); - for (let lp = 0; lp < bindings.items.length; lp += 2) { - const name = bindings.items[lp]; - const value = bindings.items[lp + 1] ?? MalType.nil; - - if (name.tag !== "MalSymbol") { - throw new Error( - `Invalid Argument: let* binding requires a symbol name: ${ - JSON.stringify(name) - }`, - ); - } - - Env.set(name, evaluate(value, innerEnv), innerEnv); - } - - ast = ast.items[2]; - env = innerEnv; - continue; + Env.set(name, evaluate(value, innerEnv), innerEnv); } - case "macroexpand": - return macroExpand(ast.items[1], env); - case "quasiquote": - ast = evaluateQuasiQuote(ast.items[1]); - continue; - case "quasiquoteexpand": - return evaluateQuasiQuote(ast.items[1]); - case "quote": - return ast.items[1]; + + ast = ast.items[2]; + env = innerEnv; + continue; } + case "macroexpand": + return macroExpand(ast.items[1], env); + case "quasiquote": + ast = evaluateQuasiQuote(ast.items[1]); + continue; + case "quasiquoteexpand": + return evaluateQuasiQuote(ast.items[1]); + case "quote": + return ast.items[1]; } + } - const evalList = evaluate_ast(ast, env); + const evalList = evaluate_ast(ast, env); - if (evalList.tag === "MalList" || evalList.tag === "MalVector") { - const [callerItem, ...callerArgs] = evalList.items; + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; - if (callerItem !== undefined) { - if (callerItem.tag === "MalInternalFunction") { - return callerItem.fn(callerArgs); - } else if (callerItem.tag === "MalFunction") { - ast = callerItem.body; - env = Env.mkEnv(callerItem.env, callerItem.params, callerArgs); - continue; - } + if (callerItem !== undefined) { + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + ast = callerItem.body; + env = Env.mkEnv(callerItem.env, callerItem.params, callerArgs); + continue; } } - - throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); - } else { - return evaluate_ast(ast, env); } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); } else { return evaluate_ast(ast, env); } From afb53f95b0c51d41013ad728ecb48c7d5eb15a8e Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Sat, 2 Jan 2021 13:06:17 +0200 Subject: [PATCH 16/20] feature: step 9 try --- impls/deno/core.ts | 290 ++++++++++++++++++++++++ impls/deno/env.ts | 2 +- impls/deno/printer.ts | 4 +- impls/deno/step2_eval.ts | 2 +- impls/deno/step3_env.ts | 2 +- impls/deno/step4_if_fn_do.ts | 2 +- impls/deno/step5_tco.ts | 2 +- impls/deno/step6_file.ts | 2 +- impls/deno/step7_quote.ts | 2 +- impls/deno/step8_macros.ts | 2 +- impls/deno/step9_try.ts | 416 +++++++++++++++++++++++++++++++++++ impls/deno/types.ts | 88 ++++++-- 12 files changed, 780 insertions(+), 34 deletions(-) create mode 100644 impls/deno/step9_try.ts diff --git a/impls/deno/core.ts b/impls/deno/core.ts index 203d994b75..6458647f00 100644 --- a/impls/deno/core.ts +++ b/impls/deno/core.ts @@ -375,4 +375,294 @@ export const ns = ( : MalType.mkList(v.items.slice(1)); }), ], + + [ + "throw", + MalType.mkInternalFunction(([v]) => { + if ( + v === undefined + ) { + throw new Error( + "Invalid Argument: throw parameter 0: expected", + ); + } + + throw v; + }), + ], + + [ + "apply", + MalType.mkInternalFunction(([f, ...rest]) => { + if ( + f === undefined || + f.tag !== "MalInternalFunction" && f.tag !== "MalFunction" + ) { + throw new Error( + "Invalid Argument: apply parameter 0: expected function", + ); + } + + const items: Array = []; + + rest.forEach((v) => { + if (v.tag === "MalList" || v.tag == "MalVector") { + v.items.forEach((i) => items.push(i)); + } else { + items.push(v); + } + }); + + if (f.tag === "MalInternalFunction") { + return f.fn(items); + } else { + const ast = f.body; + const env = Env.mkEnv(f.env, f.params, items); + return evaluate(ast, env); + } + }), + ], + [ + "map", + MalType.mkInternalFunction(([f, seq]) => { + if ( + f === undefined || + f.tag !== "MalInternalFunction" && f.tag !== "MalFunction" + ) { + throw new Error( + "Invalid Argument: map parameter 0: expected function", + ); + } + if ( + seq === undefined || seq.tag !== "MalList" && seq.tag !== "MalVector" + ) { + throw new Error( + "Invalid Argument: map parameter 1: expected sequence", + ); + } + + const fn = f.tag === "MalInternalFunction" + ? f.fn + : (args: Array): MalType.MalType => + evaluate(f.body, Env.mkEnv(f.env, f.params, args)); + + const mappedItems = seq.items.map((p) => fn([p])); + + return MalType.mkList(mappedItems); + }), + ], + [ + "nil?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean(v !== undefined && v.tag === "MalNil"); + }), + ], + [ + "true?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean( + v !== undefined && v.tag === "MalBoolean" && v.value, + ); + }), + ], + [ + "false?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean( + v !== undefined && v.tag === "MalBoolean" && !v.value, + ); + }), + ], + [ + "symbol?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean(v !== undefined && v.tag === "MalSymbol"); + }), + ], + [ + "symbol", + MalType.mkInternalFunction(([v]) => { + if (v === undefined) { + throw new Error( + "Invalid Argument: symbol parameter 0: expected string or symbol", + ); + } + if (v.tag === "MalSymbol") { + return v; + } else if (v.tag === "MalString") { + return MalType.mkSymbol(v.value); + } else { + throw new Error( + `Invalid Argument: symbol parameter 0: expected string or symbol: ${ + JSON.stringify(v) + }`, + ); + } + }), + ], + [ + "keyword", + MalType.mkInternalFunction(([v]) => { + if (v === undefined) { + throw new Error( + "Invalid Argument: symbol parameter 0: expected string or keyword", + ); + } + if (v.tag === "MalKeyword") { + return v; + } else if (v.tag === "MalString") { + return MalType.mkKeyword(`:${v.value}`); + } else { + throw new Error( + `Invalid Argument: keyword parameter 0: expected string or keyword: ${ + JSON.stringify(v) + }`, + ); + } + }), + ], + [ + "keyword?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean(v !== undefined && v.tag === "MalKeyword"); + }), + ], + [ + "vector", + MalType.mkInternalFunction((lst) => { + return MalType.mkVector(lst); + }), + ], + [ + "vector?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean(v !== undefined && v.tag === "MalVector"); + }), + ], + [ + "sequential?", + MalType.mkInternalFunction(([v]) => + MalType.mkBoolean( + v !== undefined && (v.tag === "MalVector" || v.tag === "MalList"), + ) + ), + ], + [ + "hash-map", + MalType.mkInternalFunction((items) => + MalType.mkHashMap(mkHashPairs(items, "hash-map")) + ), + ], + [ + "map?", + MalType.mkInternalFunction(([v]) => + MalType.mkBoolean(v !== undefined && v.tag === "MalHashMap") + ), + ], + [ + "assoc", + MalType.mkInternalFunction(([m, ...items]) => { + if (m === undefined || m.tag !== "MalHashMap") { + throw new Error( + "Invalid Argument: assoc parameter 0: expected a hash map", + ); + } + return MalType.mapAssoc(m, mkHashPairs(items ?? [], "assoc")); + }), + ], + [ + "dissoc", + MalType.mkInternalFunction(([m, ...items]) => { + if (m === undefined || m.tag !== "MalHashMap") { + throw new Error( + "Invalid Argument: dissoc parameter 0: expected a hash map", + ); + } + + return MalType.mapDissoc(m, items); + }), + ], + [ + "get", + MalType.mkInternalFunction(([m, key]) => { + if (m === undefined || m.tag !== "MalHashMap" && m.tag !== "MalNil") { + throw new Error( + "Invalid Argument: get parameter 0: expected a hash map", + ); + } + + if ( + key === undefined || + (key.tag !== "MalKeyword" && key.tag !== "MalString") + ) { + throw new Error( + "Invalid Argument: get parameter 1: expected a keyword or string", + ); + } + + return m.tag === "MalNil" ? MalType.nil : MalType.mapGet(m, key); + }), + ], + [ + "contains?", + MalType.mkInternalFunction(([m, key]) => { + if (m === undefined || m.tag !== "MalHashMap") { + throw new Error( + "Invalid Argument: contains? parameter 0: expected a hash map", + ); + } + if ( + key === undefined || + (key.tag !== "MalKeyword" && key.tag !== "MalString") + ) { + throw new Error( + "Invalid Argument: contains? parameter 1: expected a keyword or string", + ); + } + + return MalType.mapContains(m, key); + }), + ], + [ + "keys", + MalType.mkInternalFunction(([m]) => { + if (m === undefined || m.tag !== "MalHashMap") { + throw new Error( + "Invalid Argument: contains? parameter 0: expected a hash map", + ); + } + + return MalType.mkList(MalType.mapKeys(m)); + }), + ], + [ + "vals", + MalType.mkInternalFunction(([m]) => { + if (m === undefined || m.tag !== "MalHashMap") { + throw new Error( + "Invalid Argument: contains? parameter 0: expected a hash map", + ); + } + + return MalType.mkList(MalType.mapValues(m)); + }), + ], ]; + +const mkHashPairs = ( + items: Array, + procedureName: string, +): Array<[MalType.MalType, MalType.MalType]> => { + const args: Array<[MalType.MalType, MalType.MalType]> = []; + + if (items.length % 2 === 1) { + throw new Error( + `Invalid Argument: ${procedureName}: Odd number of arguments`, + ); + } + for (let lp = 0; lp < items.length; lp += 2) { + args.push([items[lp], items[lp + 1]]); + } + + return args; +}; diff --git a/impls/deno/env.ts b/impls/deno/env.ts index c8704b0bbb..013c2ce0ae 100644 --- a/impls/deno/env.ts +++ b/impls/deno/env.ts @@ -49,7 +49,7 @@ export const get = (name: MalType.MalSymbol, env: Env): MalType.MalType => { const result = find(name, env); if (result === undefined) { - throw new Error(`Undefined Symbol: ${name.name} not found`); + throw new Error(`'${name.name}' not found`); } else { return result; } diff --git a/impls/deno/printer.ts b/impls/deno/printer.ts index 9fff800a58..4b11567697 100644 --- a/impls/deno/printer.ts +++ b/impls/deno/printer.ts @@ -1,4 +1,4 @@ -import { MalType, mapValues } from "./types.ts"; +import { MalType, mapKeyValues } from "./types.ts"; export const prStr = ( v: MalType, @@ -12,7 +12,7 @@ export const prStr = ( return `[${v.items.map(prStrReadably).join(" ")}]`; case "MalHashMap": return `{${ - mapValues(v).map(([k, v]) => + mapKeyValues(v).map(([k, v]) => `${prStrReadably(k)} ${prStrReadably(v)}` ).join(" ") }}`; diff --git a/impls/deno/step2_eval.ts b/impls/deno/step2_eval.ts index 415871f4aa..e354ac8474 100644 --- a/impls/deno/step2_eval.ts +++ b/impls/deno/step2_eval.ts @@ -54,7 +54,7 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); } else if (ast.tag === "MalHashMap") { return MalType.mkHashMap( - MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), ); } else { return ast; diff --git a/impls/deno/step3_env.ts b/impls/deno/step3_env.ts index 6e454da564..16266b7a3b 100644 --- a/impls/deno/step3_env.ts +++ b/impls/deno/step3_env.ts @@ -48,7 +48,7 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); } else if (ast.tag === "MalHashMap") { return MalType.mkHashMap( - MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), ); } else { return ast; diff --git a/impls/deno/step4_if_fn_do.ts b/impls/deno/step4_if_fn_do.ts index bf7ff645de..0ca2053311 100644 --- a/impls/deno/step4_if_fn_do.ts +++ b/impls/deno/step4_if_fn_do.ts @@ -15,7 +15,7 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); } else if (ast.tag === "MalHashMap") { return MalType.mkHashMap( - MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), ); } else { return ast; diff --git a/impls/deno/step5_tco.ts b/impls/deno/step5_tco.ts index 87199e1bbc..c227fa370c 100644 --- a/impls/deno/step5_tco.ts +++ b/impls/deno/step5_tco.ts @@ -15,7 +15,7 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); } else if (ast.tag === "MalHashMap") { return MalType.mkHashMap( - MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), ); } else { return ast; diff --git a/impls/deno/step6_file.ts b/impls/deno/step6_file.ts index 6d221045e2..07f6d0e027 100644 --- a/impls/deno/step6_file.ts +++ b/impls/deno/step6_file.ts @@ -15,7 +15,7 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); } else if (ast.tag === "MalHashMap") { return MalType.mkHashMap( - MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), ); } else { return ast; diff --git a/impls/deno/step7_quote.ts b/impls/deno/step7_quote.ts index da16cd9ad8..cf76cc3aa3 100644 --- a/impls/deno/step7_quote.ts +++ b/impls/deno/step7_quote.ts @@ -16,7 +16,7 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); case "MalHashMap": return MalType.mkHashMap( - MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), ); default: return ast; diff --git a/impls/deno/step8_macros.ts b/impls/deno/step8_macros.ts index b34b974ec8..f623ccc7e1 100644 --- a/impls/deno/step8_macros.ts +++ b/impls/deno/step8_macros.ts @@ -16,7 +16,7 @@ const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); case "MalHashMap": return MalType.mkHashMap( - MalType.mapValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), ); default: return ast; diff --git a/impls/deno/step9_try.ts b/impls/deno/step9_try.ts new file mode 100644 index 0000000000..98f5fd1f26 --- /dev/null +++ b/impls/deno/step9_try.ts @@ -0,0 +1,416 @@ +import * as Core from "./core.ts"; +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + switch (ast.tag) { + case "MalSymbol": + return Env.get(ast, env); + case "MalList": + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + case "MalVector": + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + case "MalHashMap": + return MalType.mkHashMap( + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + default: + return ast; + } +}; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + while (true) { + // console.log(Printer.prStr(ast)); + + ast = macroExpand(ast, env); + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } + + if (ast.items[0].tag === "MalSymbol") { + switch (ast.items[0].name) { + case "def!": + return evaluateDefBang(ast, env); + case "defmacro!": + return evaluateDefMacroBang(ast, env); + case "do": + ast = evaluateDo(ast, env); + continue; + case "fn*": + return evaluateFnStar(ast, env); + case "if": + ast = evaluateIf(ast, env); + continue; + case "let*": { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + + Env.set(name, evaluate(value, innerEnv), innerEnv); + } + + ast = ast.items[2]; + env = innerEnv; + continue; + } + case "macroexpand": + return macroExpand(ast.items[1], env); + case "quasiquote": + ast = evaluateQuasiQuote(ast.items[1]); + continue; + case "quasiquoteexpand": + return evaluateQuasiQuote(ast.items[1]); + case "quote": + return ast.items[1]; + case "try*": { + if (ast.items.length === 2) { + return evaluate(ast.items[1], env); + } + + if (ast.items.length === 3) { + const catchItem = ast.items[2]; + + if ( + (catchItem.tag === "MalList" || + catchItem.tag === "MalVector") && catchItem.items.length === 3 + ) { + const catchKeyword = catchItem.items[0]; + const catchSymbol = catchItem.items[1]; + const catchBody = catchItem.items[2]; + if ( + catchKeyword.tag === "MalSymbol" && + catchKeyword.name === "catch*" && + catchSymbol.tag === "MalSymbol" + ) { + try { + return evaluate(ast.items[1], env); + } catch (e) { + const ep = e.tag !== undefined && e.tag.startsWith("Mal") + ? e + : e.message !== undefined + ? MalType.mkString(e.message) + : MalType.mkString(JSON.stringify(e)); + + env = Env.mkEnv( + env, + [catchSymbol], + [ep], + ); + ast = catchBody; + continue; + } + } + } + } + throw new Error( + `Invalid Argument: try* is not in a valid form: ${ + JSON.stringify(ast) + }`, + ); + } + } + } + + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + ast = callerItem.body; + env = Env.mkEnv(callerItem.env, callerItem.params, callerArgs); + continue; + } + } + } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); + } else { + return evaluate_ast(ast, env); + } + } +}; + +const macroExpand = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + const macroFunction = ( + ast: MalType.MalType, + env: Env.Env, + ): [MalType.MalFunction, Array] | undefined => { + if ( + (ast.tag === "MalList" || ast.tag === "MalVector") && + ast.items.length > 0 && ast.items[0].tag === "MalSymbol" + ) { + const value = Env.find(ast.items[0], env); + + return value !== undefined && value.tag === "MalFunction" && value.isMacro + ? [value, ast.items.slice(1)] + : undefined; + } else { + return undefined; + } + }; + + while (true) { + const macro = macroFunction(ast, env); + if (macro === undefined) { + return ast; + } else { + ast = evaluate( + macro[0].body, + Env.mkEnv(macro[0].env, macro[0].params, macro[1]), + ); + } + } +}; + +const evaluateDefBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDefMacroBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: defmacro! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + + if (result.tag === "MalFunction") { + result.isMacro = true; + } + + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDo = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + evaluate_ast(MalType.mkList(ast.items.slice(1, ast.items.length - 1)), env); + return ast.items[ast.items.length - 1]; +}; + +const evaluateFnStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const [_, parameters, body] = ast.items; + + if ( + parameters == undefined || + parameters.tag !== "MalList" && parameters.tag !== "MalVector" + ) { + throw new Error("Invalid Argument: fn* expects a list of parameters"); + } + if (body === undefined) { + throw new Error("Invalid Argument: fn* expects a body"); + } + + const formalParameters: Array = []; + parameters.items.forEach((p, idx) => { + if (p.tag === "MalSymbol") { + formalParameters.push(p); + } else { + throw new Error( + `Invalid Argument: Parameter ${idx + 1} in fn* is not a symbol: ${ + JSON.stringify(p) + }`, + ); + } + }); + + return MalType.mkFunction(body, formalParameters, env); +}; + +const evaluateIf = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items.length < 3) { + throw new Error( + `Invalid Argument: if expects at least 3 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + if (ast.items.length > 4) { + throw new Error( + `Invalid Argument: if expects no more than 4 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + + const ifGuard = evaluate(ast.items[1], env); + + if ( + ifGuard.tag === "MalNil" || ifGuard.tag === "MalBoolean" && !ifGuard.value + ) { + return ast.items.length === 4 ? ast.items[3] : MalType.nil; + } else { + return ast.items[2]; + } +}; + +const evaluateQuasiQuote = (ast: MalType.MalType): MalType.MalType => { + const startsWith = ( + items: Array, + symbolName: string, + ): boolean => (items.length == 2 && items[0].tag === "MalSymbol" && + items[0].name === symbolName); + + const loop = ( + element: MalType.MalType, + accumulator: MalType.MalList, + ): MalType.MalList => + (element.tag == "MalList" && startsWith(element.items, "splice-unquote")) + ? MalType.mkList( + [MalType.mkSymbol("concat"), element.items[1], accumulator], + ) + : MalType.mkList( + [MalType.mkSymbol("cons"), evaluateQuasiQuote(element), accumulator], + ); + + const foldr = (xs: MalType.MalType[]): MalType.MalList => { + let acc = MalType.mkList([]); + for (let i = xs.length - 1; i >= 0; i -= 1) { + acc = loop(xs[i], acc); + } + return acc; + }; + + switch (ast.tag) { + case "MalSymbol": + return MalType.mkList([MalType.mkSymbol("quote"), ast]); + case "MalHashMap": + return MalType.mkList([MalType.mkSymbol("quote"), ast]); + case "MalList": + return (startsWith(ast.items, "unquote")) + ? ast.items[1] + : foldr(ast.items); + case "MalVector": + return MalType.mkList([MalType.mkSymbol("vec"), foldr(ast.items)]); + default: + return ast; + } +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string, env: Env.Env): string => + print(evaluate(read(str), env)); + +const initReplEnv = () => { + const ns = Core.ns(evaluate); + + const env = Env.mkEnv( + undefined, + ns.map(([n]) => MalType.mkSymbol(n)), + ns.map(([, t]) => t), + ); + + Env.set( + MalType.mkSymbol("eval"), + MalType.mkInternalFunction(([a]) => { + if (a === undefined) { + return MalType.nil; + } + return evaluate(a, env); + }), + env, + ); + + rep("(def! not (fn* (a) (if a false true)))", env); + rep( + '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', + env, + ); + rep( + "(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)))))))", + env, + ); + + return env; +}; + +const repl = (env: Env.Env) => { + while (true) { + const value = prompt("user>"); + + if (value === null) { + break; + } + + try { + console.log(rep(value, env)); + } catch (e) { + if (e.message !== "Reader Error: No input") { + const message = (e.tag !== undefined) ? Printer.prStr(e) : e.message; + console.error(`Exception: ${message}`); + } + } + } +}; + +if (Deno.args.length > 0) { + const env = initReplEnv(); + + Env.set( + MalType.mkSymbol("*ARGV*"), + MalType.mkList(Deno.args.slice(1).map(MalType.mkString)), + env, + ); + + rep(`(load-file "${Deno.args[0]}")`, env); +} else { + const env = initReplEnv(); + Env.set(MalType.mkSymbol("*ARGV*"), MalType.mkList([]), env); + + repl(env); +} diff --git a/impls/deno/types.ts b/impls/deno/types.ts index 846becc846..707ded8534 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -46,30 +46,68 @@ export type MalHashMap = { items: Map; }; -export const mkHashMap = (values: Array<[MalType, MalType]>): MalHashMap => { - const items: Array<[string, MalType]> = []; +export const mkHashMap = (values: Array<[MalType, MalType]>): MalHashMap => ({ + tag: "MalHashMap", + items: new Map(values.map(([k, v]) => [mapHashMapKey(k), v])), +}); + +export const mapAssoc = ( + malMap: MalHashMap, + values: Array<[MalType, MalType]>, +): MalHashMap => { + const result = new Map(malMap.items); values.forEach(([k, v]) => { - if (k.tag === "MalString") { - items.push([`s${k.value}`, v]); - } else if (k.tag === "MalKeyword") { - items.push([`t${k.name}`, v]); - } else { - throw new Error( - `Precondition Error: Unable to use ${ - JSON.stringify(k) - } as a hashmap key.`, - ); - } + result.set(mapHashMapKey(k), v); + }); + + return { tag: "MalHashMap", items: result }; +}; + +export const mapDissoc = ( + malMap: MalHashMap, + values: Array, +): MalHashMap => { + const result = new Map(malMap.items); + + values.forEach((k) => { + result.delete(mapHashMapKey(k)); }); - return { tag: "MalHashMap", items: new Map(items) }; + return { tag: "MalHashMap", items: result }; }; -export const mapValues = (malMap: MalHashMap): Array<[MalType, MalType]> => - [...malMap.items].map(([k, v]) => - k.startsWith("s") ? [mkString(k.substr(1)), v] : [mkKeyword(k.substr(1)), v] - ); +export const mapGet = (malMap: MalHashMap, key: MalType): MalType => + malMap.items.get(mapHashMapKey(key)) ?? nil; + +export const mapContains = (malMap: MalHashMap, key: MalType): MalType => + mkBoolean(malMap.items.get(mapHashMapKey(key)) !== undefined); + +export const mapKeys = (malMap: MalHashMap): Array => + [...malMap.items].map(([k, _]) => reverseMapHashMapKey(k)); + +export const mapValues = (malMap: MalHashMap): Array => + [...malMap.items].map(([_, v]) => v); + +export const mapKeyValues = (malMap: MalHashMap): Array<[MalType, MalType]> => + [...malMap.items].map(([k, v]) => [reverseMapHashMapKey(k), v]); + +const mapHashMapKey = (k: MalType): string => { + if (k.tag === "MalString") { + return `s${k.value}`; + } else if (k.tag === "MalKeyword") { + return `t${k.name}`; + } else { + throw new Error( + `Precondition Error: Unable to use ${ + JSON.stringify(k) + } as a hashmap key.`, + ); + } +}; + +const reverseMapHashMapKey = (k: string): MalString | MalKeyword => + k.startsWith("s") ? mkString(k.substr(1)) : mkKeyword(k.substr(1)); export type MalNumber = { tag: "MalNumber"; @@ -104,10 +142,12 @@ export type MalBoolean = { value: boolean; }; -export const mkBoolean = (value: boolean): MalBoolean => ({ - tag: "MalBoolean", - value, -}); +const booleanTrue: MalBoolean = { tag: "MalBoolean", value: true }; + +const booleanFalse: MalBoolean = { tag: "MalBoolean", value: false }; + +export const mkBoolean = (value: boolean): MalBoolean => + value ? booleanTrue : booleanFalse; export type MalNil = { tag: "MalNil"; @@ -197,7 +237,7 @@ const hashMapEquals = (a: MalHashMap, b: MalHashMap): boolean => { return false; } - as.forEach((value, key) => { + for (let [key, value] of as) { if (!bs.has(key)) return false; const bValue = bs.get(key); @@ -205,7 +245,7 @@ const hashMapEquals = (a: MalHashMap, b: MalHashMap): boolean => { if (bValue === undefined || !equals(value, bValue)) { return false; } - }); + } return true; }; From d35daed2aece47a1abf994fb6fd867a8b690ac1a Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Sun, 3 Jan 2021 09:31:28 +0200 Subject: [PATCH 17/20] feature: step A metadata and self hosting --- impls/deno/core.ts | 145 +++++++++++ impls/deno/readline.ts | 28 +++ impls/deno/step0_repl.ts | 6 +- impls/deno/step1_read_print.ts | 8 +- impls/deno/step2_eval.ts | 8 +- impls/deno/step3_env.ts | 8 +- impls/deno/step4_if_fn_do.ts | 7 +- impls/deno/step5_tco.ts | 7 +- impls/deno/step6_file.ts | 7 +- impls/deno/step7_quote.ts | 8 +- impls/deno/step8_macros.ts | 7 +- impls/deno/step9_try.ts | 7 +- impls/deno/stepA_mal.ts | 428 +++++++++++++++++++++++++++++++++ impls/deno/types.ts | 178 ++++++++------ 14 files changed, 762 insertions(+), 90 deletions(-) create mode 100644 impls/deno/readline.ts create mode 100644 impls/deno/stepA_mal.ts diff --git a/impls/deno/core.ts b/impls/deno/core.ts index 6458647f00..09f64e9e35 100644 --- a/impls/deno/core.ts +++ b/impls/deno/core.ts @@ -2,6 +2,7 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; export const ns = ( evaluate: (ast: MalType.MalType, env: Env.Env) => MalType.MalType, @@ -527,6 +528,18 @@ export const ns = ( return MalType.mkBoolean(v !== undefined && v.tag === "MalKeyword"); }), ], + [ + "string?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean(v !== undefined && v.tag === "MalString"); + }), + ], + [ + "number?", + MalType.mkInternalFunction(([v]) => { + return MalType.mkBoolean(v !== undefined && v.tag === "MalNumber"); + }), + ], [ "vector", MalType.mkInternalFunction((lst) => { @@ -647,6 +660,138 @@ export const ns = ( return MalType.mkList(MalType.mapValues(m)); }), ], + + [ + "readline", + MalType.mkInternalFunction(([prompt]) => { + if (prompt === undefined || prompt.tag !== "MalString") { + throw new Error( + "Invalid Argument: readline parameter 0: expected a string", + ); + } + + const text = readline(`${prompt.value}> `); + + return text === undefined ? MalType.nil : MalType.mkString(text); + }), + ], + + [ + "time-ms", + MalType.mkInternalFunction((_) => MalType.mkNumber(performance.now())), + ], + [ + "meta", + MalType.mkInternalFunction(([v]) => { + if (v === undefined) { + return MalType.nil; + } else { + switch (v.tag) { + case "MalFunction": + case "MalHashMap": + case "MalInternalFunction": + case "MalList": + case "MalVector": + return v.meta ?? MalType.nil; + default: + return MalType.nil; + } + } + }), + ], + [ + "with-meta", + MalType.mkInternalFunction(([v, m]) => { + if (v === undefined || m === undefined) { + throw MalType.mkList( + [ + MalType.mkSymbol("IncorrectParameter"), + MalType.mkString("with-meta"), + ], + ); + } + return MalType.withMeta(v, m); + }), + ], + [ + "fn?", + MalType.mkInternalFunction(([v]) => + MalType.mkBoolean( + v !== undefined && + (v.tag === "MalFunction" && !v.isMacro || + v.tag === "MalInternalFunction"), + ) + ), + ], + [ + "macro?", + MalType.mkInternalFunction(([v]) => + MalType.mkBoolean(v !== undefined && v.tag === "MalFunction" && v.isMacro) + ), + ], + [ + "seq", + MalType.mkInternalFunction(([s]) => { + if (s === undefined) { + throw MalType.mkList( + [ + MalType.mkSymbol("IncorrectParameter"), + MalType.mkString("seq"), + ], + ); + } + + if (s.tag === "MalList") { + return s.items.length === 0 ? MalType.nil : s; + } else if (s.tag === "MalVector") { + return s.items.length === 0 ? MalType.nil : MalType.mkList(s.items); + } else if (s.tag === "MalNil") { + return s; + } else if (s.tag === "MalString") { + return s.value.length === 0 + ? MalType.nil + : MalType.mkList(s.value.split("").map((e) => MalType.mkString(e))); + } else { + throw MalType.mkList( + [ + MalType.mkSymbol("IncorrectParameter"), + MalType.mkString("seq"), + ], + ); + } + }), + ], + [ + "conj", + MalType.mkInternalFunction(([c, ...es]) => { + if (c === undefined) { + throw MalType.mkList( + [ + MalType.mkSymbol("IncorrectParameter"), + MalType.mkString("conj"), + ], + ); + } + if (es === undefined) { + return c; + } else if (c.tag === "MalList") { + return MalType.mkList([...es.reverse(), ...c.items]); + } else if (c.tag === "MalVector") { + return MalType.mkVector([...c.items, ...es]); + } else { + throw MalType.mkList( + [ + MalType.mkSymbol("IncorrectParameterType"), + MalType.mkString("conj"), + MalType.mkList( + [MalType.mkSymbol("list"), MalType.mkSymbol("vector")], + ), + MalType.mkString(c.tag), + ], + ); + } + }), + ], ]; const mkHashPairs = ( diff --git a/impls/deno/readline.ts b/impls/deno/readline.ts new file mode 100644 index 0000000000..05d4ac9213 --- /dev/null +++ b/impls/deno/readline.ts @@ -0,0 +1,28 @@ +export const readline = ( + pp: string | undefined = undefined, +): string | undefined => { + const result: Array = []; + + if (pp !== undefined) { + Deno.stdout.writeSync(new TextEncoder().encode(pp)); + } + + while (true) { + const buffer = new Uint8Array(16); + const uint8Read = Deno.stdin.readSync(buffer); + + if (uint8Read == null) { + return undefined; + } else { + const decoder = new TextDecoder(); + const text = decoder.decode(buffer).substr(0, uint8Read); + + if (text.endsWith("\n")) { + result.push(text.substr(0, text.length - 1)); + return result.join(""); + } else { + result.push(text); + } + } + } +}; diff --git a/impls/deno/step0_repl.ts b/impls/deno/step0_repl.ts index ad4fa1dcf7..bed7b84b92 100644 --- a/impls/deno/step0_repl.ts +++ b/impls/deno/step0_repl.ts @@ -1,3 +1,5 @@ +import { readline } from "./readline.ts"; + const read = (str: string): any => str; const eval_ = (ast: any): any => ast; @@ -7,9 +9,9 @@ const print = (exp: any): string => exp.toString(); const rep = (str: string): string => print(eval_(read(str))); while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; } diff --git a/impls/deno/step1_read_print.ts b/impls/deno/step1_read_print.ts index 05c96570c0..84a0210166 100644 --- a/impls/deno/step1_read_print.ts +++ b/impls/deno/step1_read_print.ts @@ -1,6 +1,8 @@ import * as Reader from "./reader.ts"; import * as Printer from "./printer.ts"; import { MalType } from "./types.ts"; +import { readline } from "./readline.ts"; + const read = (str: string): MalType => Reader.readStr(str); @@ -11,10 +13,12 @@ const print = (exp: MalType): string => Printer.prStr(exp); const rep = (str: string): string => print(eval_(read(str))); while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/step2_eval.ts b/impls/deno/step2_eval.ts index e354ac8474..954348bf33 100644 --- a/impls/deno/step2_eval.ts +++ b/impls/deno/step2_eval.ts @@ -2,6 +2,8 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; + const replEnv = Env.mkEnv(); @@ -96,10 +98,12 @@ const rep = (str: string): string => print(evaluate(read(str), replEnv)); type FunctionType = (a: MalType.MalType, b: MalType.MalType) => MalType.MalType; while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/step3_env.ts b/impls/deno/step3_env.ts index 16266b7a3b..3fb57bb94d 100644 --- a/impls/deno/step3_env.ts +++ b/impls/deno/step3_env.ts @@ -2,6 +2,8 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; + const replEnv = Env.mkEnv(); @@ -152,10 +154,12 @@ const print = (exp: MalType.MalType): string => Printer.prStr(exp); const rep = (str: string): string => print(evaluate(read(str), replEnv)); while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/step4_if_fn_do.ts b/impls/deno/step4_if_fn_do.ts index 0ca2053311..25a2a16ae0 100644 --- a/impls/deno/step4_if_fn_do.ts +++ b/impls/deno/step4_if_fn_do.ts @@ -3,6 +3,7 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; const read = (str: string): MalType.MalType => Reader.readStr(str); @@ -226,10 +227,12 @@ const repl = () => { const env = initReplEnv(); while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/step5_tco.ts b/impls/deno/step5_tco.ts index c227fa370c..279676482d 100644 --- a/impls/deno/step5_tco.ts +++ b/impls/deno/step5_tco.ts @@ -3,6 +3,7 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; const read = (str: string): MalType.MalType => Reader.readStr(str); @@ -207,10 +208,12 @@ const repl = () => { const env = initReplEnv(); while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/step6_file.ts b/impls/deno/step6_file.ts index 07f6d0e027..2556d4f3e6 100644 --- a/impls/deno/step6_file.ts +++ b/impls/deno/step6_file.ts @@ -3,6 +3,7 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; const read = (str: string): MalType.MalType => Reader.readStr(str); @@ -223,10 +224,12 @@ const initReplEnv = () => { const repl = (env: Env.Env) => { while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/step7_quote.ts b/impls/deno/step7_quote.ts index cf76cc3aa3..b0d3f9c4a5 100644 --- a/impls/deno/step7_quote.ts +++ b/impls/deno/step7_quote.ts @@ -3,6 +3,8 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; + const read = (str: string): MalType.MalType => Reader.readStr(str); @@ -271,10 +273,12 @@ const initReplEnv = () => { const repl = (env: Env.Env) => { while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/step8_macros.ts b/impls/deno/step8_macros.ts index f623ccc7e1..082870219e 100644 --- a/impls/deno/step8_macros.ts +++ b/impls/deno/step8_macros.ts @@ -3,6 +3,7 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; const read = (str: string): MalType.MalType => Reader.readStr(str); @@ -333,10 +334,12 @@ const initReplEnv = () => { const repl = (env: Env.Env) => { while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/step9_try.ts b/impls/deno/step9_try.ts index 98f5fd1f26..e7138fcdf9 100644 --- a/impls/deno/step9_try.ts +++ b/impls/deno/step9_try.ts @@ -3,6 +3,7 @@ import * as Env from "./env.ts"; import * as MalType from "./types.ts"; import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; const read = (str: string): MalType.MalType => Reader.readStr(str); @@ -381,10 +382,12 @@ const initReplEnv = () => { const repl = (env: Env.Env) => { while (true) { - const value = prompt("user>"); + const value = readline("user> "); - if (value === null) { + if (value === undefined) { break; + } else if (value === "") { + continue; } try { diff --git a/impls/deno/stepA_mal.ts b/impls/deno/stepA_mal.ts new file mode 100644 index 0000000000..f9c14e50db --- /dev/null +++ b/impls/deno/stepA_mal.ts @@ -0,0 +1,428 @@ +import * as Core from "./core.ts"; +import * as Env from "./env.ts"; +import * as MalType from "./types.ts"; +import * as Printer from "./printer.ts"; +import * as Reader from "./reader.ts"; +import { readline } from "./readline.ts"; + +const read = (str: string): MalType.MalType => Reader.readStr(str); + +const evaluate_ast = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + switch (ast.tag) { + case "MalSymbol": + return Env.get(ast, env); + case "MalList": + return MalType.mkList(ast.items.map((i) => evaluate(i, env))); + case "MalVector": + return MalType.mkVector(ast.items.map((i) => evaluate(i, env))); + case "MalHashMap": + return MalType.mkHashMap( + MalType.mapKeyValues(ast).map(([k, v]) => [k, evaluate(v, env)]), + ); + default: + return ast; + } +}; + +const evaluate = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + while (true) { + // console.log(Printer.prStr(ast)); + + ast = macroExpand(ast, env); + if (ast.tag === "MalList") { + if (ast.items.length === 0) { + return ast; + } + + if (ast.items[0].tag === "MalSymbol") { + switch (ast.items[0].name) { + case "def!": + return evaluateDefBang(ast, env); + case "defmacro!": + return evaluateDefMacroBang(ast, env); + case "do": + ast = evaluateDo(ast, env); + continue; + case "fn*": + return evaluateFnStar(ast, env); + case "if": + ast = evaluateIf(ast, env); + continue; + case "let*": { + const bindings = ast.items[1]; + + if (bindings.tag !== "MalList" && bindings.tag !== "MalVector") { + throw new Error( + `Invalid Argument: let* requires a list of bindings: ${ + JSON.stringify(bindings) + }`, + ); + } + + const innerEnv = Env.mkEnv(env); + for (let lp = 0; lp < bindings.items.length; lp += 2) { + const name = bindings.items[lp]; + const value = bindings.items[lp + 1] ?? MalType.nil; + + if (name.tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: let* binding requires a symbol name: ${ + JSON.stringify(name) + }`, + ); + } + + Env.set(name, evaluate(value, innerEnv), innerEnv); + } + + ast = ast.items[2]; + env = innerEnv; + continue; + } + case "macroexpand": + return macroExpand(ast.items[1], env); + case "quasiquote": + ast = evaluateQuasiQuote(ast.items[1]); + continue; + case "quasiquoteexpand": + return evaluateQuasiQuote(ast.items[1]); + case "quote": + return ast.items[1]; + case "try*": { + if (ast.items.length === 2) { + return evaluate(ast.items[1], env); + } + + if (ast.items.length === 3) { + const catchItem = ast.items[2]; + + if ( + (catchItem.tag === "MalList" || + catchItem.tag === "MalVector") && catchItem.items.length === 3 + ) { + const catchKeyword = catchItem.items[0]; + const catchSymbol = catchItem.items[1]; + const catchBody = catchItem.items[2]; + if ( + catchKeyword.tag === "MalSymbol" && + catchKeyword.name === "catch*" && + catchSymbol.tag === "MalSymbol" + ) { + try { + return evaluate(ast.items[1], env); + } catch (e) { + const ep = e.tag !== undefined && e.tag.startsWith("Mal") + ? e + : e.message !== undefined + ? MalType.mkString(e.message) + : MalType.mkString(JSON.stringify(e)); + + env = Env.mkEnv( + env, + [catchSymbol], + [ep], + ); + ast = catchBody; + continue; + } + } + } + } + throw new Error( + `Invalid Argument: try* is not in a valid form: ${ + JSON.stringify(ast) + }`, + ); + } + } + } + + const evalList = evaluate_ast(ast, env); + + if (evalList.tag === "MalList" || evalList.tag === "MalVector") { + const [callerItem, ...callerArgs] = evalList.items; + + if (callerItem !== undefined) { + if (callerItem.tag === "MalInternalFunction") { + return callerItem.fn(callerArgs); + } else if (callerItem.tag === "MalFunction") { + ast = callerItem.body; + env = Env.mkEnv(callerItem.env, callerItem.params, callerArgs); + continue; + } + } + } + + throw new Error(`Unable to invoke: ${JSON.stringify(evalList)}`); + } else { + return evaluate_ast(ast, env); + } + } +}; + +const macroExpand = (ast: MalType.MalType, env: Env.Env): MalType.MalType => { + const macroFunction = ( + ast: MalType.MalType, + env: Env.Env, + ): [MalType.MalFunction, Array] | undefined => { + if ( + (ast.tag === "MalList" || ast.tag === "MalVector") && + ast.items.length > 0 && ast.items[0].tag === "MalSymbol" + ) { + const value = Env.find(ast.items[0], env); + + return value !== undefined && value.tag === "MalFunction" && value.isMacro + ? [value, ast.items.slice(1)] + : undefined; + } else { + return undefined; + } + }; + + while (true) { + const macro = macroFunction(ast, env); + if (macro === undefined) { + return ast; + } else { + ast = evaluate( + macro[0].body, + Env.mkEnv(macro[0].env, macro[0].params, macro[1]), + ); + } + } +}; + +const evaluateDefBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: def! requires a symbol name: ${ast.items[1]}`, + ); + } + + const result = evaluate(ast.items[2], env); + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDefMacroBang = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items[1].tag !== "MalSymbol") { + throw new Error( + `Invalid Argument: defmacro! requires a symbol name: ${ast.items[1]}`, + ); + } + + let result = evaluate(ast.items[2], env); + + if (result.tag === "MalFunction") { + result = MalType.mkFunction( + result.body, + result.params, + result.env, + true, + result.meta, + ); + } + + Env.set(ast.items[1], result, env); + + return result; +}; + +const evaluateDo = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + evaluate_ast(MalType.mkList(ast.items.slice(1, ast.items.length - 1)), env); + return ast.items[ast.items.length - 1]; +}; + +const evaluateFnStar = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + const [_, parameters, body] = ast.items; + + if ( + parameters == undefined || + parameters.tag !== "MalList" && parameters.tag !== "MalVector" + ) { + throw new Error("Invalid Argument: fn* expects a list of parameters"); + } + if (body === undefined) { + throw new Error("Invalid Argument: fn* expects a body"); + } + + const formalParameters: Array = []; + parameters.items.forEach((p, idx) => { + if (p.tag === "MalSymbol") { + formalParameters.push(p); + } else { + throw new Error( + `Invalid Argument: Parameter ${idx + 1} in fn* is not a symbol: ${ + JSON.stringify(p) + }`, + ); + } + }); + + return MalType.mkFunction(body, formalParameters, env); +}; + +const evaluateIf = ( + ast: MalType.MalList, + env: Env.Env, +): MalType.MalType => { + if (ast.items.length < 3) { + throw new Error( + `Invalid Argument: if expects at least 3 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + if (ast.items.length > 4) { + throw new Error( + `Invalid Argument: if expects no more than 4 arguments: ${ + JSON.stringify(ast) + }`, + ); + } + + const ifGuard = evaluate(ast.items[1], env); + + if ( + ifGuard.tag === "MalNil" || ifGuard.tag === "MalBoolean" && !ifGuard.value + ) { + return ast.items.length === 4 ? ast.items[3] : MalType.nil; + } else { + return ast.items[2]; + } +}; + +const evaluateQuasiQuote = (ast: MalType.MalType): MalType.MalType => { + const startsWith = ( + items: Array, + symbolName: string, + ): boolean => (items.length == 2 && items[0].tag === "MalSymbol" && + items[0].name === symbolName); + + const loop = ( + element: MalType.MalType, + accumulator: MalType.MalList, + ): MalType.MalList => + (element.tag == "MalList" && startsWith(element.items, "splice-unquote")) + ? MalType.mkList( + [MalType.mkSymbol("concat"), element.items[1], accumulator], + ) + : MalType.mkList( + [MalType.mkSymbol("cons"), evaluateQuasiQuote(element), accumulator], + ); + + const foldr = (xs: MalType.MalType[]): MalType.MalList => { + let acc = MalType.mkList([]); + for (let i = xs.length - 1; i >= 0; i -= 1) { + acc = loop(xs[i], acc); + } + return acc; + }; + + switch (ast.tag) { + case "MalSymbol": + return MalType.mkList([MalType.mkSymbol("quote"), ast]); + case "MalHashMap": + return MalType.mkList([MalType.mkSymbol("quote"), ast]); + case "MalList": + return (startsWith(ast.items, "unquote")) + ? ast.items[1] + : foldr(ast.items); + case "MalVector": + return MalType.mkList([MalType.mkSymbol("vec"), foldr(ast.items)]); + default: + return ast; + } +}; + +const print = (exp: MalType.MalType): string => Printer.prStr(exp); + +const rep = (str: string, env: Env.Env): string => + print(evaluate(read(str), env)); + +const initReplEnv = () => { + const ns = Core.ns(evaluate); + + const env = Env.mkEnv( + undefined, + ns.map(([n]) => MalType.mkSymbol(n)), + ns.map(([, t]) => t), + ); + + Env.set( + MalType.mkSymbol("eval"), + MalType.mkInternalFunction(([a]) => { + if (a === undefined) { + return MalType.nil; + } + return evaluate(a, env); + }), + env, + ); + + rep("(def! not (fn* (a) (if a false true)))", env); + rep( + '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', + env, + ); + rep( + "(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)))))))", + env, + ); + rep('(def! *host-language* "deno")', env); + + return env; +}; + +const repl = (env: Env.Env) => { + while (true) { + const prompt = "user> "; + const value = readline(prompt); + + if (value === undefined) { + break; + } else if (value === "") { + continue; + } + + try { + console.log(rep(value, env)); + } catch (e) { + if (e.message !== "Reader Error: No input") { + const message = (e.tag !== undefined) ? Printer.prStr(e) : e.message; + console.error(`Exception: ${message}`); + } + } + } +}; + +if (Deno.args.length > 0) { + const env = initReplEnv(); + + Env.set( + MalType.mkSymbol("*ARGV*"), + MalType.mkList(Deno.args.slice(1).map(MalType.mkString)), + env, + ); + + rep(`(load-file "${Deno.args[0]}")`, env); +} else { + const env = initReplEnv(); + Env.set(MalType.mkSymbol("*ARGV*"), MalType.mkList([]), env); + + rep(`(println (str "Mal [" *host-language* "]"))`, env); + repl(env); +} diff --git a/impls/deno/types.ts b/impls/deno/types.ts index 707ded8534..dd9c5b8144 100644 --- a/impls/deno/types.ts +++ b/impls/deno/types.ts @@ -21,34 +21,55 @@ export type MalAtom = { export const mkAtom = (value: MalType): MalAtom => ({ tag: "MalAtom", value }); -export type MalList = { - tag: "MalList"; - items: Array; +export type MalBoolean = { + tag: "MalBoolean"; + value: boolean; }; -export const mkList = (items: Array): MalList => ({ - tag: "MalList", - items, -}); +const booleanTrue: MalBoolean = { tag: "MalBoolean", value: true }; -export type MalVector = { - tag: "MalVector"; - items: Array; +const booleanFalse: MalBoolean = { tag: "MalBoolean", value: false }; + +export const mkBoolean = (value: boolean): MalBoolean => + value ? booleanTrue : booleanFalse; + +export type MalFunction = { + tag: "MalFunction"; + body: MalType; + params: Array; + env: Env.Env; + isMacro: boolean; + meta?: MalType; }; -export const mkVector = (items: Array): MalVector => ({ - tag: "MalVector", - items, +export const mkFunction = ( + body: MalType, + params: Array, + env: Env.Env, + isMacro: boolean = false, + meta?: MalType, +): MalFunction => ({ + tag: "MalFunction", + body, + params, + env, + isMacro, + meta, }); export type MalHashMap = { tag: "MalHashMap"; items: Map; + meta?: MalType; }; -export const mkHashMap = (values: Array<[MalType, MalType]>): MalHashMap => ({ +export const mkHashMap = ( + values: Array<[MalType, MalType]>, + meta?: MalType, +): MalHashMap => ({ tag: "MalHashMap", items: new Map(values.map(([k, v]) => [mapHashMapKey(k), v])), + meta, }); export const mapAssoc = ( @@ -61,7 +82,7 @@ export const mapAssoc = ( result.set(mapHashMapKey(k), v); }); - return { tag: "MalHashMap", items: result }; + return { tag: "MalHashMap", items: result, meta: malMap.meta }; }; export const mapDissoc = ( @@ -74,7 +95,7 @@ export const mapDissoc = ( result.delete(mapHashMapKey(k)); }); - return { tag: "MalHashMap", items: result }; + return { tag: "MalHashMap", items: result, meta: malMap.meta }; }; export const mapGet = (malMap: MalHashMap, key: MalType): MalType => @@ -109,6 +130,45 @@ const mapHashMapKey = (k: MalType): string => { const reverseMapHashMapKey = (k: string): MalString | MalKeyword => k.startsWith("s") ? mkString(k.substr(1)) : mkKeyword(k.substr(1)); +export type MalInternalFunction = { + tag: "MalInternalFunction"; + fn: (args: Array) => MalType; + meta?: MalType; +}; + +export const mkInternalFunction = ( + fn: (args: Array) => MalType, + meta?: MalType, +): MalInternalFunction => ({ tag: "MalInternalFunction", fn, meta }); + +export type MalKeyword = { + tag: "MalKeyword"; + name: string; +}; + +export const mkKeyword = (name: string): MalKeyword => ({ + tag: "MalKeyword", + name, +}); + +export type MalList = { + tag: "MalList"; + items: Array; + meta?: MalType; +}; + +export const mkList = (items: Array, meta?: MalType): MalList => ({ + tag: "MalList", + items, + meta, +}); + +export type MalNil = { + tag: "MalNil"; +}; + +export const nil: MalNil = ({ tag: "MalNil" }); + export type MalNumber = { tag: "MalNumber"; value: number; @@ -137,24 +197,6 @@ export const mkString = (value: string): MalString => ({ value, }); -export type MalBoolean = { - tag: "MalBoolean"; - value: boolean; -}; - -const booleanTrue: MalBoolean = { tag: "MalBoolean", value: true }; - -const booleanFalse: MalBoolean = { tag: "MalBoolean", value: false }; - -export const mkBoolean = (value: boolean): MalBoolean => - value ? booleanTrue : booleanFalse; - -export type MalNil = { - tag: "MalNil"; -}; - -export const nil: MalNil = ({ tag: "MalNil" }); - export type MalSymbol = { tag: "MalSymbol"; name: string; @@ -165,44 +207,16 @@ export const mkSymbol = (name: string): MalSymbol => ({ name, }); -export type MalKeyword = { - tag: "MalKeyword"; - name: string; -}; - -export const mkKeyword = (name: string): MalKeyword => ({ - tag: "MalKeyword", - name, -}); - -export type MalInternalFunction = { - tag: "MalInternalFunction"; - fn: (args: Array) => MalType; -}; - -export const mkInternalFunction = ( - fn: (args: Array) => MalType, -): MalInternalFunction => ({ tag: "MalInternalFunction", fn }); - -export type MalFunction = { - tag: "MalFunction"; - body: MalType; - params: Array; - env: Env.Env; - isMacro: boolean; +export type MalVector = { + tag: "MalVector"; + items: Array; + meta?: MalType; }; -export const mkFunction = ( - body: MalType, - params: Array, - env: Env.Env, - isMacro: boolean = false, -): MalFunction => ({ - tag: "MalFunction", - body, - params, - env, - isMacro, +export const mkVector = (items: Array, meta?: MalType): MalVector => ({ + tag: "MalVector", + items, + meta, }); export const equals = (a: MalType, b: MalType): boolean => { @@ -269,3 +283,27 @@ const seqEquals = ( return true; }; + +export const withMeta = ( + v: MalType, + m: MalType, +): MalType => { + switch (v.tag) { + case "MalFunction": + return mkFunction(v.body, v.params, v.env, v.isMacro, m); + case "MalHashMap": + return { + tag: "MalHashMap", + items: new Map(v.items), + meta: m, + }; + case "MalInternalFunction": + return mkInternalFunction(v.fn, m); + case "MalList": + return mkList(v.items, m); + case "MalVector": + return mkVector(v.items, m); + default: + return v; + } +}; From c52da1b9424c58eb1c1f205a118198e4595fdec6 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Sun, 3 Jan 2021 13:13:48 +0200 Subject: [PATCH 18/20] refactor: simplify the handling of core parameters --- impls/deno/core.ts | 1271 ++++++++++++++++++-------------------------- 1 file changed, 505 insertions(+), 766 deletions(-) diff --git a/impls/deno/core.ts b/impls/deno/core.ts index 09f64e9e35..1dde656633 100644 --- a/impls/deno/core.ts +++ b/impls/deno/core.ts @@ -4,810 +4,549 @@ import * as Printer from "./printer.ts"; import * as Reader from "./reader.ts"; import { readline } from "./readline.ts"; -export const ns = ( +const mkHashPairs = ( + items: Array, + procedureName: string, +): Array<[MalType.MalType, MalType.MalType]> => { + const args: Array<[MalType.MalType, MalType.MalType]> = []; + + if (items.length % 2 === 1) { + throw new Error( + `Invalid Argument: ${procedureName}: Odd number of arguments`, + ); + } + for (let lp = 0; lp < items.length; lp += 2) { + args.push([items[lp], items[lp + 1]]); + } + + return args; +}; + +const validateArgument = ( + o: number, + v: MalType.MalType | undefined, + tags: Array | string | undefined = undefined, +) => { + if (v === undefined) { + throw { error: "UndefinedParameter", parameter: o, tags }; + } + + if (tags !== undefined) { + if (Array.isArray(tags)) { + if (!tags.includes(v.tag)) { + throw { error: "InvalidTag", parameter: o, value: v, tags }; + } + } else if (tags !== v.tag) { + throw { error: "InvalidTag", parameter: o, value: v, tags: [tags] }; + } + } +}; + +const __ns = ( evaluate: (ast: MalType.MalType, env: Env.Env) => MalType.MalType, -): Array<[string, MalType.MalType]> => [ - [ - "+", - MalType.mkInternalFunction(([a, b]) => - MalType.mkNumber(MalType.asNumber(a) + MalType.asNumber(b)) - ), - ], - [ - "-", - MalType.mkInternalFunction(([a, b]) => - MalType.mkNumber(MalType.asNumber(a) - MalType.asNumber(b)) - ), - ], - [ - "*", - MalType.mkInternalFunction(([a, b]) => - MalType.mkNumber(MalType.asNumber(a) * MalType.asNumber(b)) - ), - ], - [ - "/", - MalType.mkInternalFunction(([a, b]) => - MalType.mkNumber(MalType.asNumber(a) / MalType.asNumber(b)) - ), - ], +) => ({ + "+": ([a, b]: Array): MalType.MalType => { + validateArgument(0, a, "MalNumber"); + validateArgument(1, b, "MalNumber"); - [ - "pr_str", - MalType.mkInternalFunction(([v]) => { - console.log(Printer.prStr(v, true)); - return MalType.nil; - }), - ], - [ - "println", - MalType.mkInternalFunction((args) => { - console.log(args.map((v) => Printer.prStr(v, false)).join(" ")); - return MalType.nil; - }), - ], - [ - "prn", - MalType.mkInternalFunction((args) => { - console.log(args.map((v) => Printer.prStr(v, true)).join(" ")); + return MalType.mkNumber(asNumber(a) + asNumber(b)); + }, + + "-": ([a, b]: Array): MalType.MalType => { + validateArgument(0, a, "MalNumber"); + validateArgument(1, b, "MalNumber"); + + return MalType.mkNumber(asNumber(a) - asNumber(b)); + }, + + "*": ([a, b]: Array): MalType.MalType => { + validateArgument(0, a, "MalNumber"); + validateArgument(1, b, "MalNumber"); + + return MalType.mkNumber(asNumber(a) * asNumber(b)); + }, + + "/": ([a, b]: Array): MalType.MalType => { + validateArgument(0, a, "MalNumber"); + validateArgument(1, b, "MalNumber"); + + return MalType.mkNumber(asNumber(a) / asNumber(b)); + }, + + "pr_str": ([v]: Array): MalType.MalType => { + console.log(Printer.prStr(v, true)); + return MalType.nil; + }, + + println: (args: Array): MalType.MalType => { + console.log(args.map((v) => Printer.prStr(v, false)).join(" ")); + return MalType.nil; + }, + + prn: (args: Array): MalType.MalType => { + console.log(args.map((v) => Printer.prStr(v, true)).join(" ")); + return MalType.nil; + }, + + "pr-str": (args: Array): MalType.MalType => + MalType.mkString(args.map((v) => Printer.prStr(v, true)).join(" ")), + + str: (args: Array): MalType.MalType => + MalType.mkString(args.map((v) => Printer.prStr(v, false)).join("")), + + list: MalType.mkList, + + "list?": ([v]: Array): MalType.MalType => { + validateArgument(0, v); + + return MalType.mkBoolean(v.tag === "MalList"); + }, + + "empty?": ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector"]); + + return MalType.mkBoolean(asSeq(v).items.length === 0); + }, + + count: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector", "MalNil"]); + + return (v.tag === "MalNil") + ? MalType.mkNumber(0) + : MalType.mkNumber(asSeq(v).items.length); + }, + + "=": ([a, b]: Array): MalType.MalType => + MalType.mkBoolean(MalType.equals(a, b)), + + "<": ([a, b]: Array): MalType.MalType => { + validateArgument(0, a, "MalNumber"); + validateArgument(1, b, "MalNumber"); + + return MalType.mkBoolean(asNumber(a) < asNumber(b)); + }, + + "<=": ([a, b]: Array): MalType.MalType => { + validateArgument(0, a, "MalNumber"); + validateArgument(1, b, "MalNumber"); + + return MalType.mkBoolean(asNumber(a) <= asNumber(b)); + }, + + ">": ([a, b]: Array): MalType.MalType => { + validateArgument(0, a, "MalNumber"); + validateArgument(1, b, "MalNumber"); + + return MalType.mkBoolean(asNumber(a) > asNumber(b)); + }, + + ">=": ([a, b]: Array): MalType.MalType => { + validateArgument(0, a, "MalNumber"); + validateArgument(1, b, "MalNumber"); + + return MalType.mkBoolean(asNumber(a) >= asNumber(b)); + }, + + "read-string": ([s]: Array): MalType.MalType => { + if (s === undefined) { return MalType.nil; - }), - ], - [ - "pr-str", - MalType.mkInternalFunction((args) => - MalType.mkString(args.map((v) => Printer.prStr(v, true)).join(" ")) - ), - ], - [ - "str", - MalType.mkInternalFunction((args) => - MalType.mkString(args.map((v) => Printer.prStr(v, false)).join("")) - ), - ], - ["list", MalType.mkInternalFunction((v) => MalType.mkList(v))], - [ - "list?", - MalType.mkInternalFunction(([v]) => { - if (v === undefined) { - throw new Error("Illegal Argument: list? requires a parameter"); - } - return MalType.mkBoolean(v.tag === "MalList"); - }), - ], - [ - "empty?", - MalType.mkInternalFunction(([v]) => { - if (v === undefined || v.tag !== "MalList" && v.tag !== "MalVector") { - throw new Error( - "Illegal Argument: empty? requires a list or vector as parameter", - ); - } - return MalType.mkBoolean(v.items.length === 0); - }), - ], - [ - "count", - MalType.mkInternalFunction(([v]) => { - if (v.tag === "MalNil") { - return MalType.mkNumber(0); - } + } - if (v.tag !== "MalList" && v.tag !== "MalVector") { - throw new Error( - "Illegal Argument: count requires a list or vector parameter", - ); - } - return MalType.mkNumber(v.items.length); - }), - ], - - [ - "=", - MalType.mkInternalFunction(([a, b]) => - MalType.mkBoolean(MalType.equals(a, b)) - ), - ], - [ - "<", - MalType.mkInternalFunction(([a, b]) => - MalType.mkBoolean(MalType.asNumber(a) < MalType.asNumber(b)) - ), - ], - [ - "<=", - MalType.mkInternalFunction(([a, b]) => - MalType.mkBoolean(MalType.asNumber(a) <= MalType.asNumber(b)) - ), - ], - [ - ">", - MalType.mkInternalFunction(([a, b]) => - MalType.mkBoolean(MalType.asNumber(a) > MalType.asNumber(b)) - ), - ], - [ - ">=", - MalType.mkInternalFunction(([a, b]) => - MalType.mkBoolean(MalType.asNumber(a) >= MalType.asNumber(b)) - ), - ], - - [ - "read-string", - MalType.mkInternalFunction(([s]) => { - if (s === undefined) { - return MalType.nil; - } else if (s.tag === "MalString") { - return Reader.readStr(s.value); - } else { - throw new Error( - `Invalid Argument: read-string requires a string argument: ${ - JSON.stringify(s) - }`, - ); - } - }), - ], - [ - "slurp", - MalType.mkInternalFunction(([s]) => { - if (s === undefined) { - throw new Error( - `Invalid Argument: slurp requires a single string argument`, - ); - } else if (s.tag === "MalString") { - return MalType.mkString(Deno.readTextFileSync(s.value)); - } else { - throw new Error( - `Invalid Argument: slurp requires a string argument: ${ - JSON.stringify(s) - }`, - ); - } - }), - ], - - [ - "atom", - MalType.mkInternalFunction(([v]) => { - return MalType.mkAtom(v ?? MalType.nil); - }), - ], - [ - "atom?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean(v !== undefined && v.tag === "MalAtom"); - }), - ], - [ - "deref", - MalType.mkInternalFunction(([v]) => { - if (v === undefined) { - throw new Error( - `Invalid Argument: deref requires a single atom argument`, - ); - } else if (v.tag === "MalAtom") { - return v.value; - } else { - throw new Error( - `Invalid Argument: deref requires an atom argument: ${ - JSON.stringify(v) - }`, - ); - } - }), - ], - [ - "reset!", - MalType.mkInternalFunction(([a, v]) => { - if (a === undefined) { - throw new Error(`Invalid Argument: reset! requires an atom argument`); - } else if (a.tag !== "MalAtom") { - throw new Error( - `Invalid Argument: reset! requires an atom argument: ${ - JSON.stringify(a) - }`, - ); - } - a.value = v ?? MalType.nil; - - return a.value; - }), - ], - [ - "swap!", - MalType.mkInternalFunction(([a, f, ...args]) => { - if (a === undefined) { - throw new Error(`Invalid Argument: swap! requires an atom argument`); - } else if (a.tag !== "MalAtom") { - throw new Error( - `Invalid Argument: swap! requires an atom argument: ${ - JSON.stringify(a) - }`, - ); - } + validateArgument(0, s, "MalString"); - if (f === undefined) { - throw new Error( - `Invalid Argument: swap! requires an function argument`, - ); - } else if (f.tag !== "MalFunction" && f.tag !== "MalInternalFunction") { - throw new Error( - `Invalid Argument: swap! requires a function argument: ${ - JSON.stringify(a) - }`, - ); - } + return Reader.readStr(asString(s)); + }, - args = [a.value, ...(args ?? [])]; + slurp: ([s]: Array): MalType.MalType => { + validateArgument(0, s, "MalString"); - if (f.tag === "MalFunction") { - a.value = evaluate(f.body, Env.mkEnv(f.env, f.params, args)); - } else { - a.value = f.fn(args); - } + return MalType.mkString(Deno.readTextFileSync(asString(s))); + }, - return a.value; - }), - ], + atom: ([v]: Array): MalType.MalType => + MalType.mkAtom(v ?? MalType.nil), - [ - "cons", - MalType.mkInternalFunction(([a, b]) => { - if (a === undefined || b === undefined) { - throw new Error(`Invalid Argument: cons requires two arguments`); - } + "atom?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalAtom"), - if (b.tag === "MalNil") { - return MalType.mkList([a]); - } else if (b.tag !== "MalList" && b.tag !== "MalVector") { - throw new Error( - `Invalid Argument: cons second argument must be a list: ${ - JSON.stringify(b) - }`, - ); - } + deref: ([v]: Array): MalType.MalType => { + validateArgument(0, v, "MalAtom"); - return MalType.mkList([a, ...b.items]); - }), - ], - [ - "concat", - MalType.mkInternalFunction((lst) => { - const result: Array = []; - - lst.forEach((i) => { - if (i.tag === "MalNil") { - // do nothing - } else if (i.tag === "MalList" || i.tag === "MalVector") { - i.items.forEach((e) => result.push(e)); - } else { - throw new Error( - `Invalid Argument: concat argument must be a list: ${ - JSON.stringify(i) - }`, - ); - } - }); - - return MalType.mkList(result); - }), - ], - [ - "vec", - MalType.mkInternalFunction(([v]) => { - if (v === undefined) { - throw new Error("Invalid Argument: vec requires a single argument"); - } + return asAtom(v).value; + }, - if (v.tag === "MalVector") { - return v; - } else if (v.tag === "MalList") { - return MalType.mkVector(v.items); - } else { - throw new Error( - `Invalid Argument: vec requires a single list or vector argument: ${ - JSON.stringify(v) - }`, - ); - } - }), - ], - [ - "nth", - MalType.mkInternalFunction(([l, i]) => { - if (l === undefined || l.tag !== "MalList" && l.tag !== "MalVector") { - throw new Error( - `Invalid Argument: nth parameter 0: expected list or vector: ${ - JSON.stringify(i) - }`, - ); - } - if (i === undefined || i.tag !== "MalNumber") { - throw new Error( - `Invalid Argument: nth parameter 1: expected number: ${ - JSON.stringify(i) - }`, - ); - } + "reset!": ([a, v]: Array): MalType.MalType => { + validateArgument(0, a, "MalAtom"); - const result = l.items[i.value]; - if (result === undefined) { - throw new Error( - `Index Out Of Range: nth: ${i.value} exceeds bounds of ${ - JSON.stringify(l) - }`, - ); - } else { - return result; - } - }), - ], - [ - "first", - MalType.mkInternalFunction(([v]) => { - if ( - v === undefined || - v.tag !== "MalList" && v.tag !== "MalVector" && v.tag !== "MalNil" - ) { - throw new Error( - "Invalid Argument: first parameter 0: expected list, vector or nil", - ); - } + asAtom(a).value = v ?? MalType.nil; - return v.tag === "MalNil" ? MalType.nil : v.items[0] ?? MalType.nil; - }), - ], - [ - "rest", - MalType.mkInternalFunction(([v]) => { - if ( - v === undefined || - v.tag !== "MalList" && v.tag !== "MalVector" && v.tag !== "MalNil" - ) { - throw new Error( - "Invalid Argument: rest parameter 0: expected list, vector or nil", - ); - } + return asAtom(a).value; + }, - return v.tag === "MalNil" - ? MalType.mkList([]) - : MalType.mkList(v.items.slice(1)); - }), - ], - - [ - "throw", - MalType.mkInternalFunction(([v]) => { - if ( - v === undefined - ) { - throw new Error( - "Invalid Argument: throw parameter 0: expected", - ); - } + "swap!": ([a, f, ...args]: Array): MalType.MalType => { + validateArgument(0, a, "MalAtom"); + validateArgument(1, f, ["MalFunction", "MalInternalFunction"]); - throw v; - }), - ], - - [ - "apply", - MalType.mkInternalFunction(([f, ...rest]) => { - if ( - f === undefined || - f.tag !== "MalInternalFunction" && f.tag !== "MalFunction" - ) { - throw new Error( - "Invalid Argument: apply parameter 0: expected function", - ); - } + const ap = asAtom(a); - const items: Array = []; + args = [ap.value, ...(args ?? [])]; - rest.forEach((v) => { - if (v.tag === "MalList" || v.tag == "MalVector") { - v.items.forEach((i) => items.push(i)); - } else { - items.push(v); - } - }); + if (f.tag === "MalFunction") { + ap.value = evaluate(f.body, Env.mkEnv(f.env, f.params, args)); + } else if (f.tag === "MalInternalFunction") { + ap.value = f.fn(args); + } - if (f.tag === "MalInternalFunction") { - return f.fn(items); - } else { - const ast = f.body; - const env = Env.mkEnv(f.env, f.params, items); - return evaluate(ast, env); - } - }), - ], - [ - "map", - MalType.mkInternalFunction(([f, seq]) => { - if ( - f === undefined || - f.tag !== "MalInternalFunction" && f.tag !== "MalFunction" - ) { - throw new Error( - "Invalid Argument: map parameter 0: expected function", - ); - } - if ( - seq === undefined || seq.tag !== "MalList" && seq.tag !== "MalVector" - ) { - throw new Error( - "Invalid Argument: map parameter 1: expected sequence", - ); + return ap.value; + }, + + cons: ([a, b]: Array): MalType.MalType => { + validateArgument(0, a); + validateArgument(1, b, ["MalNil", "MalList", "MalVector"]); + + return MalType.mkList([a, ...asSeq(b).items]); + }, + + concat: (lst: Array): MalType.MalType => { + const result: Array = []; + + lst.forEach((v, i) => { + validateArgument(i, v, ["MalNil", "MalList", "MalVector"]); + if (v.tag !== "MalNil") { + asSeq(v).items.forEach((e) => result.push(e)); } + }); - const fn = f.tag === "MalInternalFunction" - ? f.fn - : (args: Array): MalType.MalType => - evaluate(f.body, Env.mkEnv(f.env, f.params, args)); - - const mappedItems = seq.items.map((p) => fn([p])); - - return MalType.mkList(mappedItems); - }), - ], - [ - "nil?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean(v !== undefined && v.tag === "MalNil"); - }), - ], - [ - "true?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean( - v !== undefined && v.tag === "MalBoolean" && v.value, - ); - }), - ], - [ - "false?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean( - v !== undefined && v.tag === "MalBoolean" && !v.value, + return MalType.mkList(result); + }, + + vec: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector"]); + + return (v.tag === "MalVector") ? v : MalType.mkVector(asSeq(v).items); + }, + + nth: ([l, i]: Array): MalType.MalType => { + validateArgument(0, l, ["MalList", "MalVector", "MalNil"]); + validateArgument(1, i, "MalNumber"); + + const result = asSeq(l).items[asNumber(i)]; + if (result === undefined) { + throw new Error( + `Index Out Of Range: nth: ${asNumber(i)} exceeds bounds of ${ + JSON.stringify(l) + }`, ); - }), - ], - [ - "symbol?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean(v !== undefined && v.tag === "MalSymbol"); - }), - ], - [ - "symbol", - MalType.mkInternalFunction(([v]) => { - if (v === undefined) { - throw new Error( - "Invalid Argument: symbol parameter 0: expected string or symbol", - ); - } - if (v.tag === "MalSymbol") { - return v; - } else if (v.tag === "MalString") { - return MalType.mkSymbol(v.value); - } else { - throw new Error( - `Invalid Argument: symbol parameter 0: expected string or symbol: ${ - JSON.stringify(v) - }`, - ); - } - }), - ], - [ - "keyword", - MalType.mkInternalFunction(([v]) => { - if (v === undefined) { - throw new Error( - "Invalid Argument: symbol parameter 0: expected string or keyword", - ); - } - if (v.tag === "MalKeyword") { - return v; - } else if (v.tag === "MalString") { - return MalType.mkKeyword(`:${v.value}`); - } else { - throw new Error( - `Invalid Argument: keyword parameter 0: expected string or keyword: ${ - JSON.stringify(v) - }`, - ); - } - }), - ], - [ - "keyword?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean(v !== undefined && v.tag === "MalKeyword"); - }), - ], - [ - "string?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean(v !== undefined && v.tag === "MalString"); - }), - ], - [ - "number?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean(v !== undefined && v.tag === "MalNumber"); - }), - ], - [ - "vector", - MalType.mkInternalFunction((lst) => { - return MalType.mkVector(lst); - }), - ], - [ - "vector?", - MalType.mkInternalFunction(([v]) => { - return MalType.mkBoolean(v !== undefined && v.tag === "MalVector"); - }), - ], - [ - "sequential?", - MalType.mkInternalFunction(([v]) => - MalType.mkBoolean( - v !== undefined && (v.tag === "MalVector" || v.tag === "MalList"), - ) - ), - ], - [ - "hash-map", - MalType.mkInternalFunction((items) => - MalType.mkHashMap(mkHashPairs(items, "hash-map")) - ), - ], - [ - "map?", - MalType.mkInternalFunction(([v]) => - MalType.mkBoolean(v !== undefined && v.tag === "MalHashMap") - ), - ], - [ - "assoc", - MalType.mkInternalFunction(([m, ...items]) => { - if (m === undefined || m.tag !== "MalHashMap") { - throw new Error( - "Invalid Argument: assoc parameter 0: expected a hash map", - ); - } - return MalType.mapAssoc(m, mkHashPairs(items ?? [], "assoc")); - }), - ], - [ - "dissoc", - MalType.mkInternalFunction(([m, ...items]) => { - if (m === undefined || m.tag !== "MalHashMap") { - throw new Error( - "Invalid Argument: dissoc parameter 0: expected a hash map", - ); - } + } else { + return result; + } + }, - return MalType.mapDissoc(m, items); - }), - ], - [ - "get", - MalType.mkInternalFunction(([m, key]) => { - if (m === undefined || m.tag !== "MalHashMap" && m.tag !== "MalNil") { - throw new Error( - "Invalid Argument: get parameter 0: expected a hash map", - ); - } + first: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector", "MalNil"]); - if ( - key === undefined || - (key.tag !== "MalKeyword" && key.tag !== "MalString") - ) { - throw new Error( - "Invalid Argument: get parameter 1: expected a keyword or string", - ); - } + return v.tag === "MalNil" ? MalType.nil : asSeq(v).items[0] ?? MalType.nil; + }, - return m.tag === "MalNil" ? MalType.nil : MalType.mapGet(m, key); - }), - ], - [ - "contains?", - MalType.mkInternalFunction(([m, key]) => { - if (m === undefined || m.tag !== "MalHashMap") { - throw new Error( - "Invalid Argument: contains? parameter 0: expected a hash map", - ); - } - if ( - key === undefined || - (key.tag !== "MalKeyword" && key.tag !== "MalString") - ) { - throw new Error( - "Invalid Argument: contains? parameter 1: expected a keyword or string", - ); - } + rest: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector", "MalNil"]); - return MalType.mapContains(m, key); - }), - ], - [ - "keys", - MalType.mkInternalFunction(([m]) => { - if (m === undefined || m.tag !== "MalHashMap") { - throw new Error( - "Invalid Argument: contains? parameter 0: expected a hash map", - ); - } + return v.tag === "MalNil" + ? MalType.mkList([]) + : MalType.mkList(asSeq(v).items.slice(1)); + }, - return MalType.mkList(MalType.mapKeys(m)); - }), - ], - [ - "vals", - MalType.mkInternalFunction(([m]) => { - if (m === undefined || m.tag !== "MalHashMap") { - throw new Error( - "Invalid Argument: contains? parameter 0: expected a hash map", - ); - } + throw: ([v]: Array): MalType.MalType => { + validateArgument(0, v); - return MalType.mkList(MalType.mapValues(m)); - }), - ], - - [ - "readline", - MalType.mkInternalFunction(([prompt]) => { - if (prompt === undefined || prompt.tag !== "MalString") { - throw new Error( - "Invalid Argument: readline parameter 0: expected a string", - ); - } + throw v; + }, + + apply: ([f, ...rest]: Array): MalType.MalType => { + validateArgument(0, f, ["MalFunction", "MalInternalFunction"]); + + const items: Array = []; - const text = readline(`${prompt.value}> `); - - return text === undefined ? MalType.nil : MalType.mkString(text); - }), - ], - - [ - "time-ms", - MalType.mkInternalFunction((_) => MalType.mkNumber(performance.now())), - ], - [ - "meta", - MalType.mkInternalFunction(([v]) => { - if (v === undefined) { - return MalType.nil; + rest.forEach((v) => { + if (v.tag === "MalList" || v.tag == "MalVector") { + v.items.forEach((i) => items.push(i)); } else { - switch (v.tag) { - case "MalFunction": - case "MalHashMap": - case "MalInternalFunction": - case "MalList": - case "MalVector": - return v.meta ?? MalType.nil; - default: - return MalType.nil; - } - } - }), - ], - [ - "with-meta", - MalType.mkInternalFunction(([v, m]) => { - if (v === undefined || m === undefined) { - throw MalType.mkList( - [ - MalType.mkSymbol("IncorrectParameter"), - MalType.mkString("with-meta"), - ], - ); + items.push(v); } - return MalType.withMeta(v, m); - }), - ], - [ - "fn?", - MalType.mkInternalFunction(([v]) => - MalType.mkBoolean( - v !== undefined && - (v.tag === "MalFunction" && !v.isMacro || - v.tag === "MalInternalFunction"), - ) + }); + + if (f.tag === "MalInternalFunction") { + return f.fn(items); + } else if (f.tag === "MalFunction") { + const ast = f.body; + const env = Env.mkEnv(f.env, f.params, items); + return evaluate(ast, env); + } else { + return MalType.nil; + } + }, + + map: ([f, seq]: Array): MalType.MalType => { + validateArgument(0, f, ["MalFunction", "MalInternalFunction"]); + validateArgument(1, seq, ["MalList", "MalVector"]); + + const fn = f.tag === "MalFunction" + ? (args: Array): MalType.MalType => + evaluate(f.body, Env.mkEnv(f.env, f.params, args)) + : asInternalFunction(f).fn; + + const mappedItems = asSeq(seq).items.map((p) => fn([p])); + + return MalType.mkList(mappedItems); + }, + + "nil?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalNil"), + + "true?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalBoolean" && v.value), + + "false?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalBoolean" && !v.value), + + "symbol?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalSymbol"), + + symbol: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalSymbol", "MalString"]); + + return (v.tag === "MalSymbol") ? v : MalType.mkSymbol(asString(v)); + }, + + keyword: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalKeyword", "MalString"]); + + return (v.tag === "MalKeyword") ? v : MalType.mkKeyword(`:${asString(v)}`); + }, + + "keyword?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalKeyword"), + + "string?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalString"), + + "number?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalNumber"), + + vector: MalType.mkVector, + + "vector?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalVector"), + + "sequential?": ([v]: Array): MalType.MalType => + MalType.mkBoolean( + v !== undefined && (v.tag === "MalVector" || v.tag === "MalList"), ), - ], - [ - "macro?", - MalType.mkInternalFunction(([v]) => - MalType.mkBoolean(v !== undefined && v.tag === "MalFunction" && v.isMacro) + + "hash-map": (items: Array): MalType.MalType => + MalType.mkHashMap(mkHashPairs(items, "hash-map")), + + "map?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalHashMap"), + + assoc: ([m, ...items]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); + + return MalType.mapAssoc(asHashMap(m), mkHashPairs(items ?? [], "assoc")); + }, + + dissoc: ([m, ...items]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); + + return MalType.mapDissoc(asHashMap(m), items); + }, + + get: ([m, key]: Array): MalType.MalType => { + validateArgument(0, m, ["MalHashMap", "MalNil"]); + validateArgument(1, key, ["MalKeyword", "MalString"]); + + return m.tag === "MalNil" ? MalType.nil : MalType.mapGet(asHashMap(m), key); + }, + + "contains?": ([m, key]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); + validateArgument(1, key, ["MalKeyword", "MalString"]); + + return MalType.mapContains(asHashMap(m), key); + }, + + keys: ([m]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); + + return MalType.mkList(MalType.mapKeys(asHashMap(m))); + }, + + vals: ([m]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); + + return MalType.mkList(MalType.mapValues(asHashMap(m))); + }, + + readline: ([prompt]: Array): MalType.MalType => { + validateArgument(0, prompt, "MalString"); + + const text = readline(`${asString(prompt)}> `); + + return text === undefined ? MalType.nil : MalType.mkString(text); + }, + + "time-ms": (_: Array): MalType.MalType => + MalType.mkNumber(performance.now()), + meta: ([v]: Array): MalType.MalType => { + if (v === undefined) { + return MalType.nil; + } else { + switch (v.tag) { + case "MalFunction": + case "MalHashMap": + case "MalInternalFunction": + case "MalVector": + case "MalList": + return v.meta ?? MalType.nil; + default: + return MalType.nil; + } + } + }, + + "with-meta": ([v, m]: Array): MalType.MalType => { + validateArgument(0, v); + validateArgument(1, m); + + return MalType.withMeta(v, m); + }, + + "fn?": ([v]: Array): MalType.MalType => + MalType.mkBoolean( + v !== undefined && + (v.tag === "MalFunction" && !v.isMacro || + v.tag === "MalInternalFunction"), ), - ], - [ - "seq", - MalType.mkInternalFunction(([s]) => { - if (s === undefined) { - throw MalType.mkList( - [ - MalType.mkSymbol("IncorrectParameter"), - MalType.mkString("seq"), - ], - ); - } - if (s.tag === "MalList") { - return s.items.length === 0 ? MalType.nil : s; - } else if (s.tag === "MalVector") { - return s.items.length === 0 ? MalType.nil : MalType.mkList(s.items); - } else if (s.tag === "MalNil") { - return s; - } else if (s.tag === "MalString") { - return s.value.length === 0 - ? MalType.nil - : MalType.mkList(s.value.split("").map((e) => MalType.mkString(e))); - } else { - throw MalType.mkList( - [ - MalType.mkSymbol("IncorrectParameter"), - MalType.mkString("seq"), - ], - ); - } - }), - ], - [ - "conj", - MalType.mkInternalFunction(([c, ...es]) => { - if (c === undefined) { - throw MalType.mkList( - [ - MalType.mkSymbol("IncorrectParameter"), - MalType.mkString("conj"), - ], - ); - } - if (es === undefined) { - return c; - } else if (c.tag === "MalList") { - return MalType.mkList([...es.reverse(), ...c.items]); - } else if (c.tag === "MalVector") { - return MalType.mkVector([...c.items, ...es]); - } else { - throw MalType.mkList( - [ - MalType.mkSymbol("IncorrectParameterType"), - MalType.mkString("conj"), - MalType.mkList( - [MalType.mkSymbol("list"), MalType.mkSymbol("vector")], - ), - MalType.mkString(c.tag), - ], - ); + "macro?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalFunction" && v.isMacro), + + seq: ([s]: Array): MalType.MalType => { + validateArgument(0, s, ["MalList", "MalVector", "MalNil", "MalString"]); + + if (s.tag === "MalList") { + return s.items.length === 0 ? MalType.nil : s; + } else if (s.tag === "MalVector") { + return s.items.length === 0 ? MalType.nil : MalType.mkList(s.items); + } else if (s.tag === "MalNil") { + return s; + } else { + const sp = asString(s); + + return sp.length === 0 + ? MalType.nil + : MalType.mkList(sp.split("").map((e) => MalType.mkString(e))); + } + }, + + conj: ([c, ...es]: Array): MalType.MalType => { + validateArgument(0, c, ["MalList", "MalVector"]); + + return (es === undefined) + ? c + : (c.tag === "MalList") + ? MalType.mkList([...es.reverse(), ...c.items]) + : MalType.mkVector([...asSeq(c).items, ...es]); + }, +}); + +export const ns = ( + evaluate: (ast: MalType.MalType, env: Env.Env) => MalType.MalType, +): Array<[string, MalType.MalType]> => { + const fs: any = __ns(evaluate); + + const mkFun = ( + name: string, + fn: (args: Array) => MalType.MalType, + ): MalType.MalInternalFunction => { + const fnp = (args: Array): MalType.MalType => { + try { + return fn(args); + } catch (e: any) { + if (e.error === "UndefinedParameter") { + throw MalType.mkList([ + MalType.mkSymbol("UndefinedParameter"), + MalType.mkSymbol(name), + MalType.mkNumber(e.parameter), + ]); + } else if (e.error === "InvalidTag") { + throw MalType.mkList([ + MalType.mkSymbol("InvalidParameter"), + MalType.mkSymbol(name), + MalType.mkNumber(e.parameter), + e.value, + MalType.mkList(e.tags.map(MalType.mkSymbol)), + ]); + } else { + throw e; + } } - }), - ], -]; + }; -const mkHashPairs = ( - items: Array, - procedureName: string, -): Array<[MalType.MalType, MalType.MalType]> => { - const args: Array<[MalType.MalType, MalType.MalType]> = []; + return MalType.mkInternalFunction(fnp); + }; - if (items.length % 2 === 1) { - throw new Error( - `Invalid Argument: ${procedureName}: Odd number of arguments`, - ); + return Object.keys(fs).map(( + key: string, + ) => [key, mkFun(key, fs[key])]); +}; + +const asAtom = (v: MalType.MalType): MalType.MalAtom => { + if (v.tag === "MalAtom") { + return v; + } else { + throw Error("Unable to coerce to atom"); } - for (let lp = 0; lp < items.length; lp += 2) { - args.push([items[lp], items[lp + 1]]); +}; + +const asHashMap = (v: MalType.MalType): MalType.MalHashMap => { + if (v.tag === "MalHashMap") { + return v; + } else { + throw Error("Unable to coerce to hashmap"); } +}; - return args; +const asInternalFunction = ( + v: MalType.MalType, +): MalType.MalInternalFunction => { + if (v.tag === "MalInternalFunction") { + return v; + } else { + throw Error("Unable to coerce to internal function"); + } +}; + +const asNumber = (v: MalType.MalType): number => { + if (v.tag === "MalNumber") { + return v.value; + } else { + throw Error("Unable to coerce to number"); + } +}; + +const asSeq = (v: MalType.MalType): MalType.MalList | MalType.MalVector => { + if (v.tag === "MalList" || v.tag === "MalVector") { + return v; + } else { + throw Error("Unable to coerce to sequence"); + } +}; + +const asString = (v: MalType.MalType): string => { + if (v.tag === "MalString") { + return v.value; + } else { + throw Error("Unable to coerce to string"); + } }; From 877bf28d32c20632daf683486e7a4724b8529caf Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Sun, 3 Jan 2021 13:39:13 +0200 Subject: [PATCH 19/20] refactor: sort core procedures --- impls/deno/core.ts | 457 +++++++++++++++++++++++---------------------- 1 file changed, 229 insertions(+), 228 deletions(-) diff --git a/impls/deno/core.ts b/impls/deno/core.ts index 1dde656633..03fda0af27 100644 --- a/impls/deno/core.ts +++ b/impls/deno/core.ts @@ -73,49 +73,6 @@ const __ns = ( return MalType.mkNumber(asNumber(a) / asNumber(b)); }, - "pr_str": ([v]: Array): MalType.MalType => { - console.log(Printer.prStr(v, true)); - return MalType.nil; - }, - - println: (args: Array): MalType.MalType => { - console.log(args.map((v) => Printer.prStr(v, false)).join(" ")); - return MalType.nil; - }, - - prn: (args: Array): MalType.MalType => { - console.log(args.map((v) => Printer.prStr(v, true)).join(" ")); - return MalType.nil; - }, - - "pr-str": (args: Array): MalType.MalType => - MalType.mkString(args.map((v) => Printer.prStr(v, true)).join(" ")), - - str: (args: Array): MalType.MalType => - MalType.mkString(args.map((v) => Printer.prStr(v, false)).join("")), - - list: MalType.mkList, - - "list?": ([v]: Array): MalType.MalType => { - validateArgument(0, v); - - return MalType.mkBoolean(v.tag === "MalList"); - }, - - "empty?": ([v]: Array): MalType.MalType => { - validateArgument(0, v, ["MalList", "MalVector"]); - - return MalType.mkBoolean(asSeq(v).items.length === 0); - }, - - count: ([v]: Array): MalType.MalType => { - validateArgument(0, v, ["MalList", "MalVector", "MalNil"]); - - return (v.tag === "MalNil") - ? MalType.mkNumber(0) - : MalType.mkNumber(asSeq(v).items.length); - }, - "=": ([a, b]: Array): MalType.MalType => MalType.mkBoolean(MalType.equals(a, b)), @@ -147,20 +104,34 @@ const __ns = ( return MalType.mkBoolean(asNumber(a) >= asNumber(b)); }, - "read-string": ([s]: Array): MalType.MalType => { - if (s === undefined) { - return MalType.nil; - } + apply: ([f, ...rest]: Array): MalType.MalType => { + validateArgument(0, f, ["MalFunction", "MalInternalFunction"]); - validateArgument(0, s, "MalString"); + const items: Array = []; - return Reader.readStr(asString(s)); + rest.forEach((v) => { + if (v.tag === "MalList" || v.tag == "MalVector") { + v.items.forEach((i) => items.push(i)); + } else { + items.push(v); + } + }); + + if (f.tag === "MalInternalFunction") { + return f.fn(items); + } else if (f.tag === "MalFunction") { + const ast = f.body; + const env = Env.mkEnv(f.env, f.params, items); + return evaluate(ast, env); + } else { + return MalType.nil; + } }, - slurp: ([s]: Array): MalType.MalType => { - validateArgument(0, s, "MalString"); + assoc: ([m, ...items]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); - return MalType.mkString(Deno.readTextFileSync(asString(s))); + return MalType.mapAssoc(asHashMap(m), mkHashPairs(items ?? [], "assoc")); }, atom: ([v]: Array): MalType.MalType => @@ -169,44 +140,6 @@ const __ns = ( "atom?": ([v]: Array): MalType.MalType => MalType.mkBoolean(v !== undefined && v.tag === "MalAtom"), - deref: ([v]: Array): MalType.MalType => { - validateArgument(0, v, "MalAtom"); - - return asAtom(v).value; - }, - - "reset!": ([a, v]: Array): MalType.MalType => { - validateArgument(0, a, "MalAtom"); - - asAtom(a).value = v ?? MalType.nil; - - return asAtom(a).value; - }, - - "swap!": ([a, f, ...args]: Array): MalType.MalType => { - validateArgument(0, a, "MalAtom"); - validateArgument(1, f, ["MalFunction", "MalInternalFunction"]); - - const ap = asAtom(a); - - args = [ap.value, ...(args ?? [])]; - - if (f.tag === "MalFunction") { - ap.value = evaluate(f.body, Env.mkEnv(f.env, f.params, args)); - } else if (f.tag === "MalInternalFunction") { - ap.value = f.fn(args); - } - - return ap.value; - }, - - cons: ([a, b]: Array): MalType.MalType => { - validateArgument(0, a); - validateArgument(1, b, ["MalNil", "MalList", "MalVector"]); - - return MalType.mkList([a, ...asSeq(b).items]); - }, - concat: (lst: Array): MalType.MalType => { const result: Array = []; @@ -220,102 +153,86 @@ const __ns = ( return MalType.mkList(result); }, - vec: ([v]: Array): MalType.MalType => { - validateArgument(0, v, ["MalList", "MalVector"]); + conj: ([c, ...es]: Array): MalType.MalType => { + validateArgument(0, c, ["MalList", "MalVector"]); - return (v.tag === "MalVector") ? v : MalType.mkVector(asSeq(v).items); + return (es === undefined) + ? c + : (c.tag === "MalList") + ? MalType.mkList([...es.reverse(), ...c.items]) + : MalType.mkVector([...asSeq(c).items, ...es]); }, - nth: ([l, i]: Array): MalType.MalType => { - validateArgument(0, l, ["MalList", "MalVector", "MalNil"]); - validateArgument(1, i, "MalNumber"); + cons: ([a, b]: Array): MalType.MalType => { + validateArgument(0, a); + validateArgument(1, b, ["MalNil", "MalList", "MalVector"]); - const result = asSeq(l).items[asNumber(i)]; - if (result === undefined) { - throw new Error( - `Index Out Of Range: nth: ${asNumber(i)} exceeds bounds of ${ - JSON.stringify(l) - }`, - ); - } else { - return result; - } + return MalType.mkList([a, ...asSeq(b).items]); }, - first: ([v]: Array): MalType.MalType => { - validateArgument(0, v, ["MalList", "MalVector", "MalNil"]); + "contains?": ([m, key]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); + validateArgument(1, key, ["MalKeyword", "MalString"]); - return v.tag === "MalNil" ? MalType.nil : asSeq(v).items[0] ?? MalType.nil; + return MalType.mapContains(asHashMap(m), key); }, - rest: ([v]: Array): MalType.MalType => { + count: ([v]: Array): MalType.MalType => { validateArgument(0, v, ["MalList", "MalVector", "MalNil"]); - return v.tag === "MalNil" - ? MalType.mkList([]) - : MalType.mkList(asSeq(v).items.slice(1)); + return (v.tag === "MalNil") + ? MalType.mkNumber(0) + : MalType.mkNumber(asSeq(v).items.length); }, - throw: ([v]: Array): MalType.MalType => { - validateArgument(0, v); + deref: ([v]: Array): MalType.MalType => { + validateArgument(0, v, "MalAtom"); - throw v; + return asAtom(v).value; }, - apply: ([f, ...rest]: Array): MalType.MalType => { - validateArgument(0, f, ["MalFunction", "MalInternalFunction"]); + dissoc: ([m, ...items]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); - const items: Array = []; + return MalType.mapDissoc(asHashMap(m), items); + }, - rest.forEach((v) => { - if (v.tag === "MalList" || v.tag == "MalVector") { - v.items.forEach((i) => items.push(i)); - } else { - items.push(v); - } - }); + "empty?": ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector"]); - if (f.tag === "MalInternalFunction") { - return f.fn(items); - } else if (f.tag === "MalFunction") { - const ast = f.body; - const env = Env.mkEnv(f.env, f.params, items); - return evaluate(ast, env); - } else { - return MalType.nil; - } + return MalType.mkBoolean(asSeq(v).items.length === 0); }, - map: ([f, seq]: Array): MalType.MalType => { - validateArgument(0, f, ["MalFunction", "MalInternalFunction"]); - validateArgument(1, seq, ["MalList", "MalVector"]); - - const fn = f.tag === "MalFunction" - ? (args: Array): MalType.MalType => - evaluate(f.body, Env.mkEnv(f.env, f.params, args)) - : asInternalFunction(f).fn; + "false?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalBoolean" && !v.value), - const mappedItems = asSeq(seq).items.map((p) => fn([p])); + first: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector", "MalNil"]); - return MalType.mkList(mappedItems); + return v.tag === "MalNil" ? MalType.nil : asSeq(v).items[0] ?? MalType.nil; }, - "nil?": ([v]: Array): MalType.MalType => - MalType.mkBoolean(v !== undefined && v.tag === "MalNil"), + "fn?": ([v]: Array): MalType.MalType => + MalType.mkBoolean( + v !== undefined && + (v.tag === "MalFunction" && !v.isMacro || + v.tag === "MalInternalFunction"), + ), - "true?": ([v]: Array): MalType.MalType => - MalType.mkBoolean(v !== undefined && v.tag === "MalBoolean" && v.value), + get: ([m, key]: Array): MalType.MalType => { + validateArgument(0, m, ["MalHashMap", "MalNil"]); + validateArgument(1, key, ["MalKeyword", "MalString"]); - "false?": ([v]: Array): MalType.MalType => - MalType.mkBoolean(v !== undefined && v.tag === "MalBoolean" && !v.value), + return m.tag === "MalNil" ? MalType.nil : MalType.mapGet(asHashMap(m), key); + }, - "symbol?": ([v]: Array): MalType.MalType => - MalType.mkBoolean(v !== undefined && v.tag === "MalSymbol"), + "hash-map": (items: Array): MalType.MalType => + MalType.mkHashMap(mkHashPairs(items, "hash-map")), - symbol: ([v]: Array): MalType.MalType => { - validateArgument(0, v, ["MalSymbol", "MalString"]); + keys: ([m]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); - return (v.tag === "MalSymbol") ? v : MalType.mkSymbol(asString(v)); + return MalType.mkList(MalType.mapKeys(asHashMap(m))); }, keyword: ([v]: Array): MalType.MalType => { @@ -327,64 +244,99 @@ const __ns = ( "keyword?": ([v]: Array): MalType.MalType => MalType.mkBoolean(v !== undefined && v.tag === "MalKeyword"), - "string?": ([v]: Array): MalType.MalType => - MalType.mkBoolean(v !== undefined && v.tag === "MalString"), + list: MalType.mkList, - "number?": ([v]: Array): MalType.MalType => - MalType.mkBoolean(v !== undefined && v.tag === "MalNumber"), + "list?": ([v]: Array): MalType.MalType => { + validateArgument(0, v); - vector: MalType.mkVector, + return MalType.mkBoolean(v.tag === "MalList"); + }, - "vector?": ([v]: Array): MalType.MalType => - MalType.mkBoolean(v !== undefined && v.tag === "MalVector"), + "macro?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalFunction" && v.isMacro), - "sequential?": ([v]: Array): MalType.MalType => - MalType.mkBoolean( - v !== undefined && (v.tag === "MalVector" || v.tag === "MalList"), - ), + map: ([f, seq]: Array): MalType.MalType => { + validateArgument(0, f, ["MalFunction", "MalInternalFunction"]); + validateArgument(1, seq, ["MalList", "MalVector"]); - "hash-map": (items: Array): MalType.MalType => - MalType.mkHashMap(mkHashPairs(items, "hash-map")), + const fn = f.tag === "MalFunction" + ? (args: Array): MalType.MalType => + evaluate(f.body, Env.mkEnv(f.env, f.params, args)) + : asInternalFunction(f).fn; + + const mappedItems = asSeq(seq).items.map((p) => fn([p])); + + return MalType.mkList(mappedItems); + }, "map?": ([v]: Array): MalType.MalType => MalType.mkBoolean(v !== undefined && v.tag === "MalHashMap"), - assoc: ([m, ...items]: Array): MalType.MalType => { - validateArgument(0, m, "MalHashMap"); - - return MalType.mapAssoc(asHashMap(m), mkHashPairs(items ?? [], "assoc")); + meta: ([v]: Array): MalType.MalType => { + if (v === undefined) { + return MalType.nil; + } else { + switch (v.tag) { + case "MalFunction": + case "MalHashMap": + case "MalInternalFunction": + case "MalVector": + case "MalList": + return v.meta ?? MalType.nil; + default: + return MalType.nil; + } + } }, - dissoc: ([m, ...items]: Array): MalType.MalType => { - validateArgument(0, m, "MalHashMap"); + "nil?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalNil"), - return MalType.mapDissoc(asHashMap(m), items); + nth: ([l, i]: Array): MalType.MalType => { + validateArgument(0, l, ["MalList", "MalVector", "MalNil"]); + validateArgument(1, i, "MalNumber"); + + const result = asSeq(l).items[asNumber(i)]; + if (result === undefined) { + throw new Error( + `Index Out Of Range: nth: ${asNumber(i)} exceeds bounds of ${ + JSON.stringify(l) + }`, + ); + } else { + return result; + } }, - get: ([m, key]: Array): MalType.MalType => { - validateArgument(0, m, ["MalHashMap", "MalNil"]); - validateArgument(1, key, ["MalKeyword", "MalString"]); + "number?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalNumber"), - return m.tag === "MalNil" ? MalType.nil : MalType.mapGet(asHashMap(m), key); + "pr_str": ([v]: Array): MalType.MalType => { + console.log(Printer.prStr(v, true)); + return MalType.nil; }, - "contains?": ([m, key]: Array): MalType.MalType => { - validateArgument(0, m, "MalHashMap"); - validateArgument(1, key, ["MalKeyword", "MalString"]); - - return MalType.mapContains(asHashMap(m), key); + println: (args: Array): MalType.MalType => { + console.log(args.map((v) => Printer.prStr(v, false)).join(" ")); + return MalType.nil; }, - keys: ([m]: Array): MalType.MalType => { - validateArgument(0, m, "MalHashMap"); + "pr-str": (args: Array): MalType.MalType => + MalType.mkString(args.map((v) => Printer.prStr(v, true)).join(" ")), - return MalType.mkList(MalType.mapKeys(asHashMap(m))); + prn: (args: Array): MalType.MalType => { + console.log(args.map((v) => Printer.prStr(v, true)).join(" ")); + return MalType.nil; }, - vals: ([m]: Array): MalType.MalType => { - validateArgument(0, m, "MalHashMap"); + "read-string": ([s]: Array): MalType.MalType => { + if (s === undefined) { + return MalType.nil; + } - return MalType.mkList(MalType.mapValues(asHashMap(m))); + validateArgument(0, s, "MalString"); + + return Reader.readStr(asString(s)); }, readline: ([prompt]: Array): MalType.MalType => { @@ -395,41 +347,21 @@ const __ns = ( return text === undefined ? MalType.nil : MalType.mkString(text); }, - "time-ms": (_: Array): MalType.MalType => - MalType.mkNumber(performance.now()), - meta: ([v]: Array): MalType.MalType => { - if (v === undefined) { - return MalType.nil; - } else { - switch (v.tag) { - case "MalFunction": - case "MalHashMap": - case "MalInternalFunction": - case "MalVector": - case "MalList": - return v.meta ?? MalType.nil; - default: - return MalType.nil; - } - } - }, + "reset!": ([a, v]: Array): MalType.MalType => { + validateArgument(0, a, "MalAtom"); - "with-meta": ([v, m]: Array): MalType.MalType => { - validateArgument(0, v); - validateArgument(1, m); + asAtom(a).value = v ?? MalType.nil; - return MalType.withMeta(v, m); + return asAtom(a).value; }, - "fn?": ([v]: Array): MalType.MalType => - MalType.mkBoolean( - v !== undefined && - (v.tag === "MalFunction" && !v.isMacro || - v.tag === "MalInternalFunction"), - ), + rest: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector", "MalNil"]); - "macro?": ([v]: Array): MalType.MalType => - MalType.mkBoolean(v !== undefined && v.tag === "MalFunction" && v.isMacro), + return v.tag === "MalNil" + ? MalType.mkList([]) + : MalType.mkList(asSeq(v).items.slice(1)); + }, seq: ([s]: Array): MalType.MalType => { validateArgument(0, s, ["MalList", "MalVector", "MalNil", "MalString"]); @@ -449,14 +381,83 @@ const __ns = ( } }, - conj: ([c, ...es]: Array): MalType.MalType => { - validateArgument(0, c, ["MalList", "MalVector"]); + "sequential?": ([v]: Array): MalType.MalType => + MalType.mkBoolean( + v !== undefined && (v.tag === "MalVector" || v.tag === "MalList"), + ), - return (es === undefined) - ? c - : (c.tag === "MalList") - ? MalType.mkList([...es.reverse(), ...c.items]) - : MalType.mkVector([...asSeq(c).items, ...es]); + slurp: ([s]: Array): MalType.MalType => { + validateArgument(0, s, "MalString"); + + return MalType.mkString(Deno.readTextFileSync(asString(s))); + }, + + str: (args: Array): MalType.MalType => + MalType.mkString(args.map((v) => Printer.prStr(v, false)).join("")), + + symbol: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalSymbol", "MalString"]); + + return (v.tag === "MalSymbol") ? v : MalType.mkSymbol(asString(v)); + }, + + "string?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalString"), + + "symbol?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalSymbol"), + + "swap!": ([a, f, ...args]: Array): MalType.MalType => { + validateArgument(0, a, "MalAtom"); + validateArgument(1, f, ["MalFunction", "MalInternalFunction"]); + + const ap = asAtom(a); + + args = [ap.value, ...(args ?? [])]; + + if (f.tag === "MalFunction") { + ap.value = evaluate(f.body, Env.mkEnv(f.env, f.params, args)); + } else if (f.tag === "MalInternalFunction") { + ap.value = f.fn(args); + } + + return ap.value; + }, + + throw: ([v]: Array): MalType.MalType => { + validateArgument(0, v); + + throw v; + }, + + "time-ms": (_: Array): MalType.MalType => + MalType.mkNumber(performance.now()), + + "true?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalBoolean" && v.value), + + vals: ([m]: Array): MalType.MalType => { + validateArgument(0, m, "MalHashMap"); + + return MalType.mkList(MalType.mapValues(asHashMap(m))); + }, + + vec: ([v]: Array): MalType.MalType => { + validateArgument(0, v, ["MalList", "MalVector"]); + + return (v.tag === "MalVector") ? v : MalType.mkVector(asSeq(v).items); + }, + + vector: MalType.mkVector, + + "vector?": ([v]: Array): MalType.MalType => + MalType.mkBoolean(v !== undefined && v.tag === "MalVector"), + + "with-meta": ([v, m]: Array): MalType.MalType => { + validateArgument(0, v); + validateArgument(1, m); + + return MalType.withMeta(v, m); }, }); From 12c7658b868d6b6a9a68886c3815c7d4928edde6 Mon Sep 17 00:00:00 2001 From: Graeme Lockley Date: Mon, 4 Jan 2021 15:46:19 +0200 Subject: [PATCH 20/20] chore: create Dockerfile --- impls/deno/Dockerfile | 32 + test.err | 1823 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1855 insertions(+) create mode 100644 impls/deno/Dockerfile create mode 100644 test.err diff --git a/impls/deno/Dockerfile b/impls/deno/Dockerfile new file mode 100644 index 0000000000..e303672a95 --- /dev/null +++ b/impls/deno/Dockerfile @@ -0,0 +1,32 @@ +FROM ubuntu:18.04 +MAINTAINER Graeme Lockley + +########################################################## +# 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 + +RUN mkdir -p /mal && chmod 777 /mal +WORKDIR /mal + +########################################################## +# Specific implementation requirements +########################################################## + +RUN apt-get -y install unzip vim + +ENV DENO_INSTALL=/deno +RUN mkdir -p /deno \ + && chmod 777 /deno \ + && curl -fsSL https://deno.land/x/install/install.sh | sh + +ENV PATH=${DENO_INSTALL}/bin:${PATH} \ + DENO_DIR=${DENO_INSTALL}/.cache/deno diff --git a/test.err b/test.err new file mode 100644 index 0000000000..91d831a2ae --- /dev/null +++ b/test.err @@ -0,0 +1,1823 @@ +error: Could not create TypeScript compiler cache location: "/deno/.cache/deno/gen" +Check the permission of the directory. +error: Could not create TypeScript compiler cache location: "/deno/.cache/deno/gen" +Check the permission of the directory. +error: Could not create TypeScript compiler cache location: "/deno/.cache/deno/gen" +Check the permission of the directory. +Check file:///mal/impls/deno/step0_repl.ts +user> abcABC123 +abcABC123 +user> hello mal world +hello mal world +user> []{}"'* ;:() +[]{}"'* ;:() +user> hello world abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 (;:() []{}"'* ;:() []{}"'* ;:() []{}"'*) +hello world abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 (;:() []{}"'* ;:() []{}"'* ;:() []{}"'*) +user> ! +! +user> & +& +user> + ++ +user> , +, +user> - +- +user> / +/ +user> < +< +user> = += +user> > +> +user> ? +? +user> @ +@ +user> ^ +^ +user> _ +_ +user> ` +` +user> ~ +~ +user> # +# +user> $ +$ +user> % +% +user> . +. +user> | +| +user> +Check file:///mal/impls/deno/step1_read_print.ts +user> 1 +1 +user> 7 +7 +user> 7 +7 +user> -123 +-123 +user> + ++ +user> abc +abc +user> abc +abc +user> abc5 +abc5 +user> abc-def +abc-def +user> - +- +user> -abc +-abc +user> ->> +->> +user> (+ 1 2) +(+ 1 2) +user> () +() +user> ( ) +() +user> (nil) +(nil) +user> ((3 4)) +((3 4)) +user> (+ 1 (+ 2 3)) +(+ 1 (+ 2 3)) +user> ( + 1 (+ 2 3 ) ) +(+ 1 (+ 2 3)) +user> (* 1 2) +(* 1 2) +user> (** 1 2) +(** 1 2) +user> (* -3 6) +(* -3 6) +user> (()()) +(() ()) +user> (1 2, 3,,,,),, +(1 2 3) +user> nil +nil +user> true +true +user> false +false +user> "abc" +"abc" +user> "abc" +"abc" +user> "abc (with parens)" +"abc (with parens)" +user> "abc\"def" +"abc\"def" +user> "" +"" +user> "\\" +"\\" +user> "\\\\\\\\\\\\\\\\\\" +"\\\\\\\\\\\\\\\\\\" +user> "&" +"&" +user> "'" +"'" +user> "(" +"(" +user> ")" +")" +user> "*" +"*" +user> "+" +"+" +user> "," +"," +user> "-" +"-" +user> "/" +"/" +user> ":" +":" +user> ";" +";" +user> "<" +"<" +user> "=" +"=" +user> ">" +">" +user> "?" +"?" +user> "@" +"@" +user> "[" +"[" +user> "]" +"]" +user> "^" +"^" +user> "_" +"_" +user> "`" +"`" +user> "{" +"{" +user> "}" +"}" +user> "~" +"~" +user> (1 2 +Syntax Error: EOF whilst expecting ')' +user> [1 2 +Syntax Error: EOF whilst expecting ']' +user> "abc +Syntax Error: EOF whilst expecting '"': "abc +user> " +Syntax Error: EOF whilst expecting '"': " +user> "\" +Syntax Error: EOF whilst expecting '"': "\" +user> "\\\\\\\\\\\\\\\\\\\" +Syntax Error: EOF whilst expecting '"': "\\\\\\\\\\\\\\\\\\\" +user> (1 "abc +Syntax Error: EOF whilst expecting '"': "abc +user> (1 "abc" +Syntax Error: EOF whilst expecting ')' +user> '1 +(quote 1) +user> '(1 2 3) +(quote (1 2 3)) +user> `1 +(quasiquote 1) +user> `(1 2 3) +(quasiquote (1 2 3)) +user> ~1 +(unquote 1) +user> ~(1 2 3) +(unquote (1 2 3)) +user> `(1 ~a 3) +(quasiquote (1 (unquote a) 3)) +user> ~@(1 2 3) +(splice-unquote (1 2 3)) +user> :kw +:kw +user> (:kw1 :kw2 :kw3) +(:kw1 :kw2 :kw3) +user> [+ 1 2] +[+ 1 2] +user> [] +[] +user> [ ] +[] +user> [[3 4]] +[[3 4]] +user> [+ 1 [+ 2 3]] +[+ 1 [+ 2 3]] +user> [ + 1 [+ 2 3 ] ] +[+ 1 [+ 2 3]] +user> ([]) +([]) +user> {} +{} +user> { } +{} +user> {"abc" 1} +{"abc" 1} +user> {"a" {"b" 2}} +{"a" {"b" 2}} +user> {"a" {"b" {"c" 3}}} +{"a" {"b" {"c" 3}}} +user> { "a" {"b" { "cde" 3 } }} +{"a" {"b" {"cde" 3}}} +user> {"a1" 1 "a2" 2 "a3" 3} +{"a1" 1 "a2" 2 "a3" 3} +user> { :a {:b { :cde 3 } }} +{:a {:b {:cde 3}}} +user> {"1" 1} +{"1" 1} +user> ({}) +({}) +user> ;; whole line comment (not an exception) +Reader Error: No input +user> 1 ; comment after expression +1 +user> 1; comment after expression +1 +user> @a +(deref a) +user> ^{"a" 1} [1 2 3] +(with-meta [1 2 3] {"a" 1}) +user> "\n" +"\n" +user> "#" +"#" +user> "$" +"$" +user> "%" +"%" +user> "." +"." +user> "\\" +"\\" +user> "|" +"|" +user> 1;! +1 +user> 1;" +1 +user> 1;# +1 +user> 1;$ +1 +user> 1;% +1 +user> 1;' +1 +user> 1;\ +1 +user> 1;\\ +1 +user> 1;\\\ +1 +user> 1;` +1 +user> 1; &()*+,-./:;<=>?@[]^_{|}~ +1 +user> "!" +"!" +user> +Check file:///mal/impls/deno/step2_eval.ts +user> (+ 1 2) +3 +user> (+ 5 (* 2 3)) +11 +user> (- (+ 5 (* 2 3)) 3) +8 +user> (/ (- (+ 5 (* 2 3)) 3) 4) +2 +user> (/ (- (+ 515 (* 87 311)) 302) 27) +1010 +user> (* -3 6) +-18 +user> (/ (- (+ 515 (* -87 311)) 296) 27) +-994 +user> (abc 1 2 3) +Unknown Symbol: abc +user> () +() +user> [1 2 (+ 1 2)] +[1 2 3] +user> {"a" (+ 7 8)} +{"a" 15} +user> {:a (+ 7 8)} +{:a 15} +user> [] +[] +user> {} +{} +user> +Check file:///mal/impls/deno/step3_env.ts +user> (+ 1 2) +3 +user> (/ (- (+ 5 (* 2 3)) 3) 4) +2 +user> (def! x 3) +3 +user> x +3 +user> (def! x 4) +4 +user> x +4 +user> (def! y (+ 1 7)) +8 +user> y +8 +user> (def! mynum 111) +111 +user> (def! MYNUM 222) +222 +user> mynum +111 +user> MYNUM +222 +user> (abc 1 2 3) +'abc' not found +user> (def! w 123) +123 +user> (def! w (abc)) +'abc' not found +user> w +123 +user> (let* (z 9) z) +9 +user> (let* (x 9) x) +9 +user> x +4 +user> (let* (z (+ 2 3)) (+ 1 z)) +6 +user> (let* (p (+ 2 3) q (+ 2 p)) (+ p q)) +12 +user> (def! y (let* (z 7) z)) +7 +user> y +7 +user> (def! a 4) +4 +user> (let* (q 9) q) +9 +user> (let* (q 9) a) +4 +user> (let* (z 2) (let* (q 9) a)) +4 +user> (let* [z 9] z) +9 +user> (let* [p (+ 2 3) q (+ 2 p)] (+ p q)) +12 +user> (let* (a 5 b 6) [3 4 a [b 7] 8]) +[3 4 5 [6 7] 8] +user> +Check file:///mal/impls/deno/step4_if_fn_do.ts +user> (list) +() +user> (list? (list)) +true +user> (empty? (list)) +true +user> (empty? (list 1)) +false +user> (list 1 2 3) +(1 2 3) +user> (count (list 1 2 3)) +3 +user> (count (list)) +0 +user> (count nil) +0 +user> (if (> (count (list 1 2 3)) 3) 89 78) +78 +user> (if (>= (count (list 1 2 3)) 3) 89 78) +89 +user> (if true 7 8) +7 +user> (if false 7 8) +8 +user> (if false 7 false) +false +user> (if true (+ 1 7) (+ 1 8)) +8 +user> (if false (+ 1 7) (+ 1 8)) +9 +user> (if nil 7 8) +8 +user> (if 0 7 8) +7 +user> (if (list) 7 8) +7 +user> (if (list 1 2 3) 7 8) +7 +user> (= (list) nil) +false +user> (if false (+ 1 7)) +nil +user> (if nil 8) +nil +user> (if nil 8 7) +7 +user> (if true (+ 1 7)) +8 +user> (= 2 1) +false +user> (= 1 1) +true +user> (= 1 2) +false +user> (= 1 (+ 1 1)) +false +user> (= 2 (+ 1 1)) +true +user> (= nil 1) +false +user> (= nil nil) +true +user> (> 2 1) +true +user> (> 1 1) +false +user> (> 1 2) +false +user> (>= 2 1) +true +user> (>= 1 1) +true +user> (>= 1 2) +false +user> (< 2 1) +false +user> (< 1 1) +false +user> (< 1 2) +true +user> (<= 2 1) +false +user> (<= 1 1) +true +user> (<= 1 2) +true +user> (= 1 1) +true +user> (= 0 0) +true +user> (= 1 0) +false +user> (= true true) +true +user> (= false false) +true +user> (= nil nil) +true +user> (= (list) (list)) +true +user> (= (list 1 2) (list 1 2)) +true +user> (= (list 1) (list)) +false +user> (= (list) (list 1)) +false +user> (= 0 (list)) +false +user> (= (list) 0) +false +user> (= (list nil) (list)) +false +user> (+ 1 2) +3 +user> ( (fn* (a b) (+ b a)) 3 4) +7 +user> ( (fn* () 4) ) +4 +user> ( (fn* (f x) (f x)) (fn* (a) (+ 1 a)) 7) +8 +user> ( ( (fn* (a) (fn* (b) (+ a b))) 5) 7) +12 +user> (def! gen-plus5 (fn* () (fn* (b) (+ 5 b)))) +# +user> (def! plus5 (gen-plus5)) +# +user> (plus5 7) +12 +user> (def! gen-plusX (fn* (x) (fn* (b) (+ x b)))) +# +user> (def! plus7 (gen-plusX 7)) +# +user> (plus7 8) +15 +user> (do (prn 101)) +101 +nil +user> (do (prn 102) 7) +102 +7 +user> (do (prn 101) (prn 102) (+ 1 2)) +101 +102 +3 +user> (do (def! a 6) 7 (+ a 8)) +14 +user> a +6 +user> (def! DO (fn* (a) 7)) +# +user> (DO 3) +7 +user> (def! sumdown (fn* (N) (if (> N 0) (+ N (sumdown (- N 1))) 0))) +# +user> (sumdown 1) +1 +user> (sumdown 2) +3 +user> (sumdown 6) +21 +user> (def! fib (fn* (N) (if (= N 0) 1 (if (= N 1) 1 (+ (fib (- N 1)) (fib (- N 2))))))) +# +user> (fib 1) +1 +user> (fib 2) +2 +user> (fib 4) +5 +user> (let* (cst (fn* (n) (if (= n 0) nil (cst (- n 1))))) (cst 1)) +nil +user> (let* (f (fn* (n) (if (= n 0) 0 (g (- n 1)))) g (fn* (n) (f n))) (f 2)) +0 +user> (if "" 7 8) +7 +user> (= "" "") +true +user> (= "abc" "abc") +true +user> (= "abc" "") +false +user> (= "" "abc") +false +user> (= "abc" "def") +false +user> (= "abc" "ABC") +false +user> (= (list) "") +false +user> (= "" (list)) +false +user> ( (fn* (& more) (count more)) 1 2 3) +3 +user> ( (fn* (& more) (list? more)) 1 2 3) +true +user> ( (fn* (& more) (count more)) 1) +1 +user> ( (fn* (& more) (count more)) ) +0 +user> ( (fn* (& more) (list? more)) ) +true +user> ( (fn* (a & more) (count more)) 1 2 3) +2 +user> ( (fn* (a & more) (count more)) 1) +0 +user> ( (fn* (a & more) (list? more)) 1) +true +user> (not false) +true +user> (not nil) +true +user> (not true) +false +user> (not "a") +false +user> (not 0) +false +user> "" +"" +user> "abc" +"abc" +user> "abc def" +"abc def" +user> "\"" +"\"" +user> "abc\ndef\nghi" +"abc\ndef\nghi" +user> "abc\\def\\ghi" +"abc\\def\\ghi" +user> "\\n" +"\\n" +user> (pr-str) +"" +user> (pr-str "") +"\"\"" +user> (pr-str "abc") +"\"abc\"" +user> (pr-str "abc def" "ghi jkl") +"\"abc def\" \"ghi jkl\"" +user> (pr-str "\"") +"\"\\\"\"" +user> (pr-str (list 1 2 "abc" "\"") "def") +"(1 2 \"abc\" \"\\\"\") \"def\"" +user> (pr-str "abc\ndef\nghi") +"\"abc\\ndef\\nghi\"" +user> (pr-str "abc\\def\\ghi") +"\"abc\\\\def\\\\ghi\"" +user> (pr-str (list)) +"()" +user> (str) +"" +user> (str "") +"" +user> (str "abc") +"abc" +user> (str "\"") +"\"" +user> (str 1 "abc" 3) +"1abc3" +user> (str "abc def" "ghi jkl") +"abc defghi jkl" +user> (str "abc\ndef\nghi") +"abc\ndef\nghi" +user> (str "abc\\def\\ghi") +"abc\\def\\ghi" +user> (str (list 1 2 "abc" "\"") "def") +"(1 2 abc \")def" +user> (str (list)) +"()" +user> (prn) + +nil +user> (prn "") +"" +nil +user> (prn "abc") +"abc" +nil +user> (prn "abc def" "ghi jkl") +"abc def" "ghi jkl" +nil +user> (prn "\"") +"\"" +nil +user> (prn "abc\ndef\nghi") +"abc\ndef\nghi" +nil +user> (prn "abc\\def\\ghi") +"abc\\def\\ghi" +nil +user> nil +nil +user> (prn (list 1 2 "abc" "\"") "def") +(1 2 "abc" "\"") "def" +nil +user> (println) + +nil +user> (println "") + +nil +user> (println "abc") +abc +nil +user> (println "abc def" "ghi jkl") +abc def ghi jkl +nil +user> (println "\"") +" +nil +user> (println "abc\ndef\nghi") +abc +def +ghi +nil +user> (println "abc\\def\\ghi") +abc\def\ghi +nil +user> (println (list 1 2 "abc" "\"") "def") +(1 2 abc ") def +nil +user> (= :abc :abc) +true +user> (= :abc :def) +false +user> (= :abc ":abc") +false +user> (= (list :abc) (list :abc)) +true +user> (if [] 7 8) +7 +user> (pr-str [1 2 "abc" "\""] "def") +"[1 2 \"abc\" \"\\\"\"] \"def\"" +user> (pr-str []) +"[]" +user> (str [1 2 "abc" "\""] "def") +"[1 2 abc \"]def" +user> (str []) +"[]" +user> (count [1 2 3]) +3 +user> (empty? [1 2 3]) +false +user> (empty? []) +true +user> (list? [4 5 6]) +false +user> (= [] (list)) +true +user> (= [7 8] [7 8]) +true +user> (= [:abc] [:abc]) +true +user> (= (list 1 2) [1 2]) +true +user> (= (list 1) []) +false +user> (= [] [1]) +false +user> (= 0 []) +false +user> (= [] 0) +false +user> (= [] "") +false +user> (= "" []) +false +user> ( (fn* [] 4) ) +4 +user> ( (fn* [f x] (f x)) (fn* [a] (+ 1 a)) 7) +8 +user> (= [(list)] (list [])) +true +user> (= [1 2 (list 3 4 [5 6])] (list 1 2 [3 4 (list 5 6)])) +true +user> +Check file:///mal/impls/deno/step5_tco.ts +user> (def! sum2 (fn* (n acc) (if (= n 0) acc (sum2 (- n 1) (+ n acc))))) +# +user> (sum2 10 0) +55 +user> (def! res2 nil) +nil +user> (def! res2 (sum2 10000 0)) +50005000 +user> res2 +50005000 +user> (def! foo (fn* (n) (if (= n 0) 0 (bar (- n 1))))) +# +user> (def! bar (fn* (n) (if (= n 0) 0 (foo (- n 1))))) +# +user> (foo 10000) +0 +user> +Check file:///mal/impls/deno/step6_file.ts +user> (do (do 1 2)) +2 +user> (read-string "(1 2 (3 4) nil)") +(1 2 (3 4) nil) +user> (= nil (read-string "nil")) +true +user> (read-string "(+ 2 3)") +(+ 2 3) +user> (read-string "\"\n\"") +"\n" +user> (read-string "7 ;; comment") +7 +user> (read-string ";; comment") +user> (eval (read-string "(+ 2 3)")) +5 +user> (slurp "../tests/test.txt") +"A line of text\n" +user> (slurp "../tests/test.txt") +"A line of text\n" +user> (load-file "../tests/inc.mal") +nil +user> (inc1 7) +8 +user> (inc2 7) +9 +user> (inc3 9) +12 +user> (def! inc3 (fn* (a) (+ 3 a))) +# +user> (def! a (atom 2)) +(atom 2) +user> (atom? a) +true +user> (atom? 1) +false +user> (deref a) +2 +user> (reset! a 3) +3 +user> (deref a) +3 +user> (swap! a inc3) +6 +user> (deref a) +6 +user> (swap! a (fn* (a) a)) +6 +user> (swap! a (fn* (a) (* 2 a))) +12 +user> (swap! a (fn* (a b) (* a b)) 10) +120 +user> (swap! a + 3) +123 +user> (def! inc-it (fn* (a) (+ 1 a))) +# +user> (def! atm (atom 7)) +(atom 7) +user> (def! f (fn* () (swap! atm inc-it))) +# +user> (f) +8 +user> (f) +9 +user> (def! g (let* (atm (atom 0)) (fn* () (deref atm)))) +# +user> (def! atm (atom 1)) +(atom 1) +user> (g) +0 +user> (load-file "../tests/computations.mal") +nil +user> (sumdown 2) +3 +user> (fib 2) +1 +user> (def! atm (atom 9)) +(atom 9) +user> @atm +9 +user> (def! g (fn* [] 78)) +# +user> (g) +78 +user> (def! g (fn* [a] (+ a 78))) +# +user> (g 3) +81 +user> (list? *ARGV*) +true +user> *ARGV* +() +user> (let* (b 12) (do (eval (read-string "(def! aa 7)")) aa )) +7 +user> (load-file "../tests/incB.mal") +nil +user> (inc4 7) +11 +user> (inc5 7) +12 +user> (load-file "../tests/incC.mal") +nil +user> mymap +{"a" 1} +user> (def! a 1) +1 +user> (let* (a 2) (eval (read-string "a"))) +1 +user> (read-string "1;!") +1 +user> (read-string "1;\"") +1 +user> (read-string "1;#") +1 +user> (read-string "1;$") +1 +user> (read-string "1;%") +1 +user> (read-string "1;'") +1 +user> (read-string "1;\\") +1 +user> (read-string "1;\\\\") +1 +user> (read-string "1;\\\\\\") +1 +user> (read-string "1;`") +1 +user> (read-string "1; &()*+,-./:;<=>?@[]^_{|}~") +1 +user> +Check file:///mal/impls/deno/step7_quote.ts +user> (cons 1 (list)) +(1) +user> (cons 1 (list 2)) +(1 2) +user> (cons 1 (list 2 3)) +(1 2 3) +user> (cons (list 1) (list 2 3)) +((1) 2 3) +user> (def! a (list 2 3)) +(2 3) +user> (cons 1 a) +(1 2 3) +user> a +(2 3) +user> (concat) +() +user> (concat (list 1 2)) +(1 2) +user> (concat (list 1 2) (list 3 4)) +(1 2 3 4) +user> (concat (list 1 2) (list 3 4) (list 5 6)) +(1 2 3 4 5 6) +user> (concat (concat)) +() +user> (concat (list) (list)) +() +user> (def! a (list 1 2)) +(1 2) +user> (def! b (list 3 4)) +(3 4) +user> (concat a b (list 5 6)) +(1 2 3 4 5 6) +user> a +(1 2) +user> b +(3 4) +user> (quote 7) +7 +user> (quote (1 2 3)) +(1 2 3) +user> (quote (1 2 (3 4))) +(1 2 (3 4)) +user> (quasiquote nil) +nil +user> (quasiquote 7) +7 +user> (quasiquote a) +a +user> (quasiquote {"a" b}) +{"a" b} +user> (quasiquote ()) +() +user> (quasiquote (1 2 3)) +(1 2 3) +user> (quasiquote (a)) +(a) +user> (quasiquote (1 2 (3 4))) +(1 2 (3 4)) +user> (quasiquote (nil)) +(nil) +user> (quasiquote (1 ())) +(1 ()) +user> (quasiquote (() 1)) +(() 1) +user> (quasiquote (1 () 2)) +(1 () 2) +user> (quasiquote (())) +(()) +user> (quasiquote (unquote 7)) +7 +user> (def! a 8) +8 +user> (quasiquote a) +a +user> (quasiquote (unquote a)) +8 +user> (quasiquote (1 a 3)) +(1 a 3) +user> (quasiquote (1 (unquote a) 3)) +(1 8 3) +user> (def! b (quote (1 "b" "d"))) +(1 "b" "d") +user> (quasiquote (1 b 3)) +(1 b 3) +user> (quasiquote (1 (unquote b) 3)) +(1 (1 "b" "d") 3) +user> (quasiquote ((unquote 1) (unquote 2))) +(1 2) +user> (let* (x 0) (quasiquote (unquote x))) +0 +user> (def! c (quote (1 "b" "d"))) +(1 "b" "d") +user> (quasiquote (1 c 3)) +(1 c 3) +user> (quasiquote (1 (splice-unquote c) 3)) +(1 1 "b" "d" 3) +user> (quasiquote (1 (splice-unquote c))) +(1 1 "b" "d") +user> (quasiquote ((splice-unquote c) 2)) +(1 "b" "d" 2) +user> (quasiquote ((splice-unquote c) (splice-unquote c))) +(1 "b" "d" 1 "b" "d") +user> (= (quote abc) (quote abc)) +true +user> (= (quote abc) (quote abcd)) +false +user> (= (quote abc) "abc") +false +user> (= "abc" (quote abc)) +false +user> (= "abc" (str (quote abc))) +true +user> (= (quote abc) nil) +false +user> (= nil (quote abc)) +false +user> '7 +7 +user> '(1 2 3) +(1 2 3) +user> '(1 2 (3 4)) +(1 2 (3 4)) +user> (cons 1 []) +(1) +user> (cons [1] [2 3]) +([1] 2 3) +user> (cons 1 [2 3]) +(1 2 3) +user> (concat [1 2] (list 3 4) [5 6]) +(1 2 3 4 5 6) +user> (concat [1 2]) +(1 2) +user> `7 +7 +user> `(1 2 3) +(1 2 3) +user> `(1 2 (3 4)) +(1 2 (3 4)) +user> `(nil) +(nil) +user> `~7 +7 +user> (def! a 8) +8 +user> `(1 ~a 3) +(1 8 3) +user> (def! b '(1 "b" "d")) +(1 "b" "d") +user> `(1 b 3) +(1 b 3) +user> `(1 ~b 3) +(1 (1 "b" "d") 3) +user> (def! c '(1 "b" "d")) +(1 "b" "d") +user> `(1 c 3) +(1 c 3) +user> `(1 ~@c 3) +(1 1 "b" "d" 3) +user> (vec (list)) +[] +user> (vec (list 1)) +[1] +user> (vec (list 1 2)) +[1 2] +user> (vec []) +[] +user> (vec [1 2]) +[1 2] +user> (def! a (list 1 2)) +(1 2) +user> (vec a) +[1 2] +user> a +(1 2) +user> ((fn* (q) (quasiquote ((unquote q) (quote (unquote q))))) (quote (fn* (q) (quasiquote ((unquote q) (quote (unquote q))))))) +((fn* (q) (quasiquote ((unquote q) (quote (unquote q))))) (quote (fn* (q) (quasiquote ((unquote q) (quote (unquote q))))))) +user> (quasiquote []) +[] +user> (quasiquote [[]]) +[[]] +user> (quasiquote [()]) +[()] +user> (quasiquote ([])) +([]) +user> (def! a 8) +8 +user> `[1 a 3] +[1 a 3] +user> (quasiquote [a [] b [c] d [e f] g]) +[a [] b [c] d [e f] g] +user> `[~a] +[8] +user> `[(~a)] +[(8)] +user> `([~a]) +([8]) +user> `[a ~a a] +[a 8 a] +user> `([a ~a a]) +([a 8 a]) +user> `[(a ~a a)] +[(a 8 a)] +user> (def! c '(1 "b" "d")) +(1 "b" "d") +user> `[~@c] +[1 "b" "d"] +user> `[(~@c)] +[(1 "b" "d")] +user> `([~@c]) +([1 "b" "d"]) +user> `[1 ~@c 3] +[1 1 "b" "d" 3] +user> `([1 ~@c 3]) +([1 1 "b" "d" 3]) +user> `[(1 ~@c 3)] +[(1 1 "b" "d" 3)] +user> `(0 unquote) +(0 unquote) +user> `(0 splice-unquote) +(0 splice-unquote) +user> `[unquote 0] +[unquote 0] +user> `[splice-unquote 0] +[splice-unquote 0] +user> (quasiquoteexpand nil) +nil +user> (quasiquoteexpand 7) +7 +user> (quasiquoteexpand a) +(quote a) +user> (quasiquoteexpand {"a" b}) +(quote {"a" b}) +user> (quasiquoteexpand ()) +() +user> (quasiquoteexpand (1 2 3)) +(cons 1 (cons 2 (cons 3 ()))) +user> (quasiquoteexpand (a)) +(cons (quote a) ()) +user> (quasiquoteexpand (1 2 (3 4))) +(cons 1 (cons 2 (cons (cons 3 (cons 4 ())) ()))) +user> (quasiquoteexpand (nil)) +(cons nil ()) +user> (quasiquoteexpand (1 ())) +(cons 1 (cons () ())) +user> (quasiquoteexpand (() 1)) +(cons () (cons 1 ())) +user> (quasiquoteexpand (1 () 2)) +(cons 1 (cons () (cons 2 ()))) +user> (quasiquoteexpand (())) +(cons () ()) +user> (quasiquoteexpand (f () g (h) i (j k) l)) +(cons (quote f) (cons () (cons (quote g) (cons (cons (quote h) ()) (cons (quote i) (cons (cons (quote j) (cons (quote k) ())) (cons (quote l) ()))))))) +user> (quasiquoteexpand (unquote 7)) +7 +user> (quasiquoteexpand a) +(quote a) +user> (quasiquoteexpand (unquote a)) +a +user> (quasiquoteexpand (1 a 3)) +(cons 1 (cons (quote a) (cons 3 ()))) +user> (quasiquoteexpand (1 (unquote a) 3)) +(cons 1 (cons a (cons 3 ()))) +user> (quasiquoteexpand (1 b 3)) +(cons 1 (cons (quote b) (cons 3 ()))) +user> (quasiquoteexpand (1 (unquote b) 3)) +(cons 1 (cons b (cons 3 ()))) +user> (quasiquoteexpand ((unquote 1) (unquote 2))) +(cons 1 (cons 2 ())) +user> (quasiquoteexpand (a (splice-unquote (b c)) d)) +(cons (quote a) (concat (b c) (cons (quote d) ()))) +user> (quasiquoteexpand (1 c 3)) +(cons 1 (cons (quote c) (cons 3 ()))) +user> (quasiquoteexpand (1 (splice-unquote c) 3)) +(cons 1 (concat c (cons 3 ()))) +user> (quasiquoteexpand (1 (splice-unquote c))) +(cons 1 (concat c ())) +user> (quasiquoteexpand ((splice-unquote c) 2)) +(concat c (cons 2 ())) +user> (quasiquoteexpand ((splice-unquote c) (splice-unquote c))) +(concat c (concat c ())) +user> (quasiquoteexpand []) +(vec ()) +user> (quasiquoteexpand [[]]) +(vec (cons (vec ()) ())) +user> (quasiquoteexpand [()]) +(vec (cons () ())) +user> (quasiquoteexpand ([])) +(cons (vec ()) ()) +user> (quasiquoteexpand [1 a 3]) +(vec (cons 1 (cons (quote a) (cons 3 ())))) +user> (quasiquoteexpand [a [] b [c] d [e f] g]) +(vec (cons (quote a) (cons (vec ()) (cons (quote b) (cons (vec (cons (quote c) ())) (cons (quote d) (cons (vec (cons (quote e) (cons (quote f) ()))) (cons (quote g) ())))))))) +user> +Check file:///mal/impls/deno/step8_macros.ts +user> (defmacro! one (fn* () 1)) +# +user> (one) +1 +user> (defmacro! two (fn* () 2)) +# +user> (two) +2 +user> (defmacro! unless (fn* (pred a b) `(if ~pred ~b ~a))) +# +user> (unless false 7 8) +7 +user> (unless true 7 8) +8 +user> (defmacro! unless2 (fn* (pred a b) (list 'if (list 'not pred) a b))) +# +user> (unless2 false 7 8) +7 +user> (unless2 true 7 8) +8 +user> (macroexpand (one)) +1 +user> (macroexpand (unless PRED A B)) +(if PRED B A) +user> (macroexpand (unless2 PRED A B)) +(if (not PRED) A B) +user> (macroexpand (unless2 2 3 4)) +(if (not 2) 3 4) +user> (defmacro! identity (fn* (x) x)) +# +user> (let* (a 123) (macroexpand (identity a))) +a +user> (let* (a 123) (identity a)) +123 +user> () +() +user> `(1) +(1) +user> (not (= 1 1)) +false +user> (not (= 1 2)) +true +user> (nth (list 1) 0) +1 +user> (nth (list 1 2) 1) +2 +user> (nth (list 1 2 nil) 2) +nil +user> (def! x "x") +"x" +user> (def! x (nth (list 1 2) 2)) +Index Out Of Range: nth: 2 exceeds bounds of {"tag":"MalList","items":[{"tag":"MalNumber","value":1},{"tag":"MalNumber","value":2}]} +user> x +"x" +user> (first (list)) +nil +user> (first (list 6)) +6 +user> (first (list 7 8 9)) +7 +user> (rest (list)) +() +user> (rest (list 6)) +() +user> (rest (list 7 8 9)) +(8 9) +user> (macroexpand (cond)) +nil +user> (cond) +nil +user> (macroexpand (cond X Y)) +(if X Y (cond)) +user> (cond true 7) +7 +user> (cond false 7) +nil +user> (macroexpand (cond X Y Z T)) +(if X Y (cond Z T)) +user> (cond true 7 true 8) +7 +user> (cond false 7 true 8) +8 +user> (cond false 7 false 8 "else" 9) +9 +user> (cond false 7 (= 2 2) 8 "else" 9) +8 +user> (cond false 7 false 8 false 9) +nil +user> (let* (x (cond false "no" true "yes")) x) +"yes" +user> (nth [1] 0) +1 +user> (nth [1 2] 1) +2 +user> (nth [1 2 nil] 2) +nil +user> (def! x "x") +"x" +user> (def! x (nth [1 2] 2)) +Index Out Of Range: nth: 2 exceeds bounds of {"tag":"MalVector","items":[{"tag":"MalNumber","value":1},{"tag":"MalNumber","value":2}]} +user> x +"x" +user> (first []) +nil +user> (first nil) +nil +user> (first [10]) +10 +user> (first [10 11 12]) +10 +user> (rest []) +() +user> (rest nil) +() +user> (rest [10]) +() +user> (rest [10 11 12]) +(11 12) +user> (rest (cons 10 [11 12])) +(11 12) +user> (let* [x (cond false "no" true "yes")] x) +"yes" +user> (def! x 2) +2 +user> (defmacro! a (fn* [] x)) +# +user> (a) +2 +user> (let* (x 3) (a)) +2 +user> +Check file:///mal/impls/deno/step9_try.ts +user> (throw "err1") +Exception: "err1" +user> (try* 123 (catch* e 456)) +123 +user> (try* abc (catch* exc (prn "exc is:" exc))) +"exc is:" "'abc' not found" +nil +user> (try* (abc 1 2) (catch* exc (prn "exc is:" exc))) +"exc is:" "'abc' not found" +nil +user> (try* (nth () 1) (catch* exc (prn "exc is:" exc))) +"exc is:" "Index Out Of Range: nth: 1 exceeds bounds of {\"tag\":\"MalList\",\"items\":[]}" +nil +user> (try* (throw "my exception") (catch* exc (do (prn "exc:" exc) 7))) +"exc:" "my exception" +7 +user> (try* (do (try* "t1" (catch* e "c1")) (throw "e1")) (catch* e "c2")) +"c2" +user> (try* (try* (throw "e1") (catch* e (throw "e2"))) (catch* e "c2")) +"c2" +user> (try* (map throw (list "my err")) (catch* exc exc)) +"my err" +user> (symbol? 'abc) +true +user> (symbol? "abc") +false +user> (nil? nil) +true +user> (nil? true) +false +user> (true? true) +true +user> (true? false) +false +user> (true? true?) +false +user> (false? false) +true +user> (false? true) +false +user> (apply + (list 2 3)) +5 +user> (apply + 4 (list 5)) +9 +user> (apply prn (list 1 2 "3" (list))) +1 2 "3" () +nil +user> (apply prn 1 2 (list "3" (list))) +1 2 "3" () +nil +user> (apply list (list)) +() +user> (apply symbol? (list (quote two))) +true +user> (apply (fn* (a b) (+ a b)) (list 2 3)) +5 +user> (apply (fn* (a b) (+ a b)) 4 (list 5)) +9 +user> (def! nums (list 1 2 3)) +(1 2 3) +user> (def! double (fn* (a) (* 2 a))) +# +user> (double 3) +6 +user> (map double nums) +(2 4 6) +user> (map (fn* (x) (symbol? x)) (list 1 (quote two) "three")) +(false true false) +user> (throw {:msg "err2"}) +Exception: {:msg "err2"} +user> (symbol? :abc) +false +user> (symbol? 'abc) +true +user> (symbol? "abc") +false +user> (symbol? (symbol "abc")) +true +user> (keyword? :abc) +true +user> (keyword? 'abc) +false +user> (keyword? "abc") +false +user> (keyword? "") +false +user> (keyword? (keyword "abc")) +true +user> (symbol "abc") +abc +user> (keyword "abc") +:abc +user> (sequential? (list 1 2 3)) +true +user> (sequential? [15]) +true +user> (sequential? sequential?) +false +user> (sequential? nil) +false +user> (sequential? "abc") +false +user> (apply + 4 [5]) +9 +user> (apply prn 1 2 ["3" 4]) +1 2 "3" 4 +nil +user> (apply list []) +() +user> (apply (fn* (a b) (+ a b)) [2 3]) +5 +user> (apply (fn* (a b) (+ a b)) 4 [5]) +9 +user> (map (fn* (a) (* 2 a)) [1 2 3]) +(2 4 6) +user> (map (fn* [& args] (list? args)) [1 2]) +(true true) +user> (vector? [10 11]) +true +user> (vector? '(12 13)) +false +user> (vector 3 4 5) +[3 4 5] +user> (map? {}) +true +user> (map? '()) +false +user> (map? []) +false +user> (map? 'abc) +false +user> (map? :abc) +false +user> (hash-map "a" 1) +{"a" 1} +user> {"a" 1} +{"a" 1} +user> (assoc {} "a" 1) +{"a" 1} +user> (get (assoc (assoc {"a" 1 } "b" 2) "c" 3) "a") +1 +user> (def! hm1 (hash-map)) +{} +user> (map? hm1) +true +user> (map? 1) +false +user> (map? "abc") +false +user> (get nil "a") +nil +user> (get hm1 "a") +nil +user> (contains? hm1 "a") +false +user> (def! hm2 (assoc hm1 "a" 1)) +{"a" 1} +user> (get hm1 "a") +nil +user> (contains? hm1 "a") +false +user> (get hm2 "a") +1 +user> (contains? hm2 "a") +true +user> (keys hm1) +() +user> (keys hm2) +("a") +user> (keys {"1" 1}) +("1") +user> (vals hm1) +() +user> (vals hm2) +(1) +user> (count (keys (assoc hm2 "b" 2 "c" 3))) +3 +user> (get {:abc 123} :abc) +123 +user> (contains? {:abc 123} :abc) +true +user> (contains? {:abcd 123} :abc) +false +user> (assoc {} :bcd 234) +{:bcd 234} +user> (keyword? (nth (keys {:abc 123 :def 456}) 0)) +true +user> (keyword? (nth (vals {"a" :abc "b" :def}) 0)) +true +user> (def! hm4 (assoc {:a 1 :b 2} :a 3 :c 1)) +{:a 3 :b 2 :c 1} +user> (get hm4 :a) +3 +user> (get hm4 :b) +2 +user> (get hm4 :c) +1 +user> (contains? {:abc nil} :abc) +true +user> (assoc {} :bcd nil) +{:bcd nil} +user> (str "A" {:abc "val"} "Z") +"A{:abc val}Z" +user> (str true "." false "." nil "." :keyw "." 'symb) +"true.false.nil.:keyw.symb" +user> (pr-str "A" {:abc "val"} "Z") +"\"A\" {:abc \"val\"} \"Z\"" +user> (pr-str true "." false "." nil "." :keyw "." 'symb) +"true \".\" false \".\" nil \".\" :keyw \".\" symb" +user> (def! s (str {:abc "val1" :def "val2"})) +"{:abc val1 :def val2}" +user> (cond (= s "{:abc val1 :def val2}") true (= s "{:def val2 :abc val1}") true) +true +user> (def! p (pr-str {:abc "val1" :def "val2"})) +"{:abc \"val1\" :def \"val2\"}" +user> (cond (= p "{:abc \"val1\" :def \"val2\"}") true (= p "{:def \"val2\" :abc \"val1\"}") true) +true +user> (apply (fn* (& more) (list? more)) [1 2 3]) +true +user> (apply (fn* (& more) (list? more)) []) +true +user> (apply (fn* (a & more) (list? more)) [1]) +true +user> (try* xyz) +Exception: 'xyz' not found +user> (try* (throw (list 1 2 3)) (catch* exc (do (prn "err:" exc) 7))) +"err:" (1 2 3) +7 +user> (def! hm3 (assoc hm2 "b" 2)) +{"a" 1 "b" 2} +user> (count (keys hm3)) +2 +user> (count (vals hm3)) +2 +user> (dissoc hm3 "a") +{"b" 2} +user> (dissoc hm3 "a" "b") +{} +user> (dissoc hm3 "a" "b" "c") +{} +user> (count (keys hm3)) +2 +user> (dissoc {:cde 345 :fgh 456} :cde) +{:fgh 456} +user> (dissoc {:cde nil :fgh 456} :cde) +{:fgh 456} +user> (= {} {}) +true +user> (= {:a 11 :b 22} (hash-map :b 22 :a 11)) +true +user> (= {:a 11 :b [22 33]} (hash-map :b [22 33] :a 11)) +true +user> (= {:a 11 :b {:c 33}} (hash-map :b {:c 33} :a 11)) +true +user> (= {:a 11 :b 22} (hash-map :b 23 :a 11)) +false +user> (= {:a 11 :b 22} (hash-map :a 11)) +false +user> (= {:a [11 22]} {:a (list 11 22)}) +true +user> (= {:a 11 :b 22} (list :a 11 :b 22)) +false +user> (= {} []) +false +user> (= [] {}) +false +user> (keyword :abc) +:abc +user> (keyword? (first (keys {":abc" 123 ":def" 456}))) +false +user> +Check file:///mal/impls/deno/stepA_mal.ts +Mal [deno] +user> (readline "mal-user> ") +mal-user> > "hello" +"\"hello\"" +user> (= "something bogus" *host-language*) +false +user> (def! e (atom {"+" +})) +(atom {"+" #}) +user> (swap! e assoc "-" -) +{"+" # "-" #} +user> ( (get @e "+") 7 8) +15 +user> ( (get @e "-") 11 8) +3 +user> (swap! e assoc "foo" (list)) +{"+" # "-" # "foo" ()} +user> (get @e "foo") +() +user> (swap! e assoc "bar" '(1 2 3)) +{"+" # "-" # "foo" () "bar" (1 2 3)} +user> (get @e "bar") +(1 2 3) +user> (do (list time-ms string? number? seq conj meta with-meta fn?) nil) +nil +user> (meta (fn* (a) a)) +nil +user> (meta (with-meta (fn* (a) a) {"b" 1})) +{"b" 1} +user> (meta (with-meta (fn* (a) a) "abc")) +"abc" +user> (def! l-wm (with-meta (fn* (a) a) {"b" 2})) +# +user> (meta l-wm) +{"b" 2} +user> (meta (with-meta l-wm {"new_meta" 123})) +{"new_meta" 123} +user> (meta l-wm) +{"b" 2} +user> (def! f-wm (with-meta (fn* [a] (+ 1 a)) {"abc" 1})) +# +user> (meta f-wm) +{"abc" 1} +user> (meta (with-meta f-wm {"new_meta" 123})) +{"new_meta" 123} +user> (meta f-wm) +{"abc" 1} +user> (def! f-wm2 ^{"abc" 1} (fn* [a] (+ 1 a))) +# +user> (meta f-wm2) +{"abc" 1} +user> (meta +) +nil +user> (def! gen-plusX (fn* (x) (with-meta (fn* (b) (+ x b)) {"meta" 1}))) +# +user> (def! plus7 (gen-plusX 7)) +# +user> (def! plus8 (gen-plusX 8)) +# +user> (plus7 8) +15 +user> (meta plus7) +{"meta" 1} +user> (meta plus8) +{"meta" 1} +user> (meta (with-meta plus7 {"meta" 2})) +{"meta" 2} +user> (meta plus8) +{"meta" 1} +user> (string? "") +true +user> (string? 'abc) +false +user> (string? "abc") +true +user> (string? :abc) +false +user> (string? (keyword "abc")) +false +user> (string? 234) +false +user> (string? nil) +false +user> (number? 123) +true +user> (number? -1) +true +user> (number? nil) +false +user> (number? false) +false +user> (number? "123") +false +user> (def! add1 (fn* (x) (+ x 1))) +# +user> (fn? +) +true +user> (fn? add1) +true +user> (fn? cond) +false +user> (fn? "+") +false +user> (fn? :+) +false +user> (fn? ^{"ismacro" true} (fn* () 0)) +true +user> (macro? cond) +true +user> (macro? +) +false +user> (macro? add1) +false +user> (macro? "+") +false +user> (macro? :+) +false +user> (macro? {}) +false +user> (conj (list) 1) +(1) +user> (conj (list 1) 2) +(2 1) +user> (conj (list 2 3) 4) +(4 2 3) +user> (conj (list 2 3) 4 5 6) +(6 5 4 2 3) +user> (conj (list 1) (list 2 3)) +((2 3) 1) +user> (conj [] 1) +[1] +user> (conj [1] 2) +[1 2] +user> (conj [2 3] 4) +[2 3 4] +user> (conj [2 3] 4 5 6) +[2 3 4 5 6] +user> (conj [1] [2 3]) +[1 [2 3]] +user> (seq "abc") +("a" "b" "c") +user> (apply str (seq "this is a test")) +"this is a test" +user> (seq '(2 3 4)) +(2 3 4) +user> (seq [2 3 4]) +(2 3 4) +user> (seq "") +nil +user> (seq '()) +nil +user> (seq []) +nil +user> (seq nil) +nil +user> (meta [1 2 3]) +nil +user> (with-meta [1 2 3] {"a" 1}) +[1 2 3] +user> (meta (with-meta [1 2 3] {"a" 1})) +{"a" 1} +user> (vector? (with-meta [1 2 3] {"a" 1})) +true +user> (meta (with-meta [1 2 3] "abc")) +"abc" +user> (with-meta [] "abc") +[] +user> (meta (with-meta (list 1 2 3) {"a" 1})) +{"a" 1} +user> (list? (with-meta (list 1 2 3) {"a" 1})) +true +user> (with-meta (list) {"a" 1}) +() +user> (empty? (with-meta (list) {"a" 1})) +true +user> (meta (with-meta {"abc" 123} {"a" 1})) +{"a" 1} +user> (map? (with-meta {"abc" 123} {"a" 1})) +true +user> (with-meta {} {"a" 1}) +{} +user> (def! l-wm (with-meta [4 5 6] {"b" 2})) +[4 5 6] +user> (meta l-wm) +{"b" 2} +user> (meta (with-meta l-wm {"new_meta" 123})) +{"new_meta" 123} +user> (meta l-wm) +{"b" 2} +user> (meta +) +nil +user> (def! f-wm3 ^{"def" 2} +) +# +user> (meta f-wm3) +{"def" 2} +user> (meta +) +nil +user> (load-file "../tests/computations.mal") +nil +user> (def! start-time (time-ms)) +2663.663896 +user> (= start-time 0) +false +user> (sumdown 10) ; Waste some time +55 +user> (> (time-ms) start-time) +true +user> (def! f (fn* [x] (number? x))) +# +user> (defmacro! m f) +# +user> (f (+ 1 1)) +true +user> (m (+ 1 1)) +false +user>