Turtle VM

This commit is contained in:
Tony Garnock-Jones 2023-02-13 17:38:43 +01:00
parent 695049ad4b
commit 4bee2be4f0
2 changed files with 247 additions and 44 deletions

View File

@ -1,4 +1,5 @@
import {
Float,
Reader,
Record,
Value as PreservesValue,
@ -6,16 +7,16 @@ import {
stringify,
} from '@syndicate-lang/core';
export type Input = PreservesValue<unknown>[] | string;
export type Input = PreservesValue<any>[] | 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 type Value<V extends VM<V>> = number | boolean | string | Closure<V> | Primitive<V> | Value<V>[];
export type Closure<V extends VM<V>> = { env: EnvironmentChain<V>, code: Token[] };
export type Primitive<V extends VM<V>> = (this: V, ... args: Value<V>[]) => Value<V>[];
export type EnvironmentChain<V extends VM<V>> = null | { rib: Environment<V>, next: EnvironmentChain<V> };
export type Environment<V extends VM<V>> = { [key: string]: Value<V> };
export type Frame<V extends VM<V>> = Closure<V> & { ip: number };
export class FuelCell {
fuel = 10000;
@ -28,43 +29,42 @@ export class TypeError extends RuntimeError {}
export class SyntaxError extends Error {}
export function isToken(x: PreservesValue<unknown>): x is Token {
export function asToken(x: PreservesValue<unknown>): Token {
switch (typeof x) {
case 'number':
case 'boolean':
case 'symbol':
case 'string':
return true;
return x;
case 'object':
if (Float.isFloat(x)) {
return x.value;
}
if (Array.isArray(x) && !Record.isRecord(x)) {
return x.every(isToken);
return x.map(asToken);
}
/* fall through */
default:
return false;
throw new SyntaxError('Invalid program token: ' + stringify(x));
}
}
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[] = [];
export class VM<Self extends VM<Self>> {
stack: Value<Self>[] = [];
rstack: Frame<Self>[] = [];
fuel = new FuelCell();
debug = false;
static parse(program: string): Token[] {
return asTokens(new Reader(program).readToEnd());
return new Reader(program).readToEnd().map(asToken);
}
constructor(program: Input, primitives: Environment = Primitives) {
const code = typeof program === 'string' ? VM.parse(program) : asTokens(program);
constructor(program: Input, primitives: Environment<Self> = Primitives) {
const code = typeof program === 'string' ? VM.parse(program) : program.map(asToken);
this.invoke({ env: { rib: primitives, next: null }, code });
}
push(... vs: Value[]) {
push(... vs: Value<Self>[]) {
for (const v of vs) {
if (v === void 0 || v === null) {
throw new TypeError("Unexpected null/undefined");
@ -73,29 +73,29 @@ export class VM {
}
}
pop(): Value {
pop(): Value<Self> {
const v = this.stack.pop();
if (v === void 0) throw new StackUnderflow("Stack underflow");
return v;
}
take(n: number): Value[] {
take(n: number): Value<Self>[] {
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) {
invoke(c: Closure<Self> | Primitive<Self>) {
if (typeof c === 'function') {
this.push(... c.call(this, ... this.take(c.length)));
this.push(... c.call(this as unknown as Self, ... 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) {
apply(who: string, v: Value<Self>) {
switch (typeof v) {
case 'function':
return this.invoke(v);
@ -119,11 +119,11 @@ export class VM {
return false;
}
get frame(): Frame {
get frame(): Frame<Self> {
return this.rstack[this.rstack.length - 1];
}
lookup(name: string): Value {
lookup(name: string): Value<Self> {
let env = this.frame.env;
while (env !== null) {
const v = env.rib[name];
@ -193,23 +193,38 @@ export class VM {
}
exec() {
while (this.step()) {
// console.log(this._summary());
// this.popToPending();
// console.log(this.frame?.code[this.frame.ip]);
if (this.debug) {
while (this.step()) {
console.log(this._summary());
this.popToPending();
console.log(this.frame?.code[this.frame.ip]);
}
} else {
while (this.step()) {}
}
}
_summary(): string {
function vstr(v: Value): string {
function cstr(c: Token): string {
if (typeof c === 'number') {
return '' + c;
} else if (Array.isArray(c)) {
return '[' + c.map(cstr).join(' ') + ']';
} else {
return stringify(c);
}
}
function vstr(v: Value<Self>): string {
switch (typeof v) {
case 'number':
return '' + v;
case 'function':
return '#' + v.name;
case 'object':
if (Array.isArray(v)) {
return '[' + v.map(vstr).join(' ') + ']';
} else {
return '#<closure ' + stringify(v.code) + '>';
return '#<closure ' + cstr(v.code) + '>';
}
default:
return stringify(v);
@ -219,15 +234,16 @@ export class VM {
}
}
export const Primitives: Environment = {
export const Primitives: Environment<any> = {
'+'(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)]; },
'neg'(v) { return [-(v as number)]; },
'to'() {
const n_or_ns = this.nextToken('to', (v): v is (symbol | symbol[]) =>
const n_or_ns = this.nextToken('to', (v: any): 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);
@ -258,15 +274,19 @@ export const Primitives: Environment = {
'take'(n) { return [this.take(n as number)]; },
'++'(vs, ws) {
(vs as Value[]).push(... (ws as Value[]));
(vs as Value<any>[]).push(... (ws as Value<any>[]));
return [vs];
},
'!'(vs, w) {
(vs as Value<any>[]).push(w);
return [vs];
},
'?'(n, vs) { return [(vs as Value[])[n as number]]; },
'length'(a) { return [(a as Value[]).length]; },
'?'(n, vs) { return [(vs as Value<any>[])[n as number]]; },
'length'(a) { return [(a as Value<any>[]).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[]); },
'restore'(vs) { return (vs as Value<any>[]); },
'untake'(vs) { (vs as Value<any>[]).push((vs as Value<any>[]).length); return (vs as Value<any>[]); },
'not'(v) { return [!v]; },
@ -274,7 +294,7 @@ export const Primitives: Environment = {
'ifelse'(v, t, f) { if (!!v) this.apply('ifelse', t); else this.apply('ifelse', f); return []; },
};
function _code(s: string): Closure {
function _code(s: string): Closure<any> {
return ({ env: { rib: Primitives, next: null }, code: VM.parse(s) });
}

183
src/turtle.ts Normal file
View File

@ -0,0 +1,183 @@
import {
Mesh,
MeshBuilder,
Quaternion,
Scene,
Vector3,
} from '@babylonjs/core/Legacy/legacy';
import { VM as BaseVM, Input, Primitives, Environment } from './cat.js';
export class PenState {
templatePath: Vector3[] = [new Vector3()];
paths: Vector3[][] | null = null;
templateScale = new Vector3(1, 1, 1);
get isDown(): boolean {
return this.paths !== null;
}
clear() {
this.templatePath = [new Vector3()];
this.paths = null;
}
set() {
if (!this.paths) throw new Error("Cannot set pen with no paths");
this.templatePath = this.paths[0];
this.paths = null;
}
down() {
this.paths = this.templatePath.map(_p => []);
}
push(pos: Vector3, q: Quaternion) {
this.templatePath.forEach((p, i) => {
const r = new Vector3();
p.multiplyToRef(this.templateScale, r);
r.rotateByQuaternionToRef(q, r);
r.addInPlace(pos);
this.paths![i].push(r);
});
}
};
const D2R = Math.PI / 180;
export class TurtleVM extends BaseVM<TurtleVM> {
container: Mesh;
counter = 0;
sideOrientation = Mesh.BACKSIDE; // TODO: see SideOrientation primitive below
pen = new PenState();
pos = new Vector3();
q = new Quaternion();
cornerDivisions = 0;
get euler(): Vector3 {
return this.q.toEulerAngles();
}
constructor(
name: string,
scene: Scene | null,
program: Input,
) {
super(program, TurtlePrimitives);
this.container = new Mesh(name, scene);
}
forwardBy(dist: number) {
const v = new Vector3(0, 0, dist);
v.rotateByQuaternionToRef(this.q, v);
this.pos.addInPlace(v);
if (this.pen.isDown) this.pen.push(this.pos, this.q);
}
setQ(newQ: Quaternion) {
if (this.cornerDivisions === 0) {
this.q = newQ;
} else {
this.q = newQ;
this.pen.push(this.pos, this.q);
}
}
rotate(y: number, x: number, z: number) {
const e = this.euler;
e.addInPlaceFromFloats(x * D2R, y * D2R, z * D2R);
this.setQ(e.toQuaternion());
}
relativeRotate(y: number, x: number, z: number) {
this.setQ(this.q.multiply(Quaternion.FromEulerAngles(x * D2R, y * D2R, z * D2R)));
}
penDown() {
this.pen.down();
this.pen.push(this.pos, this.q);
}
penUp(close: boolean) {
if (!this.pen.isDown) return;
if (close) {
throw new Error('todo');
}
const m = MeshBuilder.CreateRibbon(this.container.name + this.counter++, {
pathArray: this.pen.paths!,
sideOrientation: this.sideOrientation,
});
m.parent = this.container;
}
}
export const TurtlePrimitives: Environment<TurtleVM> = Object.assign({}, Primitives, {
'Home'() { this.pos = new Vector3(); this.q = new Quaternion(); return []; },
'GetPos'() { return [this.pos.asArray()]; },
'GetX'() { return [this.pos.x]; },
'GetY'() { return [this.pos.y]; },
'GetZ'() { return [this.pos.z]; },
'SetX'(v) { this.pos.x = v as number; return []; },
'SetY'(v) { this.pos.y = v as number; return []; },
'SetZ'(v) { this.pos.z = v as number; return []; },
'SetPos'(x, y, z) {
this.pos.set(x as number, y as number, z as number);
return [];
},
'GetHeading'() { return [this.euler.asArray().map(v => v / D2R)]; },
'GetRX'() { return [this.euler.x / D2R]; },
'GetRY'() { return [this.euler.y / D2R]; },
'GetRZ'() { return [this.euler.z / D2R]; },
'SetRX'(v) { this.q.x = v as number * D2R; return []; },
'SetRY'(v) { this.q.y = v as number * D2R; return []; },
'SetRZ'(v) { this.q.z = v as number * D2R; return []; },
'SetHeading'(x, y, z) {
this.q = Quaternion.FromEulerAngles(x as number * D2R, y as number * D2R, z as number * D2R);
return [];
},
'F'(dist) { this.forwardBy(dist as number); return []; },
'B'(dist) { this.forwardBy(-(dist as number)); return []; },
'RX'(degrees) { this.rotate(0, degrees as number, 0); return []; },
'RY'(degrees) { this.rotate(degrees as number, 0, 0); return []; },
'RZ'(degrees) { this.rotate(0, 0, degrees as number); return []; },
'U'(degrees) { this.relativeRotate(0, -(degrees as number), 0); return []; },
'D'(degrees) { this.relativeRotate(0, degrees as number, 0); return []; },
'L'(degrees) { this.relativeRotate(-(degrees as number), 0, 0); return []; },
'R'(degrees) { this.relativeRotate(degrees as number, 0, 0); return []; },
'CW'(degrees) { this.relativeRotate(0, 0, -(degrees as number)); return []; },
'CCW'(degrees) { this.relativeRotate(0, 0, degrees as number); return []; },
'ClearPen'() { this.pen.clear(); return []; },
'SetPen'() { this.pen.set(); return []; },
'PenScale'(sx, sy, sz) {
this.pen.templateScale = new Vector3(sx as number, sy as number, sz as number);
return [];
},
'PenDown'() { this.penDown(); return []; },
'PenUp'() { this.penUp(false); return []; },
'Close'() { this.penUp(true); return []; },
'SideOrientation'(s) {
// TODO: why is this back to front?? argh
switch (s) {
case "default": this.sideOrientation = Mesh.BACKSIDE; break;
case "front": this.sideOrientation = Mesh.BACKSIDE; break;
case "back": this.sideOrientation = Mesh.FRONTSIDE; break;
case "double": this.sideOrientation = Mesh.DOUBLESIDE; break;
default:
break;
}
return [];
},
'CornerDivisions'(n) { this.cornerDivisions = n as number; return []; },
} satisfies Environment<TurtleVM>);