diff --git a/src/cat.ts b/src/cat.ts index f02b3ae..2b9feec 100644 --- a/src/cat.ts +++ b/src/cat.ts @@ -3,7 +3,9 @@ import { Reader, Record, Value as PreservesValue, + embed, is, + isEmbedded, stringify, } from '@syndicate-lang/core'; @@ -11,7 +13,7 @@ export type Input = PreservesValue[] | string; export type Token = number | boolean | string | symbol | Token[]; -export type Value> = number | boolean | string | Closure | Primitive | Value[]; +export type Value> = PreservesValue | Primitive>; export type Closure> = { env: EnvironmentChain, code: Token[] }; export type Primitive> = (this: V, ... args: Value[]) => Value[]; export type EnvironmentChain> = null | { rib: Environment, next: EnvironmentChain }; @@ -96,16 +98,10 @@ export class VM> { } 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)); + if (isEmbedded(v)) { + return this.invoke(v.embeddedValue); + } else { + throw new TypeError('Got non-callable in `'+who+'`: ' + stringify(v)); } } @@ -150,29 +146,15 @@ export class VM> { 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); + if (isEmbedded(v)) { + this.invoke(v.embeddedValue); + } else { + this.push(v); } break; case 'object': - this.push({ env: this.frame.env, code: op }); + this.push(closure(this.frame.env, op)); break; default: @@ -205,38 +187,24 @@ export class VM> { } _summary(): 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 '#'; - } - default: - return stringify(v); - } - } - return `${this.rstack.length}: ${vstr(this.stack)}`; + return `${this.rstack.length}: ${stringify(this.stack)}`; } } +export function primitiveEnvironment>( + input: { [key: string]: Primitive }, +): Environment { + const output: Environment = {}; + Object.entries(input).forEach(([k, f]) => { + f.toString = () => `#${k}`; + output[k] = embed(f); + }); + return output; +} + export const D2R = Math.PI / 180; -export const Primitives: Environment = { +export const Primitives: Environment = primitiveEnvironment({ '+'(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)]; }, @@ -249,12 +217,14 @@ export const Primitives: Environment = { 'tan'(n) { return [Math.tan(n as number * D2R)]; }, 'to'() { - 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 n_or_ns = this.nextToken('to', (v: any): v is (symbol | symbol[]) => { + return 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]; + ns.forEach((n, i) => env.rib[n.description!] = vs[i]); return []; }, 'quote'() { @@ -266,10 +236,10 @@ export const Primitives: Environment = { }, '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]; }, + 'lt'(a, b) { return [(a as any) < (b as any)]; }, + 'gt'(a, b) { return [(a as any) > (b as any)]; }, + 'le'(a, b) { return [(a as any) <= (b as any)]; }, + 'ge'(a, b) { return [(a as any) >= (b as any)]; }, 'swap'(v, w) { return [w, v]; }, 'dup'(v) { return [v, v]; }, @@ -300,10 +270,14 @@ export const Primitives: Environment = { 'ifelse'(v, t, f) { if (!!v) this.apply('ifelse', t); else this.apply('ifelse', f); return []; }, '*dump*'() { console.log(this._summary()); return []; }, -}; +}); -function _code(s: string): Closure { - return ({ env: { rib: Primitives, next: null }, code: VM.parse(s) }); +function closure>(env: EnvironmentChain, code: Token[]): Value { + return embed({ env, code, toString: () => `#` }); +} + +function _code(s: string): Value { + return closure({ rib: Primitives, next: null }, VM.parse(s)); } Object.assign(Primitives, { diff --git a/src/turtle.ts b/src/turtle.ts index 04b3cd1..6addba6 100644 --- a/src/turtle.ts +++ b/src/turtle.ts @@ -3,11 +3,11 @@ import { MeshBuilder, Plane, Quaternion, - Ray, Scene, Vector3, VertexData, } from '@babylonjs/core/Legacy/legacy'; +import { stringify } from '@preserves/core'; import * as Cat from './cat.js'; import { earcut } from './earcut'; @@ -293,85 +293,89 @@ export class TurtleVM extends Cat.VM { } } -export const TurtlePrimitives: Cat.Environment = Object.assign({}, Cat.Primitives, { - 'Home'() { this.pos = new Vector3(); this.q = new Quaternion(); return []; }, +export const TurtlePrimitives: Cat.Environment = Object.assign( + {}, + Cat.Primitives, + Cat.primitiveEnvironment({ + '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'(v) { - const [x, y, z] = v as number[]; - this.pos.set(x as number, y as number, z as number); - 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'(v) { + const [x, y, z] = v as number[]; + this.pos.set(x as number, y as number, z as number); + return []; + }, - 'GetHeading'() { return [this.euler.asArray().map(v => v / Cat.D2R)]; }, - 'GetRX'() { return [this.euler.x / Cat.D2R]; }, - 'GetRY'() { return [this.euler.y / Cat.D2R]; }, - 'GetRZ'() { return [this.euler.z / Cat.D2R]; }, - 'SetRX'(v) { this.q.x = v as number * Cat.D2R; return []; }, - 'SetRY'(v) { this.q.y = v as number * Cat.D2R; return []; }, - 'SetRZ'(v) { this.q.z = v as number * Cat.D2R; return []; }, - 'SetHeading'(v) { - const [x, y, z] = v as number[]; - this.q = Quaternion.FromEulerAngles(x as number * Cat.D2R, y as number * Cat.D2R, z as number * Cat.D2R); - return []; - }, + 'GetHeading'() { return [this.euler.asArray().map(v => v / Cat.D2R)]; }, + 'GetRX'() { return [this.euler.x / Cat.D2R]; }, + 'GetRY'() { return [this.euler.y / Cat.D2R]; }, + 'GetRZ'() { return [this.euler.z / Cat.D2R]; }, + 'SetRX'(v) { this.q.x = v as number * Cat.D2R; return []; }, + 'SetRY'(v) { this.q.y = v as number * Cat.D2R; return []; }, + 'SetRZ'(v) { this.q.z = v as number * Cat.D2R; return []; }, + 'SetHeading'(v) { + const [x, y, z] = v as number[]; + this.q = Quaternion.FromEulerAngles(x as number * Cat.D2R, y as number * Cat.D2R, z as number * Cat.D2R); + return []; + }, - 'F'(dist) { this.forwardBy(dist as number); return []; }, - 'B'(dist) { this.forwardBy(-(dist as number)); 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 []; }, + '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 []; }, + '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 []; }, - 'DefinePen'() { this.pen.set(); return []; }, - 'GetPen'() { return [this.pen.templatePath.map(v => v.asArray())]; }, - 'SetPen'(p0) { - const p = p0 as [number, number, number][]; - if (this.pen.isDown && this.pen.templatePath.length !== p.length) { - throw new PenError("Cannot set pen with different number of points when pen is down"); - } - this.pen.templatePath = p.map(v => Vector3.FromArray(v)); - return []; - }, + 'ClearPen'() { this.pen.clear(); return []; }, + 'DefinePen'() { this.pen.set(); return []; }, + 'GetPen'() { return [this.pen.templatePath.map(v => v.asArray())]; }, + 'SetPen'(p0) { + const p = p0 as [number, number, number][]; + if (this.pen.isDown && this.pen.templatePath.length !== p.length) { + throw new PenError("Cannot set pen with different number of points when pen is down"); + } + this.pen.templatePath = p.map(v => Vector3.FromArray(v)); + return []; + }, - 'PenDown'() { this.penDown(); return []; }, - 'PenUp'() { this.penUp(false); return []; }, - 'Close'() { this.penUp(true); return []; }, + 'PenDown'() { this.penDown(); return []; }, + 'PenUp'() { this.penUp(false); return []; }, + 'Close'() { this.penUp(true); return []; }, - 'SetCap'(t) { - const n = t as keyof typeof CapType; - if (typeof t === 'string' && n in CapType) { - this.cap = CapType[n]; - } else { - throw new Cat.TypeError("Bad cap type: " + t); - } - return []; - }, - 'SetWireframe'(b) { this.wireframe = b as boolean; return []; }, - 'SetSmooth'(b) { this.smooth = b as boolean; return []; }, - 'SetMiter'(b) { this.miter = b as boolean; return []; }, - 'SetSideOrientation'(s) { - switch (s) { - case "DEFAULT": this.sideOrientation = Mesh.DEFAULTSIDE; break; - case "FRONT": this.sideOrientation = Mesh.FRONTSIDE; break; - case "BACK": this.sideOrientation = Mesh.BACKSIDE; break; - case "DOUBLE": this.sideOrientation = Mesh.DOUBLESIDE; break; - default: throw new TypeError("Invalid SideOrientation: " + s); - } - return []; - }, -} satisfies Cat.Environment); + 'SetCap'(t) { + const n = t as keyof typeof CapType; + if (typeof t === 'string' && n in CapType) { + this.cap = CapType[n]; + } else { + throw new Cat.TypeError("Bad cap type: " + stringify(t)); + } + return []; + }, + 'SetWireframe'(b) { this.wireframe = b as boolean; return []; }, + 'SetSmooth'(b) { this.smooth = b as boolean; return []; }, + 'SetMiter'(b) { this.miter = b as boolean; return []; }, + 'SetSideOrientation'(s) { + switch (s) { + case "DEFAULT": this.sideOrientation = Mesh.DEFAULTSIDE; break; + case "FRONT": this.sideOrientation = Mesh.FRONTSIDE; break; + case "BACK": this.sideOrientation = Mesh.BACKSIDE; break; + case "DOUBLE": this.sideOrientation = Mesh.DOUBLESIDE; break; + default: throw new TypeError("Invalid SideOrientation: " + stringify(s)); + } + return []; + }, + }) satisfies Cat.Environment, +);