What is your name?
gatlin
Welcome, gatlin!
How old are you?
32
Whoa! That is 224 in dog years!
[debug] closing reader interface ...
{
"k": {
"_args": [
{
"v": "gatlin"
},
{
"v": "32"
}
],
"_k": {}
}
}
Created
July 27, 2021 02:28
-
-
Save gatlin/1924cf3cdd86cd0be6fb339e875b08ed to your computer and use it in GitHub Desktop.
Interpreter for call-by-push-value language with side effects built with robot state machine and my precursor library.
This file contains hidden or 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
import { readFile } from "fs/promises"; | |
import { createInterface } from "readline"; | |
import type { Interface } from "readline"; | |
import { CESKM, parse_cbpv, scalar, continuation, topk } from "precursor-ts"; | |
import type { State, Value, Store } from "precursor-ts"; | |
import { | |
createMachine, | |
state, | |
transition, | |
reduce, | |
guard, | |
immediate, | |
interpret, | |
invoke | |
} from "robot3"; | |
import type { Machine } from "robot3"; | |
import { signal } from "torc"; | |
import type { Signal } from "torc"; | |
const DEBUG = true; | |
function debug(...args: unknown[]) { | |
if (DEBUG) { | |
console.debug("[debug]", ...args); | |
} | |
} | |
type Base = string | number | boolean | null | Signal<Value<Base>>; | |
type VMState = IteratorResult<State<Base>, Value<Base>>; | |
type Virtual<S = {}, K = string> = Machine<S, VMState, K>; | |
type Cmd = { | |
run: { program: string }; | |
}; | |
class VM extends CESKM<Base> { | |
protected reader: Interface = createInterface({ | |
input: process.stdin, | |
output: process.stdout | |
}).on("close", () => { | |
debug("closing reader interface ..."); | |
}); | |
protected effects: (() => Promise<void>)[] = []; | |
public readonly machine: Virtual = createMachine({ | |
INIT: state( | |
transition( | |
"run", | |
"STEP", | |
reduce( | |
(vms: VMState, cmd: Cmd["run"]): VMState => | |
this.step(this.make_initial_state(parse_cbpv(cmd.program))) | |
) | |
) | |
), | |
STEP: state( | |
immediate( | |
"HALT", | |
guard((vms: VMState): boolean => vms.done ?? false), | |
reduce((vms: VMState): VMState => { | |
this.reader.close(); | |
return vms; | |
}) | |
), | |
immediate( | |
"WAIT", | |
guard((vms: VMState): boolean => this.effects.length > 0) | |
), | |
immediate( | |
"STEP", | |
reduce((vms: VMState): VMState => this.step(vms.value as State<Base>)) | |
) | |
), | |
WAIT: invoke( | |
() => | |
Promise.allSettled( | |
this.effects.map((fn: () => Promise<void>): Promise<void> => fn()) | |
), | |
transition( | |
"done", | |
"STEP", | |
reduce((vms: VMState, evt: unknown): VMState => { | |
this.effects = []; | |
return vms; | |
}) | |
), | |
transition( | |
"error", | |
"HALT", | |
reduce((vms: VMState, evt: unknown): VMState => { | |
console.error("ERROR", evt); | |
this.reader.close(); | |
return vms; | |
}) | |
) | |
), | |
HALT: state() | |
}); | |
protected store_lookup(sto: Store<Base>, addr: string): Value<Base> { | |
let val: Value<Base> = super.store_lookup(sto, addr); | |
if ( | |
"v" in val && | |
null !== val.v && | |
"object" === typeof val.v && | |
"value" in val.v | |
) { | |
val = val.v.value; // dynamically evaluate signal | |
} | |
return val; | |
} | |
protected op(op_sym: string, args: Value<Base>[]): Value<Base> { | |
switch (op_sym) { | |
case "op:readln": { | |
const input = signal<Value<Base>>(continuation(topk())); | |
this.effects.push( | |
() => | |
new Promise((resolve) => { | |
this.reader.question("", (reply) => { | |
input.next(scalar(reply)); | |
input.finish(); | |
resolve(); | |
}); | |
}) | |
); | |
return scalar(input); | |
} | |
case "op:writeln": { | |
this.effects.push(async () => { | |
if (!("v" in args[0])) { | |
throw new Error(`argument must be a value`); | |
} | |
if ("string" !== typeof args[0].v) { | |
throw new Error(`argument must be a string: ${args[0].v}`); | |
} | |
console.log(args[0].v); | |
}); | |
return scalar(null); | |
} | |
case "op:mul": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ("number" !== typeof args[0].v || "number" !== typeof args[1].v) { | |
throw new Error(`arguments must be numbers`); | |
} | |
const result: unknown = args[0].v * args[1].v; | |
return scalar(result as Base); | |
} | |
case "op:add": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ("number" !== typeof args[0].v || "number" !== typeof args[1].v) { | |
throw new Error(`arguments must be numbers`); | |
} | |
const result: unknown = args[0].v + args[1].v; | |
return scalar(result as Base); | |
} | |
case "op:sub": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ("number" !== typeof args[0].v || "number" !== typeof args[1].v) { | |
throw new Error(`arguments must be numbers`); | |
} | |
const result: unknown = args[0].v - args[1].v; | |
return scalar(result as Base); | |
} | |
case "op:eq": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ( | |
("number" !== typeof args[0].v || "number" !== typeof args[1].v) && | |
("boolean" !== typeof args[0].v || "boolean" !== typeof args[1].v) && | |
("string" !== typeof args[0].v || "string" !== typeof args[1].v) | |
) { | |
throw new Error(`arguments must be numbers or booleans or strings`); | |
} | |
const result: unknown = args[0].v === args[1].v; | |
return scalar(result as Base); | |
} | |
case "op:lt": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ("number" !== typeof args[0].v || "number" !== typeof args[1].v) { | |
throw new Error(`arguments must be numbers`); | |
} | |
const result: unknown = args[0].v < args[1].v; | |
return scalar(result as Base); | |
} | |
case "op:lte": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ("number" !== typeof args[0].v || "number" !== typeof args[1].v) { | |
throw new Error(`arguments must be numbers`); | |
} | |
const result: unknown = args[0].v <= args[1].v; | |
return scalar(result as Base); | |
} | |
case "op:not": { | |
if (!("v" in args[0])) { | |
throw new Error(`argument must be a value`); | |
} | |
if ("boolean" !== typeof args[0].v) { | |
throw new Error(`argument must be a boolean`); | |
} | |
let result = !args[0].v; | |
return scalar(result); | |
} | |
case "op:and": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ("boolean" !== typeof args[0].v || "boolean" !== typeof args[1].v) { | |
throw new Error(`arguments must be booleans`); | |
} | |
let result = args[0].v && args[1].v; | |
return scalar(result); | |
} | |
case "op:or": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ("boolean" !== typeof args[0].v || "boolean" !== typeof args[1].v) { | |
throw new Error(`arguments must be booleans`); | |
} | |
let result = args[0].v || args[1].v; | |
return scalar(result); | |
} | |
case "op:concat": { | |
if (!("v" in args[0]) || !("v" in args[1])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ("string" !== typeof args[0].v || "string" !== typeof args[1].v) { | |
throw new Error(`arguments must be strings`); | |
} | |
const result: unknown = args[0].v.concat(args[1].v); | |
return scalar(result as Base); | |
} | |
case "op:strlen": { | |
if (!("v" in args[0])) { | |
throw new Error(`argument must be a value`); | |
} | |
if ("string" !== typeof args[0].v) { | |
throw new Error(`argument must be a string`); | |
} | |
const result: unknown = args[0].v.length; | |
return scalar(result as Base); | |
} | |
case "op:substr": { | |
if (!("v" in args[0]) || !("v" in args[1]) || !("v" in args[2])) { | |
throw new Error(`arguments must be values`); | |
} | |
if ( | |
"string" !== typeof args[0].v || | |
"number" !== typeof args[1].v || | |
"number" !== typeof args[2].v | |
) { | |
throw new Error(`arguments must be strings`); | |
} | |
const result: unknown = args[0].v.slice(args[1].v, args[2].v); | |
return scalar(result as Base); | |
} | |
case "op:str->num": { | |
if (!("v" in args[0])) { | |
throw new Error(`argument must be a value`); | |
} | |
if ("string" !== typeof args[0].v) { | |
throw new Error(`argument must be a string: ${args[0].v}`); | |
} | |
return scalar(parseInt(args[0].v as string) as Base); | |
} | |
case "op:num->str": { | |
if (!("v" in args[0])) { | |
throw new Error(`argument must be a value`); | |
} | |
if ("number" !== typeof args[0].v) { | |
throw new Error(`argument must be a number: ${args[0].v}`); | |
} | |
return scalar((args[0].v as number).toString() as Base); | |
} | |
// You are encouraged (and expected!) to add more ops here. | |
default: | |
return super.op(op_sym, args); | |
} | |
} | |
protected literal(v: unknown): Value<Base> { | |
if ( | |
"number" === typeof v || | |
"boolean" === typeof v || | |
"string" === typeof v || | |
null === v | |
) { | |
return { v }; | |
} | |
throw new Error(`${v} not a primitive value`); | |
} | |
} | |
async function main(filepath: string): Promise<Value<Base>> { | |
const program: string = (await readFile(filepath)).toString("utf-8"); | |
const vm = new VM(); | |
const { | |
subscribe, | |
next, | |
finish, | |
value: service | |
} = signal( | |
interpret(vm.machine, (newService) => { | |
next(newService); | |
}) | |
); | |
return await new Promise((resolve) => { | |
subscribe({ | |
next() { | |
const { done = false, value } = service.context as VMState; | |
if (done) { | |
finish(); | |
resolve(value as Value<Base>); | |
} | |
} | |
}).finish(); | |
service.send({ type: "run", program }); | |
}); | |
} | |
(async () => { | |
console.log( | |
JSON.stringify(await main("./b.test.precursor"), null, 2) | |
); | |
})(); | |
export {}; |
This file contains hidden or 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
(letrec ( | |
; The trivial effect. | |
(return (λ (value) (shift k | |
(! (λ (f) | |
(let effect (! ((? f) "")) | |
((? effect) value))))))) | |
; Write a string to stdout and terminate with newline. | |
(writeln (λ (line) (shift k | |
(! (λ (f) | |
(let effect (! ((? f) "io:writeln" )) | |
((? effect) line k))))))) | |
; Read in a line from stdin as a string. BLOCKS. | |
(readln (λ () (shift k | |
(! (λ (f) | |
(let effect (! ((? f) "io:readln")) | |
((? effect) k))))))) | |
; Implementation of side-effects. | |
(run-fx (λ (comp) | |
(let handle (reset (? comp)) | |
((? handle) (! (λ (effect-tag) | |
(if (op:eq "" effect-tag) (λ (value) value) | |
(if (op:eq "io:writeln" effect-tag) (λ (output continue) | |
(let output (? output) | |
(let _ (op:writeln output) | |
(let res (! (continue _)) | |
((? run-fx) res))))) | |
(if (op:eq "io:readln" effect-tag) (λ (continue) | |
(let input (op:readln) | |
(let res (! (continue input)) | |
((? run-fx) res)))) | |
_ ; undefined behavior | |
))))))))) | |
; Composes writeln and readln. | |
(prompt (λ (message) | |
(let _ ((? writeln) message) | |
((? readln))))) | |
; Helper: constructs a friendly salutation for a given name. | |
(welcome (λ (name) | |
(let name (? name) | |
(op:concat "Welcome, " | |
(op:concat name "!"))))) | |
; Helper: computes an Interesting Fact™ about a given human age. | |
(dog-years (λ (age) | |
(let age (? age) | |
(let age-times-7 (op:num->str (op:mul (op:str->num age) 7)) | |
(op:concat "Whoa! That is " | |
(op:concat age-times-7 " in dog years!")))))) | |
(pair (λ (a b) (reset ((shift k k) a b)))) | |
) | |
((? run-fx) (! | |
(let name ((? prompt) "What is your name?") | |
(let _ ((? writeln) (! ((? welcome) name))) | |
(let age ((? prompt) "How old are you?") | |
(let _ ((? writeln) (! ((? dog-years) age))) | |
(let p ((? pair) name age) | |
((? return) p)))))))) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Libraries used here that aren't on NPM as of this writing (but which are mercifully small I promise):