From df9e91ebd54969dd32c62edc221dbed4568a5efa Mon Sep 17 00:00:00 2001 From: didier amyot Date: Mon, 26 May 2025 10:28:11 -0400 Subject: [PATCH] Implemented step 2. --- step1_read_print.ts => src/reader-printer.ts | 77 +++++++---------- step0_repl.ts => src/step0_repl.ts | 8 +- src/step1_read_print.ts | 25 ++++++ src/step2_eval.ts | 90 ++++++++++++++++++++ 4 files changed, 149 insertions(+), 51 deletions(-) rename step1_read_print.ts => src/reader-printer.ts (78%) rename step0_repl.ts => src/step0_repl.ts (80%) create mode 100644 src/step1_read_print.ts create mode 100644 src/step2_eval.ts diff --git a/step1_read_print.ts b/src/reader-printer.ts similarity index 78% rename from step1_read_print.ts rename to src/reader-printer.ts index 6c58fb0..dcddc45 100644 --- a/step1_read_print.ts +++ b/src/reader-printer.ts @@ -1,12 +1,11 @@ -import {readUserInputs} from "./src/shared.ts"; -import {assertEquals} from "jsr:@std/assert"; +import {assertEquals} from "https://deno.land/std@0.224.0/assert/assert_equals.ts"; -type StringToken = { type: "string"; value: string }; -type NumberToken = { type: "number"; value: number }; -type BooleanToken = { type: "boolean"; value: boolean }; -type SymToken = { type: "sym"; value: string }; -type NilToken = { type: "nil" }; -type ListToken = { +export type StringToken = { type: "string"; value: string }; +export type NumberToken = { type: "number"; value: number }; +export type BooleanToken = { type: "boolean"; value: boolean }; +export type SymToken = { type: "sym"; value: string }; +export type NilToken = { type: "nil"; value: null }; +export type ListToken = { type: "list"; value: ( | StringToken @@ -15,14 +14,19 @@ type ListToken = { | SymToken | NilToken | ListToken + | FunctionToken )[]; }; -type AST = + +export type FunctionToken = {type: 'function', value: (...args: any[]) => any}; + +export type AST = | NilToken | StringToken | NumberToken | BooleanToken | SymToken + | FunctionToken | ListToken; function findStringEnd(s: string): number { @@ -51,8 +55,8 @@ function findTokenEnd(source: string): number { return ret !== -1 ? ret : source.length; } -function isNumber(token: string): boolean{ - return /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/.test(token) +function isNumber(token: string): boolean { + return /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/.test(token); } function findNextNoneStringToken(source: string): AST { @@ -60,7 +64,7 @@ function findNextNoneStringToken(source: string): AST { const token = source.substring(0, tokenEnd); if (token === "nil") { - return { type: "nil" }; + return { type: "nil", value: null }; } if (token === "true" || token === "false") { @@ -70,14 +74,13 @@ function findNextNoneStringToken(source: string): AST { return { type: "number", value: JSON.parse(token) }; } return { type: "sym", value: token }; - } function findNextToken(source: string): [AST, string] { if (source.startsWith("(")) { let x = source.substring(1).trim(); let token; - const lst = []; + const lst: AST[] = []; while (!x.trim().startsWith(")")) { [token, x] = findNextToken(x); lst.push(token); @@ -96,20 +99,16 @@ function findNextToken(source: string): [AST, string] { return [findNextNoneStringToken(token), source.substring(tokenEnd).trim()]; } -function read(source: string): AST { +export function readAST(source: string): AST { source = source.trim(); - const [ret, rest] = findNextToken(source); + const [ret, _] = findNextToken(source); return ret; } -function eval_(ast: AST): AST { - return ast; -} - -function print(form: AST): string { +export function printAST(form: AST): string { if (form.type === "list") { - const ret = form.value.map(print).join(" "); + const ret = form.value.map(printAST).join(" "); return `(${ret})`; } if (form.type === "sym") { @@ -121,10 +120,6 @@ function print(form: AST): string { return JSON.stringify(form.value); } -function rep(source: string): string { - return print(eval_(read(source))); -} - Deno.test("basic strings", function (): void { const testCases: [string, string][] = [ ['"foo"', "foo"], @@ -133,7 +128,7 @@ Deno.test("basic strings", function (): void { ]; for (const [input, exptValue] of testCases) { const expt: StringToken = { type: "string", value: exptValue }; - assertEquals(expt, read(input)); + assertEquals(expt, readAST(input)); } }); @@ -145,7 +140,7 @@ Deno.test("basic symbols", function (): void { ]; for (const [input, exptValue] of testCases) { const expt: SymToken = { type: "sym", value: exptValue }; - assertEquals(expt, read(input)); + assertEquals(expt, readAST(input)); } }); @@ -157,7 +152,7 @@ Deno.test("basic numbers", function (): void { ]; for (const [input, exptValue] of testCases) { const expt: NumberToken = { type: "number", value: exptValue }; - assertEquals(expt, read(input)); + assertEquals(expt, readAST(input)); } }); @@ -168,7 +163,7 @@ Deno.test("basic booleans", function (): void { ]; for (const [input, exptValue] of testCases) { const expt: BooleanToken = { type: "boolean", value: exptValue }; - assertEquals(expt, read(input)); + assertEquals(expt, readAST(input)); } }); @@ -191,7 +186,7 @@ Deno.test("basic list", function (): void { numberToken(3), numberToken(4), ]); - assertEquals(expt, read(input)); + assertEquals(expt, readAST(input)); }); Deno.test("list of numbers", function (): void { @@ -201,7 +196,7 @@ Deno.test("list of numbers", function (): void { numberToken(2), numberToken(3), ]); - assertEquals(expt, read(input)); + assertEquals(expt, readAST(input)); }); Deno.test("composed list", function (): void { @@ -211,12 +206,12 @@ Deno.test("composed list", function (): void { listToken([symToken("mul"), numberToken(4), numberToken(9)]), listToken([symToken("inc"), numberToken(-5)]), ]); - assertEquals(expt, read(input)); + assertEquals(expt, readAST(input)); }); Deno.test("basic nil", function () { - const expt: NilToken = { type: "nil" }; - assertEquals(expt, read("nil")); + const expt: NilToken = { type: "nil", value: null }; + assertEquals(expt, readAST("nil")); }); Deno.test("step 1 tests", function (): void { @@ -227,17 +222,9 @@ Deno.test("step 1 tests", function (): void { [" abc ", "abc"], ["(123 456)", "(123 456)"], ["( 123 456 789 )", "(123 456 789)"], - ["( + 2 (* 3 4) ) ", "(+ 2 (* 3 4))"] + ["( + 2 (* 3 4) ) ", "(+ 2 (* 3 4))"], ]; for (const [input, expt] of cases) { - assertEquals(expt, print(read(input))); + assertEquals(expt, printAST(readAST(input))); } }); - -function main(): void { - for (const userInput of readUserInputs()) { - rep(userInput); - } -} - -// main(); diff --git a/step0_repl.ts b/src/step0_repl.ts similarity index 80% rename from step0_repl.ts rename to src/step0_repl.ts index b998ad6..2f4bb3b 100644 --- a/step0_repl.ts +++ b/src/step0_repl.ts @@ -1,4 +1,4 @@ -import {readUserInputs} from "./src/shared.ts"; +import {readUserInputs} from "./shared.ts"; function read(source: string): string { return source; @@ -18,12 +18,8 @@ function rep(source: string): string{ -function main(): void { +export function main(): void { for(const userInput of readUserInputs()){ rep(userInput); } - - } - -main(); \ No newline at end of file diff --git a/src/step1_read_print.ts b/src/step1_read_print.ts new file mode 100644 index 0000000..8bad32b --- /dev/null +++ b/src/step1_read_print.ts @@ -0,0 +1,25 @@ +import {readUserInputs} from "./shared.ts"; +import {AST, readAST, printAST} from './reader-printer.ts'; + +function read(source: string): AST { + return readAST(source); +} + +function eval_(ast: AST): AST { + return ast; +} + +function print(form: AST): string { + return printAST(form); +} + +function rep(source: string): string { + return print(eval_(read(source))); +} + + +export function main(): void { + for (const userInput of readUserInputs()) { + rep(userInput); + } +} diff --git a/src/step2_eval.ts b/src/step2_eval.ts new file mode 100644 index 0000000..a6b2483 --- /dev/null +++ b/src/step2_eval.ts @@ -0,0 +1,90 @@ +import {assertEquals} from "https://deno.land/std@0.224.0/assert/assert_equals.ts"; +import {AST, FunctionToken, printAST, readAST} from "./reader-printer.ts"; + +type MalEnv = Map; + +function functionToken(fn: (...args: any[]) => any): FunctionToken { + return { type: "function", value: fn }; +} + +function isBoolean(v: unknown): v is boolean { + return v === true || v === false; +} + +function isNumber(v: unknown): v is number { + return typeof v === "number" && isFinite(v); +} + +function isString(v: unknown): v is string { + return typeof v === "string" || v instanceof String; +} + +class REPL { + private readonly env: MalEnv; + + constructor(env: MalEnv) { + this.env = env; + } + + static mathRepl(): REPL { + const mathFunctions = new Map(); + mathFunctions.set("+", functionToken((a, b) => a + b)); + mathFunctions.set("-", functionToken((a, b) => a - b)); + mathFunctions.set("*", functionToken((a, b) => a * b)); + mathFunctions.set("/", functionToken((a, b) => a / b)); + + return new REPL(mathFunctions); + } + + read(source: string): AST { + return readAST(source); + } + + cast(value: unknown): AST { + if (value === true || value === false) { + return { type: "boolean", value: value }; + } + if (isBoolean(value)) { + return { type: "boolean", value: value }; + } + if (isString(value)) { + return { type: "string", value: value }; + } + if (isNumber(value)) { + return { type: "number", value: value }; + } + throw new Error(`Unknown return type from functon: ${value}`); + } + + eval_(ast: AST): AST { + if (ast.type === "list") { + const fn = this.eval_(ast.value[0]) as FunctionToken; + const ret = fn.value( + ...ast.value.slice(1).map((n) => this.eval_(n).value), + ); + return this.cast(ret); + } + if (ast.type === "sym") { + const ret = this.env.get(ast.value); + if (ret === undefined) { + throw new Error(`Unknown reference ${ast.value}`); + } + return ret; + } + return ast; + } + + print(form: AST): string { + return printAST(form); + } + + rep(source: string): string { + return this.print(this.eval_(this.read(source))); + } +} + +Deno.test("basic", function (): void { + const repl = REPL.mathRepl(); + assertEquals(repl.rep("(+ 1 1)"), "2"); + assertEquals(repl.rep("(+ 2 (* 3 4))"), "14"); +});