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 output ())
(let (enter_scope leave_scope define_sym get_sym) (call Syms))
(fn generate () (do (fn generate () (do
(call emit "#!/usr/bin/env node\n") (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) (call emit_exprs ast)
(return (call strings_join output)) (return (call strings_join output))
)) ))
@ -11,7 +17,7 @@
(fn emit_exprs (exprs) (do (fn emit_exprs (exprs) (do
(for expr exprs (do (for expr exprs (do
(call emit_expr expr) (call emit_expr expr)
(call emit "\n") (call emit ";\n")
)) ))
)) ))
@ -21,13 +27,33 @@
(call emit_list expr) (call emit_list expr)
) (if (== ty "int") ( ) (if (== ty "int") (
(let (_ _ value) expr) (let (_ _ value) expr)
(call emit (call format "%" value)) (call emit (call format "{ type: \"int\", value: % }" value))
) (if (== ty "string") ( ) (if (== ty "string") (
(let (_ _ value) expr) (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") ( ) (if (== ty "ident") (
(let (_ _ value) expr) (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 ) (do
(call panic "unknown expr type '%' on line %" ty line) (call panic "unknown expr type '%' on line %" ty line)
))))) )))))
@ -36,7 +62,7 @@
(fn emit_list (expr) (do (fn emit_list (expr) (do
(let (ty line s) expr) (let (ty line s) expr)
(if (== (call len s) 0) (do (if (== (call len s) 0) (do
(call emit "[]") (call emit "{ type: \"list\", values: [] }")
(return) (return)
)) ))
(let ((_ _ id)) s) (let ((_ _ id)) s)
@ -53,6 +79,9 @@
(call emit (call format "_%" name)) (call emit (call format "_%" name))
)) ))
(call define_sym name ("fn" name line))
(call emit (call format ") {\n" name)) (call emit (call format ") {\n" name))
(call emit_expr body) (call emit_expr body)
(call emit "}") (call emit "}")
@ -85,7 +114,7 @@
(call emit ")") (call emit ")")
) (do ) (do
(call emit "[") (call emit "{ type: \"list\", values: [")
(let first true) (let first true)
(for e s (do (for e s (do
(if (not first) (do (if (not first) (do
@ -95,7 +124,7 @@
(call emit_expr e) (call emit_expr e)
)) ))
(call emit "]") (call emit "] }")
))))) )))))
)) ))
@ -106,6 +135,65 @@
(return (generate)) (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 (fn string_escape (str) (do
(let str_len (call len str)) (let str_len (call len str))
(let i 0) (let i 0)
@ -357,8 +445,10 @@
)) ))
(let input_filename "program.phi")
(call println "reading file...") (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 ===")
//(call println text) //(call println text)
@ -382,7 +472,7 @@
//)) //))
(call println "emitting...") (call println "emitting...")
(let emitter (call Emitter ast)) (let emitter (call Emitter ast input_filename))
(let (emit) emitter) (let (emit) emitter)
(let js_code (call emit)) (let js_code (call emit))

236
phi.js
View File

@ -1,7 +1,8 @@
"use strict"; "use strict";
import * as fs from "node:fs"; import fs from "node:fs";
import process from "node:process"; import process from "node:process";
import { Runtime } from "./runtime.js";
function main() { function main() {
const filename = process.argv[2]; const filename = process.argv[2];
@ -20,7 +21,7 @@ function main() {
lastValue = result.value; lastValue = result.value;
} }
if (lastValue !== null) { if (lastValue !== null) {
console.log(valueToJs(lastValue)); console.log(Runtime.valueToJs(lastValue));
} }
} }
@ -28,8 +29,8 @@ class Evaluator {
constructor(filename) { constructor(filename) {
this.syms = { parent: undefined, map: new Map(this.builtins) }; this.syms = { parent: undefined, map: new Map(this.builtins) };
this.currentLine = 0; this.currentLine = 0;
this.callStack = [{ name: "<file>", line: 0 }];
this.filename = filename; this.filename = filename;
this.runtime = new Runtime(filename);
} }
/** /**
@ -37,9 +38,13 @@ class Evaluator {
*/ */
eval(expr) { eval(expr) {
if (!expr) { if (!expr) {
console.log(this.callStack) this.runtime.printPanic(
throw new Error() "expression could not be evaluated",
this.currentLine,
);
throw new Error("expression could not be evaluated");
} }
this.currentLine = expr.line; this.currentLine = expr.line;
if (expr.type === "list") { if (expr.type === "list") {
return this.evalList(expr, expr.line); return this.evalList(expr, expr.line);
@ -129,8 +134,13 @@ class Evaluator {
} else if (id in this.artithmeticOps) { } else if (id in this.artithmeticOps) {
const left = this.evalToValue(s[1]); const left = this.evalToValue(s[1]);
const right = this.evalToValue(s[2]); const right = this.evalToValue(s[2]);
if (id === "+" && left.type === "string" && right.type === "string") { if (
return { type: "value", value: { type: "string", value: left.value + right.value } }; id === "+" && left.type === "string" && right.type === "string"
) {
return {
type: "value",
value: { type: "string", value: left.value + right.value },
};
} }
return { return {
type: "value", type: "value",
@ -139,6 +149,26 @@ class Evaluator {
value: this.artithmeticOps[id](left.value, right.value), 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) { } else if (id in this.comparisonOps) {
const left = this.evalToValue(s[1]); const left = this.evalToValue(s[1]);
const right = this.evalToValue(s[2]); const right = this.evalToValue(s[2]);
@ -235,7 +265,7 @@ class Evaluator {
} }
} }
this.syms = outerSyms; this.syms = outerSyms;
return { type: "value", value: { type: "null" } } return { type: "value", value: { type: "null" } };
} }
evalDo(expr) { evalDo(expr) {
@ -268,21 +298,15 @@ class Evaluator {
throw new Error("cannot call non-function"); throw new Error("cannot call non-function");
} }
if (this.callStack.length > 100) { this.runtime.pushCall(fnValue.name, expr.line);
this.panic("stack overflow")
this.printCallStack()
process.exit(1);
}
this.callStack.push({ name: fnValue.name, line: expr.line });
const callerSyms = this.syms; const callerSyms = this.syms;
this.syms = { this.syms = {
parent: fnValue.syms, parent: fnValue.syms,
map: new Map(), map: new Map(),
}; };
if (fnValue.params.length !== args.length) { if (fnValue.params.length !== args.length) {
throw new Error( this.panic(
`incorrect amount of arguments for function '${fnValue.name}' on line ${expr.line}`, `incorrect amount of arguments for function '${fnValue.name}'`,
); );
} }
for (let i = 0; i < fnValue.params.length; ++i) { for (let i = 0; i < fnValue.params.length; ++i) {
@ -299,7 +323,7 @@ class Evaluator {
} }
this.syms = callerSyms; this.syms = callerSyms;
this.callStack.pop() this.runtime.popCall();
return { type: "value", value: returnValue }; return { type: "value", value: returnValue };
} }
@ -342,10 +366,10 @@ class Evaluator {
}); });
} else if (pattern.type === "list") { } else if (pattern.type === "list") {
if (value.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()) { 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 { } else {
throw new Error(`cannot assign to pattern on line ${pattern.line}`); throw new Error(`cannot assign to pattern on line ${pattern.line}`);
@ -363,89 +387,8 @@ class Evaluator {
} }
panic(msg) { panic(msg) {
console.error(`\x1b[1;91mpanic\x1b[1;97m: ${msg}\x1b[0m"`); this.runtime.setLine(this.currentLine);
this.printCallStack(); this.runtime.panic(msg);
}
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 = { artithmeticOps = {
@ -453,8 +396,6 @@ class Evaluator {
"-": (left, right) => right - left, "-": (left, right) => right - left,
}; };
comparisonOps = { 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,
"<=": (left, right) => left <= right, "<=": (left, right) => left <= right,
@ -463,24 +404,33 @@ class Evaluator {
assignOps = { assignOps = {
"=": (_, right) => right, "=": (_, right) => right,
"+=": (left, right) => ({ "+=": (left, right) => ({
type: left.type === "string" && left.type === right.type ? "string" : "int", type: left.type === "string" && left.type === right.type
value: left.value + right.value ? "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 = { builtinFns = {
"format": (...args) => this.builtinFormat(...args), "format": (...args) => this.runtime.builtinFormat(...args),
"println": (...args) => this.builtinPrintln(...args), "print": (...args) => this.runtime.builtinPrint(...args),
"panic": (...args) => this.builtinPanic(...args), "println": (...args) => this.runtime.builtinPrintln(...args),
"read_text_file": (...args) => this.builtinReadTextFile(...args), "panic": (...args) =>
"write_text_file": (...args) => this.builtinWriteTextFile(...args), this.runtime.builtinPanic(this.currentLine, ...args),
"push": (...args) => this.builtinPush(...args), "read_text_file": (...args) =>
"at": (...args) => this.builtinAt(...args), this.runtime.builtinReadTextFile(...args),
"len": (...args) => this.builtinLen(...args), "write_text_file": (...args) =>
"string_to_int": (...args) => this.builtinStringToInt(...args), this.runtime.builtinWriteTextFile(...args),
"char_code": (...args) => this.builtinCharCode(...args), "push": (...args) => this.runtime.builtinPush(...args),
"strings_join": (...args) => this.builtinStringsJoin(...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 = { 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 * @param {Expr} expr
* @returns {string} * @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}`);
}
}
}