Last active
March 4, 2022 16:20
-
-
Save leontrolski/6588a2a8709b5e1331da34346b9608a3 to your computer and use it in GitHub Desktop.
interpreter with assignment
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 80 line parser and interpreter for a small language with | |
// integers, assignments, lexical scope, first class functions | |
const source = ` | |
(block | |
(assign x 5) | |
(assign f | |
(function | |
(a b) | |
(block | |
(assign c (+ a b)) | |
(* (negative c) x) | |
) | |
) | |
) | |
(print (f 1 3)) | |
)` | |
type Var = {type: "var", name: string} | |
type ASTNode = (Var | number | ASTNode[]) | |
type Value = number | F | null | |
type F = (...args: Value[]) => Value | |
type Scope = {[K: string]: Value} | |
const zip = (ks: ASTNode, vs: Value[]): Scope => Object.fromEntries((ks as Var[]).map((k, i) => [k.name, vs[i]])) | |
const reduce = <T>(predicate: () => boolean, append: (v: T) => T, start: T): T => { | |
while(predicate()) start = append(start) | |
return start | |
} | |
const parse = (source: string): ASTNode => { | |
let i = 0 | |
const isWhitespace = () => " \n".includes(source[i]) | |
const isNumber = () => "0123456789".includes(source[i]) | |
const isVar = () => !" \n()".includes(source[i]) | |
const isOpening = () => source[i] === "(" | |
const isntClosing = () => source[i] !== ")" | |
const gobble = (p: () => boolean): string => reduce(p, (s: string) => s + source[i++], "") | |
const inner = (source: string): ASTNode => { | |
let node: ASTNode | |
gobble(isWhitespace) | |
if (isNumber()) node = parseInt(gobble(isNumber)) | |
else if (isVar()) node = {type: "var", name: gobble(isVar)} | |
else if (isOpening()){ | |
i++ // gobble ( | |
node = reduce(isntClosing, (l: ASTNode[]) => [...l, inner(source)], []) | |
i++ // gobble ) | |
} | |
else throw new Error("Unknown character") | |
gobble(isWhitespace) | |
return node | |
} | |
return inner(source) | |
} | |
const special: {[K: string]: (scope: Scope, children: ASTNode[]) => Value} = { | |
block: (scope, children) => { | |
const localScope = {...scope} | |
const values = children.map(child => interpret(localScope, child)) | |
return values[values.length - 1] // blocks evaluate to their final value | |
}, | |
assign: (scope, children) => { | |
const [v, node] = children as [Var, ASTNode] | |
scope[v.name] = interpret(scope, node) | |
return null | |
}, | |
function: (scope, [vars, node]): F => (...args) => interpret({...scope, ...zip(vars, args)}, node), | |
} | |
const builtins = { | |
print: (...args: any[]) => { console.log(...args); return null}, | |
negative: (a: any) => -a, | |
"*": (a: any, b: any) => a * b, | |
"+": (a: any, b: any) => a + b, | |
} | |
const interpret = (scope: Scope, node: ASTNode): Value => { | |
if (typeof node === "number") return node | |
if ("type" in node && node.type === "var") return scope[node.name] | |
const [head, ...tail] = node as [Var, ASTNode, ASTNode] | |
const specialF = special[head.name] | |
// if it's special, don't interpret the children | |
if (specialF) return specialF(scope, tail) | |
// else interpret each child, return head(...tail) | |
const [f, ...args] = (node as ASTNode[]).map(child => interpret(scope, child)) as [F, Value, Value] | |
return f(...args) | |
} | |
interpret(builtins, parse(source)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment