diff --git a/.idea/deno.xml b/.idea/deno.xml new file mode 100644 index 0000000..2e4b145 --- /dev/null +++ b/.idea/deno.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/shared.ts b/src/shared.ts new file mode 100644 index 0000000..cf8bd92 --- /dev/null +++ b/src/shared.ts @@ -0,0 +1,10 @@ + +export function* readUserInputs(): Generator{ + while(true){ + const u = prompt("user> ") + if(u === null){ + return + } + yield u + } +} \ No newline at end of file diff --git a/step0_repl.ts b/step0_repl.ts index e7b346c..b998ad6 100644 --- a/step0_repl.ts +++ b/step0_repl.ts @@ -1,3 +1,5 @@ +import {readUserInputs} from "./src/shared.ts"; + function read(source: string): string { return source; } @@ -14,15 +16,6 @@ function rep(source: string): string{ return print(eval_(read( source))) } -function* readUserInputs(): Generator{ - while(true){ - const u = prompt("user> ") - if(u === null){ - return - } - yield u - } -} function main(): void { diff --git a/step1_read_print.ts b/step1_read_print.ts new file mode 100644 index 0000000..6c58fb0 --- /dev/null +++ b/step1_read_print.ts @@ -0,0 +1,243 @@ +import {readUserInputs} from "./src/shared.ts"; +import {assertEquals} from "jsr:@std/assert"; + +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 = { + type: "list"; + value: ( + | StringToken + | NumberToken + | BooleanToken + | SymToken + | NilToken + | ListToken + )[]; +}; +type AST = + | NilToken + | StringToken + | NumberToken + | BooleanToken + | SymToken + | ListToken; + +function findStringEnd(s: string): number { + let idx = 1; + let previousChar = ""; + for (const c of Array.from(s.substring(1))) { + if ('"' === c && previousChar !== "\\") { + return idx; + } + previousChar = c; + idx++; + } + throw new Error(`Syntax error. The string ${s} never ends.`); +} + +function findNextString(source: string): [string, string] { + const stringEnd = findStringEnd(source); + const ret = JSON.parse(source.substring(0, stringEnd + 1)); + return [ret, source.substring(stringEnd + 1).trim()]; +} + +function findTokenEnd(source: string): number { + // const match = source.match(/(\s|\))/gi); + const re = /(\s|\))/gi; + const ret = source.search(re); + return ret !== -1 ? ret : source.length; +} + +function isNumber(token: string): boolean{ + return /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/.test(token) +} + +function findNextNoneStringToken(source: string): AST { + const tokenEnd = findTokenEnd(source); + const token = source.substring(0, tokenEnd); + + if (token === "nil") { + return { type: "nil" }; + } + + if (token === "true" || token === "false") { + return { type: "boolean", value: token === "true" }; + } + if (isNumber(token)) { + 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 = []; + while (!x.trim().startsWith(")")) { + [token, x] = findNextToken(x); + lst.push(token); + x = x.trim(); + } + return [{ type: "list", value: lst }, x.trim().substring(1)]; + } + + if (source.startsWith('"')) { + const [ret, rest] = findNextString(source); + return [{ type: "string", value: ret }, rest]; + } + + const tokenEnd = findTokenEnd(source); + const token = source.substring(0, tokenEnd); + return [findNextNoneStringToken(token), source.substring(tokenEnd).trim()]; +} + +function read(source: string): AST { + source = source.trim(); + const [ret, rest] = findNextToken(source); + + return ret; +} + +function eval_(ast: AST): AST { + return ast; +} + +function print(form: AST): string { + if (form.type === "list") { + const ret = form.value.map(print).join(" "); + return `(${ret})`; + } + if (form.type === "sym") { + return form.value; + } + if (form.type === "nil") { + return "nil"; + } + 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"], + [' "foo" ', "foo"], + ['"bonjour \\"poline\\""', 'bonjour "poline"'], + ]; + for (const [input, exptValue] of testCases) { + const expt: StringToken = { type: "string", value: exptValue }; + assertEquals(expt, read(input)); + } +}); + +Deno.test("basic symbols", function (): void { + const testCases: [string, string][] = [ + ["foo", "foo"], + ["trueFalse", "trueFalse"], + ["nil?", "nil?"], + ]; + for (const [input, exptValue] of testCases) { + const expt: SymToken = { type: "sym", value: exptValue }; + assertEquals(expt, read(input)); + } +}); + +Deno.test("basic numbers", function (): void { + const testCases: [string, number][] = [ + ["567", 567], + ["-34", -34], + ["-34.5", -34.5], + ]; + for (const [input, exptValue] of testCases) { + const expt: NumberToken = { type: "number", value: exptValue }; + assertEquals(expt, read(input)); + } +}); + +Deno.test("basic booleans", function (): void { + const testCases: [string, boolean][] = [ + ["true", true], + ["false", false], + ]; + for (const [input, exptValue] of testCases) { + const expt: BooleanToken = { type: "boolean", value: exptValue }; + assertEquals(expt, read(input)); + } +}); + +function numberToken(value: number): NumberToken { + return { type: "number", value }; +} + +function symToken(value: string): SymToken { + return { type: "sym", value }; +} + +function listToken(value: (NumberToken | SymToken | ListToken)[]): ListToken { + return { type: "list", value: value }; +} + +Deno.test("basic list", function (): void { + const input = "(add 3 4)"; + const expt = listToken([ + symToken("add"), + numberToken(3), + numberToken(4), + ]); + assertEquals(expt, read(input)); +}); + +Deno.test("list of numbers", function (): void { + const input = "( 1 2 3)"; + const expt = listToken([ + numberToken(1), + numberToken(2), + numberToken(3), + ]); + assertEquals(expt, read(input)); +}); + +Deno.test("composed list", function (): void { + const input = "(add (mul 4 9) (inc -5))"; + const expt = listToken([ + symToken("add"), + listToken([symToken("mul"), numberToken(4), numberToken(9)]), + listToken([symToken("inc"), numberToken(-5)]), + ]); + assertEquals(expt, read(input)); +}); + +Deno.test("basic nil", function () { + const expt: NilToken = { type: "nil" }; + assertEquals(expt, read("nil")); +}); + +Deno.test("step 1 tests", function (): void { + const cases: [string, string][] = [ + ["123", "123"], + [" 123 ", "123"], + ["abc", "abc"], + [" abc ", "abc"], + ["(123 456)", "(123 456)"], + ["( 123 456 789 )", "(123 456 789)"], + ["( + 2 (* 3 4) ) ", "(+ 2 (* 3 4))"] + ]; + for (const [input, expt] of cases) { + assertEquals(expt, print(read(input))); + } +}); + +function main(): void { + for (const userInput of readUserInputs()) { + rep(userInput); + } +} + +// main();