"use strict"; import fs from "node:fs"; import process from "node:process"; import { Runtime } from "./runtime.js"; function main() { const filename = process.argv[2]; const text = fs.readFileSync(filename).toString(); const ast = new Parser(text).parse(); let lastValue = null; const evaluator = new Evaluator(filename); for (const expr of ast) { const result = evaluator.eval(expr); if (result.type !== "value") { break; } lastValue = result.value; } if (lastValue !== null) { // console.log(Runtime.valueToJs(lastValue)); } } class Evaluator { constructor(filename) { this.syms = { parent: undefined, map: new Map(this.builtins) }; this.currentLine = 0; this.filename = filename; this.runtime = new Runtime(filename); } /** * @param {Expr} expr */ eval(expr) { if (!expr) { this.runtime.printPanic( "expression could not be evaluated", this.currentLine, ); throw new Error("expression could not be evaluated"); } this.currentLine = expr.line; this.runtime.setLine(expr.line) 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) { this.panic( `undefined symbol '${expr.value}'`, ); } if (sym.type === "local") { return { type: "value", value: { ...sym.value } }; } else { 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") { return this.evalFn(expr); } else if (id === "return") { return { type: "return", value: s[1] ? this.evalToValue(s[1]) : { type: "null" }, }; } else if (id === "let") { return this.evalLet(expr); } else if (id === "if") { return this.evalIf(expr); } else if (id === "loop") { return this.evalLoop(expr); } else if (id === "for") { return this.evalFor(expr); } else if (id === "break") { return { type: "break", value: s[1] ? this.evalToValue(s[1]) : { type: "null" }, }; } else if (id === "do") { return this.evalDo(expr); } else if (id === "call") { return this.evalCall(expr); } 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 this.artithmeticOps) { const left = this.evalToValue(s[1]); const right = this.evalToValue(s[2]); if ( id === "+" && left.type === "string" && right.type === "string" ) { return { type: "value", value: { type: "string", value: left.value + right.value }, }; } return { type: "value", value: { type: "int", value: this.artithmeticOps[id](left.value, right.value), }, }; } else if (id === "==") { const left = this.evalToValue(s[1]); const right = this.evalToValue(s[2]); return { type: "value", value: this.runtime.opEq(left, right), }; } else if (id === "!=") { const left = this.evalToValue(s[1]); const right = this.evalToValue(s[2]); return { type: "value", value: this.runtime.opNe(left, right), }; } else if (id in this.comparisonOps) { const left = this.evalToValue(s[1]); const right = this.evalToValue(s[2]); return { type: "value", value: { type: "bool", value: this.comparisonOps[id](left.value, right.value), }, }; } else if (id in this.assignOps) { return this.evalAssign(expr); } else { return { type: "value", value: { type: "list", values: s.map((expr) => this.evalToValue(expr)), }, }; } } evalFn(expr) { const s = expr.values; const name = s[1].value; this.syms.map.set(name, { type: "fn", line: expr.line, name, params: s[2].values.map((ident) => ident.value), body: s[3], syms: this.syms, }); return { type: "value", value: { type: "null" } }; } evalLet(expr) { const s = expr.values; const value = this.evalToValue(s[2]); this.assignPattern(s[1], value); return { type: "value", value: { type: "null" } }; } evalIf(expr) { const s = expr.values; 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" }; } } evalLoop(expr) { const s = expr.values; 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; } } } evalFor(expr) { const s = expr.values; const value = this.evalToValue(s[2]); if (value.type !== "list") { throw new Error( `expected list on line ${expr.line}`, ); } const outerSyms = this.syms; this.syms = { parent: outerSyms, map: new Map() }; for (let i = 0; i < value.values.length; ++i) { this.assignPattern(s[1], value.values[i]); const result = this.eval(s[3]); if (result.type === "break") { return { type: "value", value: result.value }; } else if (result.type !== "value") { return result; } } this.syms = outerSyms; return { type: "value", value: { type: "null" } }; } evalDo(expr) { const s = expr.values; const outerSyms = this.syms; this.syms = { parent: outerSyms, map: new Map() }; 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; } this.syms = outerSyms; return { type: "value", value: lastValue }; } evalCall(expr) { const s = expr.values; 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"); } this.runtime.pushCall(fnValue.name, expr.line); const callerSyms = this.syms; this.syms = { parent: fnValue.syms, map: new Map(), }; if (fnValue.params.length !== args.length) { this.panic( `incorrect amount of arguments for function '${fnValue.name}'`, ); } 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; this.runtime.popCall(); return { type: "value", value: returnValue }; } evalAssign(expr) { const s = expr.values; const id = s[0].value; 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 value = this.evalToValue(s[2]); if (sym.type === "local") { sym.value = this.assignOps[id](sym.value, value); } else { throw new Error( `cannot assign to symbol on line ${expr.line}`, ); } } else { throw new Error( `cannot assign to expression on line ${expr.line}`, ); } return { type: "value", value: { type: "null" } }; } /** @param {Expr} pattern */ assignPattern(pattern, value) { if (pattern.type === "ident") { if (pattern.value === "_") { return; } this.syms.map.set(pattern.value, { type: "local", line: pattern.line, value, }); } else if (pattern.type === "list") { if (value.type !== "list") { this.panic(`expected list`); } for (const [i, p] of pattern.values.entries()) { this.assignPattern(p, value.values[i] ?? { type: "null" }); } } else { throw new Error(`cannot assign to pattern on line ${pattern.line}`); } } 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; } } panic(msg) { this.runtime.setLine(this.currentLine); this.runtime.panic(msg); } artithmeticOps = { "+": (left, right) => right + left, "-": (left, right) => right - left, }; comparisonOps = { "<": (left, right) => left < right, ">": (left, right) => left > right, "<=": (left, right) => left <= right, ">=": (left, right) => left >= right, }; assignOps = { "=": (_, right) => right, "+=": (left, right) => ({ type: left.type === "string" && left.type === right.type ? "string" : "int", value: left.value + right.value, }), "-=": (left, right) => ({ type: "int", value: left.value - right.value, }), }; builtinFns = { "format": (...args) => this.runtime.builtinFormat(...args), "print": (...args) => this.runtime.builtinPrint(...args), "println": (...args) => this.runtime.builtinPrintln(...args), "panic": (...args) => this.runtime.builtinPanic(...args), "read_text_file": (...args) => this.runtime.builtinReadTextFile(...args), "write_text_file": (...args) => this.runtime.builtinWriteTextFile(...args), "push": (...args) => this.runtime.builtinPush(...args), "at": (...args) => this.runtime.builtinAt(...args), "set": (...args) => this.runtime.builtinSet(...args), "len": (...args) => this.runtime.builtinLen(...args), "string_to_int": (...args) => this.runtime.builtinStringToInt(...args), "char_code": (...args) => this.runtime.builtinCharCode(...args), "strings_join": (...args) => this.runtime.builtinStringsJoin(...args), "get_args": (...args) => this.runtime.builtinGetArgs(...args), }; consts = { "null": { type: "null" }, "false": { type: "bool", value: false }, "true": { type: "bool", value: true }, }; builtins = [ ...Object.entries(this.builtinFns) .map(([key, fn]) => [key, { type: "builtin", fn }]), ...Object.entries(this.consts), ]; } /** * @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();