Skip to content

Instantly share code, notes, and snippets.

@leontrolski
Last active March 4, 2022 16:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leontrolski/6588a2a8709b5e1331da34346b9608a3 to your computer and use it in GitHub Desktop.
Save leontrolski/6588a2a8709b5e1331da34346b9608a3 to your computer and use it in GitHub Desktop.
interpreter with assignment
// 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