From 7bc9504f124d8843f47d6bcda3c4d89c59e73ba0 Mon Sep 17 00:00:00 2001 From: sfj Date: Tue, 9 Sep 2025 16:12:15 +0200 Subject: [PATCH] init --- compile.phi | 7 + phi.js | 387 ++++++++++++++++++++++++++++++++++++++++++++++++++++ program.phi | 7 + 3 files changed, 401 insertions(+) create mode 100644 compile.phi create mode 100644 phi.js create mode 100644 program.phi diff --git a/compile.phi b/compile.phi new file mode 100644 index 0000000..1a62bc5 --- /dev/null +++ b/compile.phi @@ -0,0 +1,7 @@ + +(let text (call read_text_file "program.phi")) +(fn a (b c) (do + +)) + +(call println text) diff --git a/phi.js b/phi.js new file mode 100644 index 0000000..dee087e --- /dev/null +++ b/phi.js @@ -0,0 +1,387 @@ +"use strict"; + +import * as fs from "fs"; + +function main() { + const text = fs.readFileSync(process.argv[2]).toString(); + + const ast = new Parser(text).parse(); + + let lastValue = null; + + const evaluator = new Evaluator(); + 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() { + this.syms = { parent: undefined, map: new Map(builtins) }; + } + + /** + * @param {Expr} expr + */ + eval(expr) { + 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 findInTree = (syms, ident) => { + if (syms.map.has(ident)) + return syms.map.get(ident); + else if (syms.parent) + return findInTree(syms.parent, ident); + else + return undefined; + } + const sym = findInTree(this.syms, expr.value); + if (!sym) + throw new Error(`could not find symbol '${expr.value}' on line ${expr.line}`); + 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]?.value ?? undefined; + if (id === "fn") { + const name = s[1].value; + this.syms.map.set(name, { + type: "fn", + name, + params: s[2].values.map(ident => ident.value), + body: s[3], + syms: this.syms, + }); + return { type: "value", value: { type: "null" } }; + } else if (id === "call") { + 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"); + } + 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 on line ${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; + return { type: "value", value: returnValue }; + } else if (id === "return") { + return { type: "return", value: s[1] ? this.evalToValue(s[1]) : { type: "null" } }; + } else if (id === "let") { + const value = this.evalToValue(s[2]); + this.syms.map.set(s[1].value, value); + return { type: "value", value: { type: "null" } }; + } else if (id === "do") { + let lastValue = { type: "null" }; + + for (const expr of s.slice(1)) { + const result = this.eval(expr); + if (result.type !== "value") { + break; + } + lastValue = result.value; + } + return { type: "value", value: lastValue }; + } else if (s[0] === "if") { + const cond = this.evalToValue(s[1]); + if (cond.type !== "bool") { + throw new Error(`expected bool on line ${line}`); + } + if (cond.value) { + return this.eval(s[2]); + } else if (s[3]) { + return this.eval(s[3]); + } else { + return { type: "value", value: "null" }; + } + } else if (s[0] === "loop") { + 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; + } + } + } else if (s[0] === "break") { + return { type: "break", value: s[1] ? this.evalToValue(s[1]) : { type: "null" } }; + } else { + return { type: "value", value: { type: "list", values: s.map(expr => this.evalToValue(expr)) } }; + } + } +} + +const builtinFns = { + println(msg) { + console.log(valueToPrint(msg)); + return { type: "null" }; + }, + read_text_file(path) { + const text = fs.readFileSync(path.value); + return { type: "string", value: text }; + } +}; + +const builtins = Object.entries(builtinFns) + .map(([key, fn]) => [key, { type: "builtin", fn }]); + +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}`) + } + } + + 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 { + this.idx += 1; + if (!this.done && this.curr === "\n") { + this.line += 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(); diff --git a/program.phi b/program.phi new file mode 100644 index 0000000..10ded09 --- /dev/null +++ b/program.phi @@ -0,0 +1,7 @@ + +(fn hello () (do + (call println "hello world") + (call println "hello world") +)) + +(call hello) \ No newline at end of file