compile test program

This commit is contained in:
sfja 2025-09-11 22:05:15 +02:00
parent d084959281
commit b9b479fd63
3 changed files with 379 additions and 177 deletions

View File

@ -1,9 +1,15 @@
(fn Emitter (ast) (do
(fn Emitter (ast filename) (do
(let output ())
(let (enter_scope leave_scope define_sym get_sym) (call Syms))
(fn generate () (do
(call emit "#!/usr/bin/env node\n")
(call emit "import { Runtime } from \"./runtime.js\"\n")
(call emit "const runtime = new Runtime(\"")
(call emit filename)
(call emit "\")\n")
(call emit_exprs ast)
(return (call strings_join output))
))
@ -11,7 +17,7 @@
(fn emit_exprs (exprs) (do
(for expr exprs (do
(call emit_expr expr)
(call emit "\n")
(call emit ";\n")
))
))
@ -21,13 +27,33 @@
(call emit_list expr)
) (if (== ty "int") (
(let (_ _ value) expr)
(call emit (call format "%" value))
(call emit (call format "{ type: \"int\", value: % }" value))
) (if (== ty "string") (
(let (_ _ value) expr)
(call emit (call format "\"%\"" (call string_escape value)))
(call emit (call format "{ type: \"string\", value: \"%\" }" (call string_escape value)))
) (if (== ty "ident") (
(let (_ _ value) expr)
(call emit (call format "_%" value))
(let sym (call get_sym value))
(if (== sym null) (do
(call panic "undefined symbol '%'" value)
))
(let (sym_ty) sym)
(if (== sym_ty "builtin") (do
(let (_ id) sym)
(if (== id "println") (do
(call emit (call format
"((...args) => (runtime.setLine(%), runtime.builtinPrintln(...args)))"
line))
) (do
(call panic "unknown builtin '%'" id)
))
) (if (== sym_ty "fn") (do
(call emit (call format "_%" value))
) (do
(call panic "not implemented '%'" sym_ty)
)))
) (do
(call panic "unknown expr type '%' on line %" ty line)
)))))
@ -36,7 +62,7 @@
(fn emit_list (expr) (do
(let (ty line s) expr)
(if (== (call len s) 0) (do
(call emit "[]")
(call emit "{ type: \"list\", values: [] }")
(return)
))
(let ((_ _ id)) s)
@ -53,6 +79,9 @@
(call emit (call format "_%" name))
))
(call define_sym name ("fn" name line))
(call emit (call format ") {\n" name))
(call emit_expr body)
(call emit "}")
@ -85,7 +114,7 @@
(call emit ")")
) (do
(call emit "[")
(call emit "{ type: \"list\", values: [")
(let first true)
(for e s (do
(if (not first) (do
@ -95,7 +124,7 @@
(call emit_expr e)
))
(call emit "]")
(call emit "] }")
)))))
))
@ -106,6 +135,65 @@
(return (generate))
))
(fn Syms () (do
(let syms (null (
("println" ("builtin" "println"))
)))
(fn enter_scope () (do
(= syms (syms ()))
))
(fn leave_scope () (do
(let (parent _) syms)
(= syms parent)
))
(fn define (ident sym) (do
(let (_ syms) syms)
(let i 0)
(loop (do
(if (>= i (call len syms)) (break))
(let (s_ident s_sym) (call at syms i))
(if (== ident s_ident) (do
(call set s_syms i sym)
(return)
))
(+= i 1)
))
(call push syms (ident sym))
))
(fn find_sym (syms ident) (do
(let (parent map) syms)
(let i 0)
(loop (do
(if (>= i (call len map)) (break))
(let (s_ident s_sym) (call at map i))
(if (== ident s_ident) (do
(return s_sym)
))
(+= i 1)
))
(if (!= parent null) (do
(return (call find_syms parent ident))
))
(return null)
))
(fn get (ident) (do
(return (call find_sym syms ident))
))
(return (
enter_scope
leave_scope
define
get
))
))
(fn string_escape (str) (do
(let str_len (call len str))
(let i 0)
@ -357,8 +445,10 @@
))
(let input_filename "program.phi")
(call println "reading file...")
(let text (call read_text_file "program.phi"))
(let text (call read_text_file input_filename))
//(call println "=== text ===")
//(call println text)
@ -382,7 +472,7 @@
//))
(call println "emitting...")
(let emitter (call Emitter ast))
(let emitter (call Emitter ast input_filename))
(let (emit) emitter)
(let js_code (call emit))

236
phi.js
View File

