"use strict"; import * as fs from "node:fs"; import process from "node:process"; 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 sym = this.findSym(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]?.type === "ident" ? 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 === "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 === "if") { const cond = this.evalToValue(s[1]); if (cond.type !== "bool") { throw new Error( `expected bool on line ${expr.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 (id === "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 (id === "break") { return { type: "break", value: s[1] ? this.evalToValue(s[1]) : { 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") { return result; } lastValue = result.value; } return { type: "value", value: lastValue }; } 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 === "not") { const value = this.evalToValue(s[1]); return { type: "value", value: { type: "bool", value: !value.value }, }; } else if (id === "or") { const left = this.evalToValue(s[1]); if (left.value) { return { type: "value", value: left }; } else { const right = this.evalToValue(s[2]); return { type: "value", value: right }; } } else if (id === "and") { const left = this.evalToValue(s[1]); if (left.value) { const right = this.evalToValue(s[2]); return { type: "value", value: right }; } else { return { type: "value", value: left }; } } else if (id in artithmeticOps) { const left = this.evalToValue(s[1]); const right = this.evalToValue(s[2]); return { type: "value", value: { type: "int", value: artithmeticOps[id](left.value, right.value), }, }; } else if (id in comparisonOps) { const left = this.evalToValue(s[1]); const right = this.evalToValue(s[2]); return { type: "value", value: { type: "bool", value: comparisonOps[id](left.value, right.value), }, }; } else if (id in assignOps) { if (s[1].type === "ident") { const sym = this.findSym(s[1].value); if (!sym) { throw new Error( `could not find symbol '${expr.value}' on line ${expr.line}`, ); } const right = this.evalToValue(s[2]); const newValue = assignOps[id](sym, right); sym.type = newValue.type; sym.value = newValue.value; } else { throw new Error( `cannot assign to expression on line ${expr.line}`, ); } return { type: "value", value: { type: "null" } }; } else { return { type: "value", value: { type: "list", values: s.map((expr) => this.evalToValue(expr)), }, }; } } findSym(ident, syms = this.syms) { if (syms.map.has(ident)) { return syms.map.get(ident); } else if (syms.parent) { return this.findSym(ident, syms.parent); } else { return undefined; } } } const artithmeticOps = { "+": (left, right) => right + left, "-": (left, right) => right - left, }; const comparisonOps = { "==": (left, right) => left === right, "!=": (left, right) => left !== right, "<": (left, right) => left < right, ">": (left, right) => left > right, "<=": (left, right) => left <= right, ">=": (left, right) => left >= right, }; const assignOps = { "=": (_, right) => right, "+=": (left, right) => ({ type: "int", value: left.value + right.value }), "-=": (left, right) => ({ type: "int", value: left.value - right.value }), }; const builtinFns = { println(msg, ...args) { let text = valueToPrint(msg); for (const arg of args) { text = text.replace("%", valueToPrint(arg)); } console.log(text); return { type: "null" }; }, read_text_file(path) { const text = fs.readFileSync(path.value).toString(); return { type: "string", value: text }; }, push(list, value) { if (list.type === "string") { list.value += value.value; return list; } list.values.push(value); return list; }, at(value, index) { if (value.type === "string") { return { type: "string", value: value.value[index.value] }; } return value.values[index.value]; }, len(value) { if (value.type === "string") { return { type: "int", value: value.value.length }; } return { type: "int", value: value.values.length }; }, }; const consts = { "null": { type: "null" }, "false": { type: "bool", value: false }, "true": { type: "bool", value: true }, }; const builtins = [ ...Object.entries(builtinFns) .map(([key, fn]) => [key, { type: "builtin", fn }]), ...Object.entries(consts), ]; 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} on line ${this.line}`, ); } } 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 { if (!this.done && this.curr === "\n") { this.line += 1; } this.idx += 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();