import fs from "node:fs"; import process from "node:process"; export class Runtime { constructor(filename) { this.callStack = [{ name: "", filename, line: 0 }]; this.currentFilename = filename; this.currentLine = 0; } setFile(filename) { this.currentFilename = filename; } info(filename, line) { this.currentFilename = filename; this.currentLine = line; } pushCall(name, filename, line = this.currentLine) { this.callStack.push({ name, filename, line }); } popCall() { this.callStack.pop(); } panic(msg) { this.printPanic(msg); process.exit(1); } printPanic(msg) { console.error(`\x1b[1;91mpanic\x1b[1;97m: ${msg}\x1b[0m`); this.printCallStack(); } printCallStack() { const reversedStack = this.callStack.toReversed(); const last = reversedStack[0]; console.error( ` \x1b[90mat \x1b[37m${last.name} \x1b[90m(${this.currentFilename}:${this.currentLine})\x1b[0m`, ); for (let i = 0; i < reversedStack.length - 1 && i < 20; ++i) { const { name, filename } = reversedStack[i + 1]; const { line } = reversedStack[i]; console.error( ` \x1b[90mat \x1b[37m${name} \x1b[90m(${filename}:${line})\x1b[0m`, ); } } format(msg, ...args) { let value = Runtime.valueToPrint(msg); for (const arg of args) { value = value.replace("%", Runtime.valueToPrint(arg)); } return value; } equalityOperation(left, right) { if (left.type === "null") { return right.type === "null"; } if (right.type === "null") { return left.type === "null"; } if (left.type !== right.type) { this.panic(`cannot compare ${left.type} with ${right.type}`); } const type = left.type; if (["bool", "int", "string"].includes(type)) { return left.value === right.value; } else if (type === "list") { if (left.values.length !== right.values.length) { return false; } for (let i = 0; i < left.values.length; ++i) { if (!this.opEq(left.values[i], right.values[i])) { return false; } } return true; } else { this.panic(`equality not implemented for ${type}`); } } opEq(left, right) { return { type: "bool", value: this.equalityOperation(left, right) }; } opNe(left, right) { return { type: "bool", value: !this.equalityOperation(left, right) }; } opNot(expr) { if (expr.type !== "bool") { this.panic(`cannot apply not to type ${expr.type}`); } return { type: "bool", value: !expr.value }; } comparisonOperation(left, right, action) { if (left.type !== "int" || right.type !== "int") { this.panic(`cannot compare types ${left.type} with ${right.type}`); } return { type: "bool", value: action(left.value, right.value) }; } opLt(left, right) { return this.comparisonOperation(left, right, (l, r) => l < r); } opGt(left, right) { return this.comparisonOperation(left, right, (l, r) => l > r); } opLte(left, right) { return this.comparisonOperation(left, right, (l, r) => l <= r); } opGte(left, right) { return this.comparisonOperation(left, right, (l, r) => l >= r); } opAdd(left, right) { if (left.type === "int" && right.type === "int") { return { type: "int", value: left.value + right.value }; } else if (left.type === "string" && right.type === "string") { return { type: "string", value: left.value + right.value }; } else { this.panic(`cannot apply '+' on ${left.type} and ${right.type}`); } } opSub(left, right) { if (left.type === "int" && right.type === "int") { return { type: "int", value: left.value - right.value }; } else { this.panic(`cannot apply '-' on ${left.type} and ${right.type}`); } } truthy(value) { if (value.type !== "bool") { this.panic("expected bool"); } return value.value; } builtinFormat(msg, ...args) { return { type: "string", value: this.format(msg, ...args) }; } builtinPrint(msg, ...args) { process.stdout.write(this.format(msg, ...args)); return { type: "null" }; } builtinPrintln(msg, ...args) { console.log(this.format(msg, ...args)); return { type: "null" }; } builtinPanic(msg, ...args) { this.panic(this.format(msg, ...args)); return { type: "null" }; } builtinReadTextFile(path) { const text = fs.readFileSync(path.value).toString(); return { type: "string", value: text }; } builtinWriteTextFile(path, text) { fs.writeFileSync(path.value, text.value); return { type: "null" }; } builtinPush(list, value) { if (list.type === "string") { list.value += value.value; return list; } list.values.push(value); return list; } builtinAt(value, index) { if (value.type === "string") { return { type: "string", value: value.value[index.value] }; } return value.values[index.value] ?? { type: "null" }; } builtinSet(subject, index, value) { subject.values[index.value] = value; return { type: "null" }; } builtinLen(value) { if (value.type === "string") { return { type: "int", value: value.value.length }; } return { type: "int", value: value.values.length }; } builtinStringToInt(value) { return { type: "int", value: Number(value.value) }; } builtinCharCode(value) { return { type: "int", value: value.value.charCodeAt(0) }; } builtinStringsJoin(value) { return { type: "string", value: value.values .map((value) => value.value) .join(""), }; } builtinGetArgs() { return { type: "list", values: process.argv.slice(2) .map((value) => ({ type: "string", value })), }; } static 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) => Runtime.valueToString(v)).join(" ") })`; } else { throw new Error(`unknown value type ${value.type}`); } } static 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) => Runtime.valueToString(v)).join(" ") })`; } else { throw new Error(`unknown value type ${value.type}`); } } static 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) => Runtime.valueToJs(v)); } else { throw new Error(`unknown value type ${value.type}`); } } }