diff --git a/compile.phi b/compile.phi index 953b456..69755b5 100644 --- a/compile.phi +++ b/compile.phi @@ -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)) diff --git a/phi.js b/phi.js index e8f0f6f..9f446aa 100644 --- a/phi.js +++ b/phi.js @@ -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: "", 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} diff --git a/runtime.js b/runtime.js new file mode 100644 index 0000000..99d1b1a --- /dev/null +++ b/runtime.js @@ -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: "", 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}`); + } + } +}