From 4bee2be4f06e4c91ba6a3cd26d03745ff6b9f61f Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Mon, 13 Feb 2023 17:38:43 +0100 Subject: [PATCH] Turtle VM --- src/cat.ts | 108 +++++++++++++++++------------ src/turtle.ts | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 44 deletions(-) create mode 100644 src/turtle.ts diff --git a/src/cat.ts b/src/cat.ts index bcd9f2c..46b91b0 100644 --- a/src/cat.ts +++ b/src/cat.ts @@ -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[] | string; +export type Input = PreservesValue[] | 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> = number | boolean | string | Closure | Primitive | Value[]; +export type Closure> = { env: EnvironmentChain, code: Token[] }; +export type Primitive> = (this: V, ... 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; @@ -28,43 +29,42 @@ export class TypeError extends RuntimeError {} export class SyntaxError extends Error {} -export function isToken(x: PreservesValue): x is Token { +export function asToken(x: PreservesValue): 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[]): Token[] { - if (!isToken(xs)) throw new SyntaxError('Invalid program: ' + stringify(xs)); - return xs; -} - -export class VM { - stack: Value[] = []; - rstack: Frame[] = []; +export class VM> { + stack: Value[] = []; + rstack: Frame[] = []; 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 = 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[]) { 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 { const v = this.stack.pop(); if (v === void 0) throw new StackUnderflow("Stack underflow"); return v; } - take(n: number): Value[] { + 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) { + invoke(c: Closure | Primitive) { 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) { switch (typeof v) { case 'function': return this.invoke(v); @@ -119,11 +119,11 @@ export class VM { return false; } - get frame(): Frame { + get frame(): Frame { return this.rstack[this.rstack.length - 1]; } - lookup(name: string): Value { + lookup(name: string): Value { 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): 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 '#'; + return '#'; } default: return stringify(v); @@ -219,15 +234,16 @@ export class VM { } } -export const Primitives: Environment = { +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)]; }, + '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[]).push(... (ws as Value[])); + return [vs]; + }, + '!'(vs, w) { + (vs as Value[]).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[])[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[]); }, + 'restore'(vs) { return (vs as Value[]); }, + 'untake'(vs) { (vs as Value[]).push((vs as Value[]).length); return (vs as Value[]); }, '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 { return ({ env: { rib: Primitives, next: null }, code: VM.parse(s) }); } diff --git a/src/turtle.ts b/src/turtle.ts new file mode 100644 index 0000000..9847657 --- /dev/null +++ b/src/turtle.ts @@ -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 { + 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 = 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);