@ -1,7 +1,8 @@
"use strict";
import * as fs from "node:fs";
import fs from "node:fs";
import process from "node:process";
import { Runtime } from "./runtime.js";
function main() {
const filename = process.argv[2];
@ -20,7 +21,7 @@ function main() {
lastValue = result.value;
}
if (lastValue !== null) {
console.log(valueToJs(lastValue));
console.log(Runtime.valueToJs(lastValue));
}
}
@ -28,8 +29,8 @@ 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;
this.runtime = new Runtime(filename);
}
/**
@ -37,9 +38,13 @@ class Evaluator {
*/
eval(expr) {
if (!expr) {
console.log(this.callStack)
throw new Error()
this.runtime.printPanic(
"expression could not be evaluated",
this.currentLine,
);
throw new Error("expression could not be evaluated");
}
this.currentLine = expr.line;
if (expr.type === "list") {
return this.evalList(expr, expr.line);
@ -129,8 +134,13 @@ class Evaluator {
} 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 } };
if (
id === "+" && left.type === "string" && right.type === "string"
) {
return {
type: "value",
value: { type: "string", value: left.value + right.value },
};
}
return {
type: "value",
@ -139,6 +149,26 @@ class Evaluator {
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: {
type: "bool",
value: this.runtime.eq(left, right),
},
};
} else if (id === "!=") {
const left = this.evalToValue(s[1]);
const right = this.evalToValue(s[2]);
return {
type: "value",
value: {
type: "bool",
value: !this.runtime.eq(left, right),
},
};
} else if (id in this.comparisonOps) {
const left = this.evalToValue(s[1]);
const right = this.evalToValue(s[2]);
@ -235,7 +265,7 @@ class Evaluator {
}
}
this.syms = outerSyms;
return { type: "value", value: { type: "null" } }
return { type: "value", value: { type: "null" } };
}
evalDo(expr) {
@ -268,21 +298,15 @@ class Evaluator {
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 });
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) {
throw new Error(
`incorrect amount of arguments for function '${fnValue.name}' on line ${expr.line}`,
this.panic(
`incorrect amount of arguments for function '${fnValue.name}'`,
);
}
for (let i = 0; i < fnValue.params.length; ++i) {
@ -299,7 +323,7 @@ class Evaluator {
}
this.syms = callerSyms;
this.callStack.pop()
this.runtime.popCall();
return { type: "value", value: returnValue };
}
@ -342,10 +366,10 @@ class Evaluator {
});
} else if (pattern.type === "list") {
if (value.type !== "list") {
throw new Error(`expected list on line ${pattern.line}`);
this.panic(`expected list`);
}
for (const [i, p] of pattern.values.entries()) {
this.assignPattern(p, value.values[i] ?? { type: "null" })
this.assignPattern(p, value.values[i] ?? { type: "null" });
}
} else {
throw new Error(`cannot assign to pattern on line ${pattern.line}`);
@ -363,89 +387,8 @@ class Evaluator {
}
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("")
};
this.runtime.setLine(this.currentLine);
this.runtime.panic(msg);
}
artithmeticOps = {
@ -453,8 +396,6 @@ class Evaluator {
"-": (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,
@ -463,24 +404,33 @@ class Evaluator {
assignOps = {
"=": (_, right) => right,
"+=": (left, right) => ({
type: left.type === "string" && left.type === right.type ? "string" : "int",
value: left.value + right.value
type: left.type === "string" && left.type === right.type
? "string"
: "int",
value: left.value + right.value,
}),
"-=": (left, right) => ({
type: "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),
"format": (...args) => this.runtime.builtinFormat(...args),
"print": (...args) => this.runtime.builtinPrint(...args),
"println": (...args) => this.runtime.builtinPrintln(...args),
"panic": (...args) =>
this.runtime.builtinPanic(this.currentLine, ...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),
"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),
};
consts = {
@ -496,54 +446,6 @@ class Evaluator {
];
}
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}

210
runtime.js Normal file
View File

@ -0,0 +1,210 @@
import fs from "node:fs";
import process from "node:process";
export class Runtime {
constructor(filename) {
this.syms = { parent: undefined, map: new Map(this.builtins) };
this.callStack = [{ name: "<entry>", line: 0 }];
this.filename = filename;
this.currentLine = 0;
}
setLine(line) {
this.currentLine = line;
}
pushCall(name, line) {
this.callStack.push({ name, 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 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`,
);
}
}
format(msg, ...args) {
let value = Runtime.valueToPrint(msg);
for (const arg of args) {
value = value.replace("%", Runtime.valueToPrint(arg));
}
return value;
}
eq(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.eq(left.values[i], right.values[i])) {
return false;
}
}
return true;
} else {
this.panic(`equality not implemented for ${type}`);
}
}
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(currentLine, msg, ...args) {
this.panic(this.format(msg, ...args), currentLine);
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(""),
};
}
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}`);
}
}
}