303 lines
8.6 KiB
JavaScript
303 lines
8.6 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
|
|
export class Runtime {
|
|
constructor(filename) {
|
|
this.callStack = [{ name: "<entry>", 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(filename) {
|
|
const text = fs.readFileSync(filename.value).toString();
|
|
return { type: "string", value: text };
|
|
}
|
|
|
|
builtinWriteTextFile(filename, text) {
|
|
fs.writeFileSync(filename.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 })),
|
|
};
|
|
}
|
|
|
|
builtinFsBasename(filepath) {
|
|
return { type: "string", value: path.basename(filepath.value) };
|
|
}
|
|
|
|
builtinFsDirname(filepath) {
|
|
return { type: "string", value: path.dirname(filepath.value) };
|
|
}
|
|
|
|
builtinFsCwd() {
|
|
return { type: "string", value: process.cwd() };
|
|
}
|
|
|
|
builtinFsResolve(base, relative) {
|
|
return {
|
|
type: "string",
|
|
value: path.resolve(base.value, relative.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}`);
|
|
}
|
|
}
|
|
}
|