725 lines
22 KiB
JavaScript
725 lines
22 KiB
JavaScript
"use strict";
|
|
|
|
import * as fs from "node:fs";
|
|
import process from "node:process";
|
|
|
|
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(valueToJs(lastValue));
|
|
}
|
|
}
|
|
|
|
class Evaluator {
|
|
constructor(filename) {
|
|
this.syms = { parent: undefined, map: new Map(this.builtins) };
|
|
this.currentLine = 0;
|
|
this.callStack = [{ name: "<file>", line: 0 }];
|
|
this.filename = filename;
|
|
}
|
|
|
|
/**
|
|
* @param {Expr} expr
|
|
*/
|
|
eval(expr) {
|
|
if (!expr) {
|
|
console.log(this.callStack)
|
|
throw new Error()
|
|
}
|
|
this.currentLine = 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) {
|
|
throw new Error(
|
|
`could not find symbol '${expr.value}' on line ${expr.line}`,
|
|
);
|
|
}
|
|
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 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");
|
|
}
|
|
|
|
if (this.callStack.length > 100) {
|
|
this.panic("stack overflow")
|
|
this.printCallStack()
|
|
process.exit(1);
|
|
}
|
|
|
|
this.callStack.push({ name: fnValue.name, line: expr.line });
|
|
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 for function '${fnValue.name}' on line ${expr.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;
|
|
this.callStack.pop()
|
|
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") {
|
|
throw new Error(`expected list on line ${pattern.line}`);
|
|
}
|
|
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) {
|
|
console.error(`\x1b[1;91mpanic\x1b[1;97m: ${msg}\x1b[0m"`);
|
|
this.printCallStack();
|
|
}
|
|
|
|
printCallStack() {
|
|
const last = this.callStack[this.callStack.length - 1];
|
|
console.error(` \x1b[90mat \x1b[37m${last.name} \x1b[90m(${this.filename}:${this.currentLine})\x1b[0m`);
|
|
for (let i = this.callStack.length - 2; i >= 0; --i) {
|
|
const name = this.callStack[i].name;
|
|
const line = this.callStack[i + 1].line;
|
|
console.error(` \x1b[90mat \x1b[37m${name} \x1b[90m(${this.filename}:${line})\x1b[0m`);
|
|
}
|
|
}
|
|
|
|
builtinFormat(msg, ...args) {
|
|
let value = valueToPrint(msg);
|
|
for (const arg of args) {
|
|
value = value.replace("%", valueToPrint(arg));
|
|
}
|
|
return { type: "string", value };
|
|
}
|
|
builtinPrintln(msg, ...args) {
|
|
let text = valueToPrint(msg);
|
|
|
|
for (const arg of args) {
|
|
text = text.replace("%", valueToPrint(arg));
|
|
}
|
|
|
|
console.log(text);
|
|
return { type: "null" };
|
|
}
|
|
builtinPanic(msg, ...args) {
|
|
let text = valueToPrint(msg);
|
|
|
|
for (const arg of args) {
|
|
text = text.replace("%", valueToPrint(arg));
|
|
}
|
|
|
|
this.panic(text);
|
|
process.exit(1);
|
|
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];
|
|
}
|
|
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("")
|
|
};
|
|
}
|
|
|
|
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,
|
|
"<=": (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.builtinFormat(...args),
|
|
"println": (...args) => this.builtinPrintln(...args),
|
|
"panic": (...args) => this.builtinPanic(...args),
|
|
"read_text_file": (...args) => this.builtinReadTextFile(...args),
|
|
"write_text_file": (...args) => this.builtinWriteTextFile(...args),
|
|
"push": (...args) => this.builtinPush(...args),
|
|
"at": (...args) => this.builtinAt(...args),
|
|
"len": (...args) => this.builtinLen(...args),
|
|
"string_to_int": (...args) => this.builtinStringToInt(...args),
|
|
"char_code": (...args) => this.builtinCharCode(...args),
|
|
"strings_join": (...args) => this.builtinStringsJoin(...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),
|
|
];
|
|
}
|
|
|
|
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();
|