init
This commit is contained in:
commit
7bc9504f12
7
compile.phi
Normal file
7
compile.phi
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
(let text (call read_text_file "program.phi"))
|
||||||
|
(fn a (b c) (do
|
||||||
|
|
||||||
|
))
|
||||||
|
|
||||||
|
(call println text)
|
387
phi.js
Normal file
387
phi.js
Normal file
@ -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();
|
7
program.phi
Normal file
7
program.phi
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
(fn hello () (do
|
||||||
|
(call println "hello world")
|
||||||
|
(call println "hello world")
|
||||||
|
))
|
||||||
|
|
||||||
|
(call hello)
|
Loading…
x
Reference in New Issue
Block a user