Skip to content

Instantly share code, notes, and snippets.

@gatlin
Created July 27, 2021 02:28
Show Gist options
  • Save gatlin/1924cf3cdd86cd0be6fb339e875b08ed to your computer and use it in GitHub Desktop.
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.
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 {};
(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))))))))
)
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": {}
  }
}
@gatlin
Copy link
Author

gatlin commented Jul 27, 2021

Libraries used here that aren't on NPM as of this writing (but which are mercifully small I promise):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment