phi-lang/runtime.js
2025-09-25 15:55:59 +02:00

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}`);
}
}
}