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