Small concatenative language
This commit is contained in:
parent
3dab93e6d0
commit
695049ad4b
|
@ -0,0 +1,308 @@
|
|||
import {
|
||||
Reader,
|
||||
Record,
|
||||
Value as PreservesValue,
|
||||
is,
|
||||
stringify,
|
||||
} from '@syndicate-lang/core';
|
||||
|
||||
export type Input = PreservesValue<unknown>[] | string;
|
||||
|
||||
export type Token = number | boolean | string | symbol | Token[];
|
||||
|
||||
export type Value = number | boolean | string | Closure | Primitive | Value[];
|
||||
export type Closure = { env: EnvironmentChain, code: Token[] };
|
||||
export type Primitive = (this: VM, ... args: Value[]) => Value[];
|
||||
export type EnvironmentChain = null | { rib: Environment, next: EnvironmentChain };
|
||||
export type Environment = { [key: string]: Value };
|
||||
export type Frame = Closure & { ip: number };
|
||||
|
||||
export class FuelCell {
|
||||
fuel = 10000;
|
||||
}
|
||||
|
||||
export class RuntimeError extends Error {}
|
||||
export class FuelExhausted extends RuntimeError {}
|
||||
export class StackUnderflow extends RuntimeError {}
|
||||
export class TypeError extends RuntimeError {}
|
||||
|
||||
export class SyntaxError extends Error {}
|
||||
|
||||
export function isToken(x: PreservesValue<unknown>): x is Token {
|
||||
switch (typeof x) {
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'symbol':
|
||||
case 'string':
|
||||
return true;
|
||||
case 'object':
|
||||
if (Array.isArray(x) && !Record.isRecord(x)) {
|
||||
return x.every(isToken);
|
||||
}
|
||||
/* fall through */
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function asTokens(xs: PreservesValue<unknown>[]): Token[] {
|
||||
if (!isToken(xs)) throw new SyntaxError('Invalid program: ' + stringify(xs));
|
||||
return xs;
|
||||
}
|
||||
|
||||
export class VM {
|
||||
stack: Value[] = [];
|
||||
rstack: Frame[] = [];
|
||||
fuel = new FuelCell();
|
||||
|
||||
static parse(program: string): Token[] {
|
||||
return asTokens(new Reader(program).readToEnd());
|
||||
}
|
||||
|
||||
constructor(program: Input, primitives: Environment = Primitives) {
|
||||
const code = typeof program === 'string' ? VM.parse(program) : asTokens(program);
|
||||
this.invoke({ env: { rib: primitives, next: null }, code });
|
||||
}
|
||||
|
||||
push(... vs: Value[]) {
|
||||
for (const v of vs) {
|
||||
if (v === void 0 || v === null) {
|
||||
throw new TypeError("Unexpected null/undefined");
|
||||
}
|
||||
this.stack.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
pop(): Value {
|
||||
const v = this.stack.pop();
|
||||
if (v === void 0) throw new StackUnderflow("Stack underflow");
|
||||
return v;
|
||||
}
|
||||
|
||||
take(n: number): Value[] {
|
||||
if (this.stack.length < n) {
|
||||
throw new StackUnderflow("Stack underflow: need " +n+", have "+this.stack.length);
|
||||
}
|
||||
return n > 0 ? this.stack.splice(-n) : [];
|
||||
}
|
||||
|
||||
invoke(c: Closure | Primitive) {
|
||||
if (typeof c === 'function') {
|
||||
this.push(... c.call(this, ... this.take(c.length)));
|
||||
} else {
|
||||
this.popToPending(); // tail calls
|
||||
this.rstack.push({ env: { rib: {}, next: c.env }, code: c.code, ip: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
apply(who: string, v: Value) {
|
||||
switch (typeof v) {
|
||||
case 'function':
|
||||
return this.invoke(v);
|
||||
case 'object':
|
||||
if (!Array.isArray(v)) {
|
||||
return this.invoke(v);
|
||||
}
|
||||
/* fall through */
|
||||
default:
|
||||
throw new TypeError('Got non-callable in `'+who+'`: ' + stringify(v));
|
||||
}
|
||||
}
|
||||
|
||||
popToPending(): boolean {
|
||||
while (this.rstack.length > 0) {
|
||||
if (this.frame.ip < this.frame.code.length) {
|
||||
return true;
|
||||
}
|
||||
this.rstack.pop();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get frame(): Frame {
|
||||
return this.rstack[this.rstack.length - 1];
|
||||
}
|
||||
|
||||
lookup(name: string): Value {
|
||||
let env = this.frame.env;
|
||||
while (env !== null) {
|
||||
const v = env.rib[name];
|
||||
if (v !== void 0) return v;
|
||||
env = env.next;
|
||||
}
|
||||
throw new Error("Unknown operator: " + name);
|
||||
}
|
||||
|
||||
step(): boolean {
|
||||
if (!this.popToPending()) return false;
|
||||
|
||||
if (this.fuel.fuel <= 0) throw new FuelExhausted("Fuel exhausted");
|
||||
this.fuel.fuel--;
|
||||
|
||||
const op = this.frame.code[this.frame.ip++];
|
||||
|
||||
switch (typeof op) {
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'string':
|
||||
this.push(op);
|
||||
break;
|
||||
|
||||
case 'symbol':
|
||||
const v = this.lookup(op.description!);
|
||||
switch (typeof v) {
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'string':
|
||||
this.push(v);
|
||||
break;
|
||||
|
||||
case 'function':
|
||||
case 'object':
|
||||
if (Array.isArray(v)) {
|
||||
this.push(v);
|
||||
} else {
|
||||
this.invoke(v);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
((_: never) => { throw new Error("Unhandled environment value: " + _); })(v);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
this.push({ env: this.frame.env, code: op });
|
||||
break;
|
||||
|
||||
default:
|
||||
((_: never) => { throw new Error("Unhandled token: " + _); })(op);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
nextToken<T extends Token>(who: string, f: ((v: Token) => v is T) = ((_v): _v is T => true)): T {
|
||||
const t = this.frame.code[this.frame.ip++];
|
||||
if (typeof t === 'undefined') throw new SyntaxError("Missing token after `"+who+"`");
|
||||
if (!f(t)) throw new SyntaxError("Syntax error after `"+who+"`");
|
||||
return t;
|
||||
}
|
||||
|
||||
nextLiteralSymbol(who: string): symbol {
|
||||
return this.nextToken(who, (v): v is symbol => typeof v === 'symbol');
|
||||
}
|
||||
|
||||
exec() {
|
||||
while (this.step()) {
|
||||
// console.log(this._summary());
|
||||
// this.popToPending();
|
||||
// console.log(this.frame?.code[this.frame.ip]);
|
||||
}
|
||||
}
|
||||
|
||||
_summary(): string {
|
||||
function vstr(v: Value): string {
|
||||
switch (typeof v) {
|
||||
case 'function':
|
||||
return '#' + v.name;
|
||||
case 'object':
|
||||
if (Array.isArray(v)) {
|
||||
return '[' + v.map(vstr).join(' ') + ']';
|
||||
} else {
|
||||
return '#<closure ' + stringify(v.code) + '>';
|
||||
}
|
||||
default:
|
||||
return stringify(v);
|
||||
}
|
||||
}
|
||||
return `${this.rstack.length}: ${vstr(this.stack)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const Primitives: Environment = {
|
||||
'+'(a, b) { return [(a as number) + (b as number)]; },
|
||||
'-'(a, b) { return [(a as number) - (b as number)]; },
|
||||
'*'(a, b) { return [(a as number) * (b as number)]; },
|
||||
'/'(a, b) { return [(a as number) / (b as number)]; },
|
||||
'%'(a, b) { return [(a as number) % (b as number)]; },
|
||||
|
||||
'to'() {
|
||||
const n_or_ns = this.nextToken('to', (v): v is (symbol | symbol[]) =>
|
||||
typeof v === 'symbol' || (Array.isArray(v) && v.every(w => typeof w === 'symbol')));
|
||||
const ns = Array.isArray(n_or_ns) ? n_or_ns : [n_or_ns];
|
||||
const vs = this.take(ns.length);
|
||||
const env = this.frame.env!;
|
||||
for (let i = 0; i < ns.length; i++) env.rib[ns[i].description!] = vs[i];
|
||||
return [];
|
||||
},
|
||||
'quote'() {
|
||||
return [this.lookup(this.nextLiteralSymbol('quote').description!)];
|
||||
},
|
||||
'apply'(v) {
|
||||
this.apply('apply', v);
|
||||
return [];
|
||||
},
|
||||
|
||||
'eq'(a, b) { return [is(a, b)]; },
|
||||
'lt'(a, b) { return [a < b]; },
|
||||
'gt'(a, b) { return [a > b]; },
|
||||
'le'(a, b) { return [a <= b]; },
|
||||
'ge'(a, b) { return [a >= b]; },
|
||||
|
||||
'swap'(v, w) { return [w, v]; },
|
||||
'dup'(v) { return [v, v]; },
|
||||
'over'(v, w) { return [v, w, v]; },
|
||||
'rot'(v, w, x) { return [w, x, v]; },
|
||||
'-rot'(v, w, x) { return [x, v, w]; },
|
||||
'drop'(_v) { return []; },
|
||||
|
||||
'take'(n) { return [this.take(n as number)]; },
|
||||
'++'(vs, ws) {
|
||||
(vs as Value[]).push(... (ws as Value[]));
|
||||
return [vs];
|
||||
},
|
||||
|
||||
'?'(n, vs) { return [(vs as Value[])[n as number]]; },
|
||||
'length'(a) { return [(a as Value[]).length]; },
|
||||
'save'() { return [this.take(this.stack.length)]; },
|
||||
'restore'(vs) { return (vs as Value[]); },
|
||||
'untake'(vs) { (vs as Value[]).push((vs as Value[]).length); return (vs as Value[]); },
|
||||
|
||||
'not'(v) { return [!v]; },
|
||||
|
||||
'if'(v, t) { if (!!v) this.apply('if', t); return []; },
|
||||
'ifelse'(v, t, f) { if (!!v) this.apply('ifelse', t); else this.apply('ifelse', f); return []; },
|
||||
};
|
||||
|
||||
function _code(s: string): Closure {
|
||||
return ({ env: { rib: Primitives, next: null }, code: VM.parse(s) });
|
||||
}
|
||||
|
||||
Object.assign(Primitives, {
|
||||
'prepend': _code('to [vs n] n take vs ++'),
|
||||
'times': _code('dup 0 le [drop drop] [to [c n] c quote c n 1 - times] ifelse'),
|
||||
'iota': _code('to [n] 0 [dup 1 +] n times take'),
|
||||
'map': _code('to [vs f] 0 [dup vs ? f swap 1 + ] vs length times take'),
|
||||
'flatMap': _code('to [vs f] 0 take 0 [dup vs ? f rot swap ++ swap 1 +] vs length times drop'),
|
||||
'filter': _code('to [vs f] vs [dup f [1 take] [drop 0 take] ifelse] flatMap'),
|
||||
});
|
||||
|
||||
// function R(s: string) {
|
||||
// console.log('\n' + s);
|
||||
// try {
|
||||
// const vm = new VM(s);
|
||||
// vm.exec();
|
||||
// console.log(s, '-->', vm.stack);
|
||||
// } catch (e) {
|
||||
// console.error(s, '-/->', (e as Error).message);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// R('[dup 0 eq [drop] [dup 1 - x] ifelse] to x 5 x')
|
||||
// R('[dup 0 eq [drop] [dup to v 1 - x v] ifelse] to x 5 x')
|
||||
// R('[1 - dup 0 lt [drop] [dup x] ifelse] to x 5 x')
|
||||
// R('[1 - dup 0 lt [drop] [dup to v x v] ifelse] to x 5 x')
|
||||
// R('[3] 5 times');
|
||||
// R('2 3 4 save [2 *] map restore');
|
||||
// R('20 iota [dup 3 % 0 eq [1 take] [drop 0 take] ifelse] flatMap restore');
|
||||
// R('20 iota [3 % 0 eq] filter restore');
|
Loading…
Reference in New Issue