"use strict"; import * as fs from "fs"; function main() { const text = fs.readFileSync(process.argv[2]).toString(); const ast = new Parser(text).parse(); let lastValue = null; const evaluator = new Evaluator(); for (const expr of ast) { const result = evaluator.eval(expr); if (result.type !== "value") { break; } lastValue = result.value; } if (lastValue !== null) { console.log(valueToJs(lastValue)); } } class Evaluator { constructor() { this.syms = { parent: undefined, map: new Map(builtins) }; } /** * @param {Expr} expr */ eval(expr) { if (expr.type === "list") { return this.evalList(expr, expr.line); } else if (expr.type === "int") { return { type: "value", value: { type: "int", value: expr.value } }; } else if (expr.type === "string") { return { type: "value", value: { type: "string", value: expr.value } }; } else if (expr.type === "ident") { const findInTree = (syms, ident) => { if (syms.map.has(ident)) return syms.map.get(ident); else if (syms.parent) return findInTree(syms.parent, ident); else return undefined; } const sym = findInTree(this.syms, expr.value); if (!sym) throw new Error(`could not find symbol '${expr.value}' on line ${expr.line}`); return { type: "value", value: sym }; } else { throw new Error(`unknown expr type '${expr.type}' on line ${expr.line}`); } } evalToValue(expr) { const result = this.eval(expr); if (result.type !== "value") { throw new Error(`expected value on line ${expr.line}`); } return result.value; } evalList(expr) { const s = expr.values; const id = s[0]?.value ?? undefined; if (id === "fn") { const name = s[1].value; this.syms.map.set(name, { type: "fn", name, params: s[2].values.map(ident => ident.value), body: s[3], syms: this.syms, }); return { type: "value", value: { type: "null" } }; } else if (id === "call") { const args = s.slice(2).map(arg => this.evalToValue(arg)); const fnValue = this.evalToValue(s[1]); if (fnValue.type === "builtin") { return { type: "value", value: fnValue.fn(...args) }; } else if (fnValue.type !== "fn") { throw new Error("cannot call non-function"); } const callerSyms = this.syms; this.syms = { parent: fnValue.syms, map: new Map(), }; if (fnValue.params.length !== args.length) { throw new Error(`incorrect amount of arguments on line ${line}`); } for (let i = 0; i < fnValue.params.length; ++i) { this.syms.map.set(fnValue.params[i], args[i]); } let returnValue = { type: "null" }; const result = this.eval(fnValue.body); if (result.type === "value" || result.type === "return") { returnValue = result.value; } else { throw new Error(`illegal ${result.type} across boundry`) } this.syms = callerSyms; return { type: "value", value: returnValue }; } else if (id === "return") { return { type: "return", value: s[1] ? this.evalToValue(s[1]) : { type: "null" } }; } else if (id === "let") { const value = this.evalToValue(s[2]); this.syms.map.set(s[1].value, value); return { type: "value", value: { type: "null" } }; } else if (id === "do") { let lastValue = { type: "null" }; for (const expr of s.slice(1)) { const result = this.eval(expr); if (result.type !== "value") { break; } lastValue = result.value; } return { type: "value", value: lastValue }; } else if (s[0] === "if") { const cond = this.evalToValue(s[1]); if (cond.type !== "bool") { throw new Error(`expected bool on line ${line}`); } if (cond.value) { return this.eval(s[2]); } else if (s[3]) { return this.eval(s[3]); } else { return { type: "value", value: "null" }; } } else if (s[0] === "loop") { while (true) { const result = this.eval(s[1]); if (result.type === "break") { return { type: "value", value: result.value }; } else if (result.type !== "value") { return result; } } } else if (s[0] === "break") { return { type: "break", value: s[1] ? this.evalToValue(s[1]) : { type: "null" } }; } else { return { type: "value", value: { type: "list", values: s.map(expr => this.evalToValue(expr)) } }; } } } const builtinFns = { println(msg) { console.log(valueToPrint(msg)); return { type: "null" }; }, read_text_file(path) { const text = fs.readFileSync(path.value); return { type: "string", value: text }; } }; const builtins = Object.entries(builtinFns) .map(([key, fn]) => [key, { type: "builtin", fn }]); function valueToPrint(value) { if (value.type === "null") { return "null"; } else if (value.type === "bool") { return `${value.value}`; } else if (value.type === "int") { return `${value.value}`; } else if (value.type === "string") { return `${value.value}`; } else if (value.type === "list") { return `(${value.values.map(v => valueToString(v)).join(" ")})`; } else { throw new Error(`unknown value type ${value.type}`); } } function valueToString(value) { if (value.type === "null") { return "null"; } else if (value.type === "bool") { return `${value.value}`; } else if (value.type === "int") { return `${value.value}`; } else if (value.type === "string") { return `"${value.value}"`; } else if (value.type === "list") { return `(${value.values.map(v => valueToString(v)).join(" ")})`; } else { throw new Error(`unknown value type ${value.type}`); } } function valueToJs(value) { if (value.type === "null") { return null; } else if (value.type === "bool") { return value.value; } else if (value.type === "int") { return value.value; } else if (value.type === "string") { return value.value; } else if (value.type === "list") { return value.values.map(v => valueToJs(v)); } else { throw new Error(`unknown value type ${value.type}`); } } /** * @param {Expr} expr * @returns {string} */ function exprToString(expr) { if (expr.type === "ident") { return expr.value; } else if (expr.type === "int") { return `${expr.value}`; } else if (expr.type === "string") { return `"${expr.value}"`; } else if (expr.type === "list") { return `(${expr.values.map(v => exprToString(v)).join(" ")})`; } else { throw new Error(`unknown value type ${expr.type}`); } } /** * @typedef {{ type: string, line: number, value: any, values?: Expr } } Expr */ class Parser { constructor(text) { const stringExtractor = new StringExtractor(text); stringExtractor.extract(); this.strings = stringExtractor.getStrings(); this.text = stringExtractor.getOutputText(); this.tokens = this.text .replace(/\/\/.*?$/mg, "") .replace(/([\(\)\n])/g, " $1 ") .split(/[ \t\r]/) .filter(tok => tok !== ""); this.idx = 0; this.line = 1; } /** * * @returns {Expr[]} */ parse() { if (this.curr === "\n") this.step(); const exprs = []; while (!this.done) { exprs.push(this.parseExpr()); } return exprs; } parseExpr() { const line = this.line; if (this.eat("(")) { const values = []; while (!this.test(")")) { values.push(this.parseExpr()); } if (!this.test(")")) { throw new Error(`expected ')'`) } this.step(); return { type: "list", line, values }; } else if (this.test(/STRING_\d+/)) { const id = Number(this.curr.match(/STRING_(\d+)/)[1]); this.step(); return { type: "string", line, value: this.strings[id] }; } else if (this.test(/0|(:?[1-9][0-9]*)/)) { const value = Number(this.curr); this.step(); return { type: "int", line, value }; } else if (this.test(/[a-zA-Z0-9\+\-\*/%&\|=\?\!<>'_]+/)) { const value = this.curr; this.step(); return { type: "ident", line, value }; } else { throw new Error(`expected expression, got ${this.curr}`) } } eat(tok) { if (!this.test(tok)) return false; this.step() return true; } test(tok) { if (this.done) return false; if (typeof tok === "string") return this.curr === tok; else if (tok instanceof RegExp) return new RegExp(`^${tok.source}$`) .test(this.curr); else throw new Error() } step() { do { this.idx += 1; if (!this.done && this.curr === "\n") { this.line += 1; } } while (!this.done && this.curr === "\n"); } get done() { return this.idx >= this.tokens.length; } get curr() { return this.tokens[this.idx]; } } class StringExtractor { constructor(text) { this.text = text; this.idx = 0; this.outputText = ""; this.strings = []; } extract() { while (this.idx < this.text.length) { if (this.text[this.idx] == '\"') { this.extractString() } else { this.outputText += this.text[this.idx]; this.idx += 1; } } } extractString() { this.idx += 1; let value = ""; while (this.idx < this.text.length && this.text[this.idx] != '"') { if (this.text[this.idx] == '\\') { this.idx += 1; if (this.idx > this.text.length) break; const ch = this.text[this.idx]; value += { "0": "\0", "t": "\t", "r": "\r", "n": "\n", }[ch] ?? ch; } else { value += this.text[this.idx]; } this.idx += 1; } if (this.idx >= this.text.length || this.text[this.idx] != '"') { throw new Error("expected '\"'"); } this.idx += 1; const id = this.strings.length; this.strings.push(value); this.outputText += `STRING_${id}`; } getStrings() { return this.strings; } getOutputText() { return this.outputText; } } main();