openzoo 0.51.2 → 0.51.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,591 @@
1
+ // IR → bytecode for the shared runtime (runtime/zoo-host/src/vm.rs).
2
+ //
3
+ // Same IR the Rust printer consumes, same semantics: every expression leaves
4
+ // one `Val` on the stack; statements leave nothing. Callbacks, local closures,
5
+ // IIFEs and try bodies are inline regions of their parent function sharing its
6
+ // local slots and getting their own `args` — which is exactly how the Rust
7
+ // backend's closures see the enclosing bindings.
8
+ import { helperIds, hostIds } from './hostids.js';
9
+
10
+ export const MAGIC = Buffer.from('ZOOB');
11
+ export const VERSION = 1;
12
+
13
+ export const OP = {
14
+ NOP: 0, PUSH_CONST: 1, PUSH_UNDEF: 2, PUSH_NULL: 3, DUP: 4, POP: 5, SWAP: 6, LOAD: 7, STORE: 8, LOAD_ARG: 9, ARGS_FROM: 10,
15
+ NEW_OBJ: 11, OBJ_SET_K: 12, OBJ_SET: 13, OBJ_SPREAD: 14, NEW_ARR: 15, ARR_PUSH: 16, ARR_SPREAD: 17, GET_K: 18, GET: 19,
16
+ TEMPLATE: 20, BIN: 21, CMP: 22, UNARY: 23, IN: 24, JMP: 25, JF: 26, JT: 27, JF_KEEP: 28, JT_KEEP: 29, JNN_KEEP: 30,
17
+ JNULLISH_UNDEF: 31, CALLM: 32, HOST: 33, HELPER: 34, MATH: 35, GLOBAL: 36, JSON_PARSE: 37, JSON_STRINGIFY: 38, KEYS: 39,
18
+ VALUES: 40, ENTRIES: 41, ISARRAY: 42, NEW_ERROR: 43, LOG: 44, RESP: 45, PARAMS: 46, PARAM: 47, CALL_FN: 48, CALL_INLINE: 49,
19
+ RET: 50, SEND: 51, THROW: 52, TRY_PUSH: 53, TRY_POP: 54, CAUGHT: 55, ITER_INIT: 56, ITER_NEXT: 57, STORE_PATH: 58,
20
+ STORE_PATH_DISCARD: 59, DELETE_PATH: 60, TRUTHY: 61, NULLISH: 62, STRICT_EQ_KEEP: 63,
21
+ };
22
+ const BIN = { add: 0, sub: 1, mul: 2, div: 3, rem: 4, pow: 5, bit_and: 6, bit_or: 7, bit_xor: 8, shl: 9, shr: 10, ushr: 11 };
23
+ const CMP = { strict_eq: 0, strict_ne: 1, loose_eq: 2, loose_ne: 3, lt: 4, le: 5, gt: 6, ge: 7 };
24
+ const UNARY = { neg: 0, plus: 1, not: 2, bitnot: 3, typeof: 4, void: 5 };
25
+ const CALLM_CB = 1, CALLM_DEFAULT_SORT = 2, CALLM_MUT = 4;
26
+ const HOST_Q = 1, HOST_VOID = 2;
27
+ const METHOD_INDEX = { GET: 0, POST: 1, PUT: 2, DELETE: 3, PATCH: 4, OPTIONS: 5, HEAD: 6 };
28
+
29
+ export class BytecodeError extends Error {}
30
+
31
+ /** Module-wide constant pool. */
32
+ class Consts {
33
+ constructor() { this.list = []; this.index = new Map(); }
34
+ key(v) { return typeof v === 'number' ? (Number.isNaN(v) ? 'n:NaN' : `n:${Object.is(v, -0) ? '-0' : v}`) : `${typeof v}:${v}`; }
35
+ add(v) {
36
+ const k = this.key(v);
37
+ if (this.index.has(k)) return this.index.get(k);
38
+ const i = this.list.length;
39
+ if (i >= 65535) throw new BytecodeError('too many constants');
40
+ this.list.push(v); this.index.set(k, i);
41
+ return i;
42
+ }
43
+ encode() {
44
+ const parts = [u16(this.list.length)];
45
+ for (const v of this.list) {
46
+ if (v === undefined) parts.push(Buffer.from([0]));
47
+ else if (v === null) parts.push(Buffer.from([1]));
48
+ else if (v === false) parts.push(Buffer.from([2]));
49
+ else if (v === true) parts.push(Buffer.from([3]));
50
+ else if (typeof v === 'number') { const b = Buffer.alloc(9); b[0] = 4; b.writeDoubleLE(v, 1); parts.push(b); }
51
+ else { const s = Buffer.from(String(v), 'utf8'); parts.push(Buffer.from([5]), u32(s.length), s); }
52
+ }
53
+ return Buffer.concat(parts);
54
+ }
55
+ }
56
+ function u16(n) { const b = Buffer.alloc(2); b.writeUInt16LE(n); return b; }
57
+ function u32(n) { const b = Buffer.alloc(4); b.writeUInt32LE(n); return b; }
58
+
59
+ /** One top-level function's code: slot table, jumps, labels. */
60
+ class Fn {
61
+ constructor(consts, mod) {
62
+ this.consts = consts; this.mod = mod;
63
+ this.code = []; // bytes
64
+ this.slots = new Map(); // name -> slot
65
+ this.nlocals = 0;
66
+ this.labels = []; // loop/switch/chain label contexts
67
+ this.tries = []; // active try contexts: { finalizer, phase, tryDepthAtLoop? }
68
+ this.inlines = new Map(); // letfn name -> code offset
69
+ this.tmp = 0;
70
+ }
71
+ slot(name) {
72
+ if (!this.slots.has(name)) { this.slots.set(name, this.nlocals++); if (this.nlocals > 65535) throw new BytecodeError('too many locals'); }
73
+ return this.slots.get(name);
74
+ }
75
+ temp() { return this.slot(`__t${this.tmp++}`); }
76
+ emit(...bytes) { for (const b of bytes) this.code.push(b & 0xff); }
77
+ u16(n) { this.emit(n & 0xff, (n >> 8) & 0xff); }
78
+ u32(n) { this.emit(n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff); }
79
+ i32(n) { this.u32(n >>> 0); }
80
+ pos() { return this.code.length; }
81
+ /** Emit a jump with a patchable 4-byte offset; returns the patch site. */
82
+ jmp(op) { this.emit(op); const at = this.pos(); this.i32(0); return at; }
83
+ /** Point the jump at `at` to the current position (offset relative to the end of the operand). */
84
+ patch(at, target = this.pos()) { const rel = target - (at + 4); this.code[at] = rel & 0xff; this.code[at + 1] = (rel >> 8) & 0xff; this.code[at + 2] = (rel >> 16) & 0xff; this.code[at + 3] = (rel >>> 24) & 0xff; }
85
+ jumpTo(op, target) { this.emit(op); const at = this.pos(); this.i32(0); this.patch(at, target); }
86
+ k(v) { return this.consts.add(v); }
87
+ pushConst(v) { this.emit(OP.PUSH_CONST); this.u16(this.k(v)); }
88
+
89
+ // ---- expressions
90
+
91
+ E(ir) {
92
+ switch (ir.k) {
93
+ case 'undef': this.emit(OP.PUSH_UNDEF); return;
94
+ case 'null': this.emit(OP.PUSH_NULL); return;
95
+ case 'bool': this.pushConst(!!ir.v); return;
96
+ case 'num': this.pushConst(ir.v); return;
97
+ case 'str': this.pushConst(String(ir.v)); return;
98
+ case 'raw': return this.raw(ir.code);
99
+ case 'var': this.emit(OP.LOAD); this.u16(this.slot(ir.name)); return;
100
+ case 'template': {
101
+ for (const p of ir.parts) { if (p.s !== undefined) this.pushConst(p.s); else this.E(p.e); }
102
+ this.emit(OP.TEMPLATE, Math.min(255, ir.parts.length));
103
+ if (ir.parts.length > 255) throw new BytecodeError('template too long');
104
+ return;
105
+ }
106
+ case 'obj': {
107
+ this.emit(OP.NEW_OBJ);
108
+ for (const p of ir.props) {
109
+ if (p.spread) { this.E(p.spread); this.emit(OP.OBJ_SPREAD); continue; }
110
+ if (typeof p.key === 'string') { this.E(p.value); this.emit(OP.OBJ_SET_K); this.u16(this.k(p.key)); }
111
+ else { this.E(p.key.e); this.E(p.value); this.emit(OP.OBJ_SET); }
112
+ }
113
+ return;
114
+ }
115
+ case 'arr': {
116
+ this.emit(OP.NEW_ARR);
117
+ for (const i of ir.items) {
118
+ if (i.spread) { this.E(i.spread); this.emit(OP.ARR_SPREAD); }
119
+ else { if (i.hole) this.emit(OP.PUSH_UNDEF); else this.E(i.e); this.emit(OP.ARR_PUSH); }
120
+ }
121
+ return;
122
+ }
123
+ case 'get': this.E(ir.obj); this.emit(OP.GET_K); this.u16(this.k(ir.key)); return;
124
+ case 'getc': this.E(ir.obj); this.E(ir.key); this.emit(OP.GET); return;
125
+ case 'chain': {
126
+ const ctx = { kind: 'chain', name: ir.label, breaks: [] };
127
+ this.labels.push(ctx);
128
+ this.E(ir.e);
129
+ this.labels.pop();
130
+ for (const at of ctx.breaks) this.patch(at);
131
+ return;
132
+ }
133
+ case 'optcheck': {
134
+ this.E(ir.e);
135
+ const ctx = [...this.labels].reverse().find((l) => l.kind === 'chain' && l.name === ir.label);
136
+ if (!ctx) throw new BytecodeError(`optcheck outside its chain ${ir.label}`);
137
+ ctx.breaks.push(this.jmp(OP.JNULLISH_UNDEF));
138
+ return;
139
+ }
140
+ case 'bin': this.E(ir.l); this.E(ir.r); this.emit(OP.BIN, BIN[ir.op] ?? this.bad(`binary ${ir.op}`)); return;
141
+ case 'cmp': this.E(ir.l); this.E(ir.r); this.emit(OP.CMP, CMP[ir.op] ?? this.bad(`compare ${ir.op}`)); return;
142
+ case 'logical': {
143
+ this.E(ir.l);
144
+ const at = this.jmp(ir.op === 'and' ? OP.JF_KEEP : ir.op === 'or' ? OP.JT_KEEP : OP.JNN_KEEP);
145
+ this.E(ir.r);
146
+ this.patch(at);
147
+ return;
148
+ }
149
+ case 'cond': {
150
+ this.C(ir.test);
151
+ const toElse = this.jmp(OP.JF);
152
+ this.E(ir.then);
153
+ const toEnd = this.jmp(OP.JMP);
154
+ this.patch(toElse);
155
+ this.E(ir.else);
156
+ this.patch(toEnd);
157
+ return;
158
+ }
159
+ case 'unary': this.E(ir.e); this.emit(OP.UNARY, UNARY[ir.op] ?? this.bad(`unary ${ir.op}`)); return;
160
+ case 'in': this.E(ir.key); this.E(ir.obj); this.emit(OP.IN); return;
161
+ case 'delete': {
162
+ if (ir.lv.k !== 'lv-member') { this.pushConst(true); return; }
163
+ const path = this.lvPath(ir.lv);
164
+ if (!path) { this.E(ir.lv.obj.e ?? { k: 'undef' }); this.emit(OP.POP); this.pushConst(true); return; }
165
+ const keys = this.hoistKeys(path.keys);
166
+ this.pushKeys(keys);
167
+ this.emit(OP.DELETE_PATH); this.u16(this.slot(path.root)); this.emit(keys.length);
168
+ return;
169
+ }
170
+ case 'assign': return this.assign(ir);
171
+ case 'update': return this.update(ir);
172
+ case 'seq': ir.exprs.forEach((e, i) => { this.E(e); if (i < ir.exprs.length - 1) this.emit(OP.POP); }); return;
173
+ case 'callm': return this.callm(ir);
174
+ case 'host': {
175
+ const id = hostIds[ir.fn];
176
+ if (id === undefined) this.bad(`host ${ir.fn}`);
177
+ for (const a of ir.args) this.E(a);
178
+ this.emit(OP.HOST, id, ir.args.length, (ir.q ? HOST_Q : 0) | (ir.void ? HOST_VOID : 0));
179
+ return;
180
+ }
181
+ case 'pre': for (const s of ir.stmts) this.S(s); this.E(ir.e); return;
182
+ case 'math': for (const a of ir.args) this.E(a); this.emit(OP.MATH); this.u16(this.k(ir.name)); this.emit(ir.args.length); return;
183
+ case 'global': for (const a of ir.args) this.E(a); this.emit(OP.GLOBAL); this.u16(this.k(ir.name)); this.emit(ir.args.length); return;
184
+ case 'jsonparse': this.E(ir.e); this.emit(OP.JSON_PARSE); return;
185
+ case 'jsonstringify': this.E(ir.e); this.emit(OP.JSON_STRINGIFY); return;
186
+ case 'keys': this.E(ir.e); this.emit(OP.KEYS); return;
187
+ case 'values': this.E(ir.e); this.emit(OP.VALUES); return;
188
+ case 'entries': this.E(ir.e); this.emit(OP.ENTRIES); return;
189
+ case 'isarray': this.E(ir.e); this.emit(OP.ISARRAY); return;
190
+ case 'newerror': for (const a of ir.args) this.E(a); this.emit(OP.NEW_ERROR); this.u16(this.k(ir.name)); this.emit(ir.args.length); return;
191
+ case 'log': for (const a of ir.args) this.E(a); this.emit(OP.LOG, ir.args.length); return;
192
+ case 'helper': {
193
+ const id = helperIds[ir.name];
194
+ if (id === undefined) this.bad(`helper ${ir.name}`);
195
+ for (const a of ir.args) this.E(a);
196
+ this.emit(OP.HELPER, id, ir.args.length);
197
+ return;
198
+ }
199
+ case 'resp': this.E(ir.body); this.E(ir.init); this.emit(OP.RESP); this.u16(this.k(ir.kind)); return;
200
+ case 'params':
201
+ if (ir.fn === 'query') { this.emit(OP.PARAMS, 0); return; }
202
+ if (ir.fn === 'params') { this.emit(OP.PARAMS, 1); return; }
203
+ this.emit(OP.PARAM); this.u16(this.k(ir.name)); this.emit(ir.catchAll ? 1 : 0); return;
204
+ case 'callh': {
205
+ for (const a of ir.args) this.E(a);
206
+ if (this.inlines.has(ir.name)) { this.emit(OP.CALL_INLINE); this.u32(this.inlines.get(ir.name)); this.emit(ir.args.length); return; }
207
+ const fi = this.mod.fnIndex.get(ir.name);
208
+ if (fi === undefined) this.bad(`call of unknown function ${ir.name}`);
209
+ this.emit(OP.CALL_FN); this.u16(fi); this.emit(ir.args.length);
210
+ return;
211
+ }
212
+ case 'constref': {
213
+ const fi = this.mod.fnIndex.get(ir.name);
214
+ if (fi === undefined) this.bad(`unknown const ${ir.name}`);
215
+ this.emit(OP.CALL_FN); this.u16(fi); this.emit(0);
216
+ return;
217
+ }
218
+ case 'iife': {
219
+ for (const a of ir.args) this.E(a);
220
+ const off = this.region(ir.fn);
221
+ this.emit(OP.CALL_INLINE); this.u32(off); this.emit(ir.args.length);
222
+ return;
223
+ }
224
+ default: this.bad(`IR expression ${ir.k}`);
225
+ }
226
+ }
227
+
228
+ bad(what) { throw new BytecodeError(`bytecode: unsupported ${what}`); }
229
+
230
+ /** The handful of literal Rust snippets the lowering emits. */
231
+ raw(code) {
232
+ let m;
233
+ if ((m = code.match(/^__args\.get\((\d+)\)\.cloned\(\)\.unwrap_or\(Val::Undef\)$/))) { this.emit(OP.LOAD_ARG, Number(m[1])); return; }
234
+ if (code === 'Val::Arr(__args.to_vec())') { this.emit(OP.ARGS_FROM, 0); return; }
235
+ if (code === 'Val::Num(f64::NAN)') { this.pushConst(NaN); return; }
236
+ if (code === 'Val::Num(f64::INFINITY)') { this.pushConst(Infinity); return; }
237
+ if (code === 'Val::Num(f64::NEG_INFINITY)') { this.pushConst(-Infinity); return; }
238
+ this.bad(`raw Rust "${code}"`);
239
+ }
240
+
241
+ /** Emit an inline region (callback / closure / iife body) out of line; returns its offset. */
242
+ region(fn) {
243
+ const over = this.jmp(OP.JMP);
244
+ const off = this.pos();
245
+ const savedTries = this.tries; this.tries = [];
246
+ const savedLabels = this.labels; this.labels = [];
247
+ for (const s of fn.params) this.S(s);
248
+ for (const s of fn.body) this.S(s);
249
+ this.emit(OP.PUSH_UNDEF, OP.RET);
250
+ this.tries = savedTries; this.labels = savedLabels;
251
+ this.patch(over);
252
+ return off;
253
+ }
254
+
255
+ // ---- conditions
256
+
257
+ C(c) {
258
+ switch (c.k) {
259
+ case 'c-truthy': this.E(c.e); return;
260
+ case 'c-not': this.C(c.c); this.emit(OP.UNARY, UNARY.not); return;
261
+ case 'c-and': { this.C(c.l); const at = this.jmp(OP.JF_KEEP); this.C(c.r); this.patch(at); return; }
262
+ case 'c-or': { this.C(c.l); const at = this.jmp(OP.JT_KEEP); this.C(c.r); this.patch(at); return; }
263
+ case 'c-cmp': this.E(c.l); this.E(c.r); this.emit(OP.CMP, CMP[c.op] ?? this.bad(`compare ${c.op}`)); return;
264
+ case 'c-nullish': this.E(c.e); this.emit(OP.NULLISH); return;
265
+ case 'c-undef': this.E(c.e); this.emit(OP.PUSH_UNDEF); this.emit(OP.CMP, CMP.strict_eq); return;
266
+ default: this.bad(`condition ${c.k}`);
267
+ }
268
+ }
269
+
270
+ // ---- lvalues
271
+
272
+ /** Flatten an lvalue into { root: localName, keys: [...] } or null when the base is an expression. */
273
+ lvPath(lv) {
274
+ const keys = [];
275
+ let cur = lv;
276
+ while (cur.k === 'lv-member') { keys.unshift(cur.key); cur = cur.obj; }
277
+ if (cur.k === 'lv-var') return { root: cur.name, keys };
278
+ return null; // lv-expr base
279
+ }
280
+ /** Evaluate computed keys once, left to right, into temps. Returns descriptors. */
281
+ hoistKeys(keys) {
282
+ return keys.map((key) => {
283
+ if (typeof key === 'string') return { s: key };
284
+ const t = this.temp();
285
+ this.E(key); this.emit(OP.STORE); this.u16(t);
286
+ return { t };
287
+ });
288
+ }
289
+ pushKeys(keys) { for (const k of keys) { if (k.s !== undefined) this.pushConst(k.s); else { this.emit(OP.LOAD); this.u16(k.t); } } }
290
+ readPath(root, keys) {
291
+ this.emit(OP.LOAD); this.u16(this.slot(root));
292
+ for (const k of keys) { if (k.s !== undefined) { this.emit(OP.GET_K); this.u16(this.k(k.s)); } else { this.emit(OP.LOAD); this.u16(k.t); this.emit(OP.GET); } }
293
+ }
294
+ /** Read an lvalue whose base is an expression (lv-expr chain). */
295
+ readLvExpr(lv) {
296
+ if (lv.k === 'lv-var') { this.emit(OP.LOAD); this.u16(this.slot(lv.name)); return; }
297
+ if (lv.k === 'lv-expr') { this.E(lv.e); return; }
298
+ this.readLvExpr(lv.obj);
299
+ if (typeof lv.key === 'string') { this.emit(OP.GET_K); this.u16(this.k(lv.key)); } else { this.E(lv.key); this.emit(OP.GET); }
300
+ }
301
+
302
+ assign(ir) {
303
+ const v = this.temp();
304
+ const path = this.lvPath(ir.lv);
305
+ if (ir.lv.k === 'lv-var') {
306
+ if (ir.op) { this.emit(OP.LOAD); this.u16(this.slot(ir.lv.name)); this.E(ir.value); this.emit(OP.BIN, BIN[ir.op]); }
307
+ else this.E(ir.value);
308
+ this.emit(OP.DUP); this.emit(OP.STORE); this.u16(this.slot(ir.lv.name));
309
+ return;
310
+ }
311
+ if (path) {
312
+ const keys = this.hoistKeys(path.keys);
313
+ if (ir.op) { this.readPath(path.root, keys); this.E(ir.value); this.emit(OP.BIN, BIN[ir.op]); } else this.E(ir.value);
314
+ this.emit(OP.STORE); this.u16(v);
315
+ this.emit(OP.LOAD); this.u16(v);
316
+ this.pushKeys(keys);
317
+ this.emit(OP.STORE_PATH); this.u16(this.slot(path.root)); this.emit(keys.length);
318
+ this.emit(OP.LOAD); this.u16(v);
319
+ return;
320
+ }
321
+ // expression base: evaluate for effects, the write is lost (as in the Rust backend)
322
+ if (ir.lv.k === 'lv-expr') { this.E(ir.value); return; }
323
+ if (ir.op) { this.readLvExpr(ir.lv); this.E(ir.value); this.emit(OP.BIN, BIN[ir.op]); } else this.E(ir.value);
324
+ return;
325
+ }
326
+
327
+ update(ir) {
328
+ const path = this.lvPath(ir.lv);
329
+ const old = this.temp(), nw = this.temp();
330
+ const keys = path ? this.hoistKeys(path.keys) : [];
331
+ if (path) this.readPath(path.root, keys); else this.readLvExpr(ir.lv);
332
+ this.emit(OP.UNARY, UNARY.plus); this.emit(OP.STORE); this.u16(old);
333
+ this.emit(OP.LOAD); this.u16(old); this.pushConst(1); this.emit(OP.BIN, ir.delta > 0 ? BIN.add : BIN.sub); this.emit(OP.STORE); this.u16(nw);
334
+ if (path) { this.emit(OP.LOAD); this.u16(nw); this.pushKeys(keys); this.emit(OP.STORE_PATH); this.u16(this.slot(path.root)); this.emit(keys.length); }
335
+ this.emit(OP.LOAD); this.u16(ir.prefix ? nw : old);
336
+ }
337
+
338
+ callm(ir) {
339
+ const path = ir.lv ? this.lvPath(ir.lv) : null;
340
+ let keys = [];
341
+ if (path) { keys = this.hoistKeys(path.keys); this.readPath(path.root, keys); }
342
+ else if (ir.lv) this.readLvExpr(ir.lv);
343
+ else this.E(ir.recv);
344
+ for (const a of ir.args) this.E(a);
345
+ let flags = 0;
346
+ let cbOff = null;
347
+ if (ir.cb) {
348
+ if (ir.cb.kind === 'fn') { flags |= CALLM_CB; cbOff = this.region(ir.cb.fn); }
349
+ else if (ir.cb.kind === 'ref') {
350
+ flags |= CALLM_CB;
351
+ if (this.inlines.has(ir.cb.name)) cbOff = this.inlines.get(ir.cb.name);
352
+ else {
353
+ const fi = this.mod.fnIndex.get(ir.cb.name);
354
+ if (fi === undefined) this.bad(`callback ${ir.cb.name}`);
355
+ // wrap the module function in a tiny inline trampoline: args -> CALL_FN
356
+ const over = this.jmp(OP.JMP);
357
+ cbOff = this.pos();
358
+ this.emit(OP.ARGS_FROM, 0);
359
+ this.emit(OP.CALL_FN_SPREAD ?? OP.NOP); // placeholder, replaced below
360
+ this.code.length -= 2;
361
+ // simple: pass up to 3 positional args
362
+ this.code.length -= 0;
363
+ this.emit(OP.LOAD_ARG, 0, OP.LOAD_ARG, 1, OP.LOAD_ARG, 2, OP.CALL_FN); this.u16(fi); this.emit(3, OP.RET);
364
+ this.patch(over);
365
+ }
366
+ } else flags |= CALLM_DEFAULT_SORT;
367
+ }
368
+ if (path) flags |= CALLM_MUT;
369
+ this.emit(OP.CALLM); this.u16(this.k(ir.name)); this.emit(ir.args.length, flags);
370
+ if (flags & CALLM_CB) this.u32(cbOff);
371
+ if (path) { this.pushKeys(keys); this.emit(OP.STORE_PATH); this.u16(this.slot(path.root)); this.emit(keys.length); }
372
+ }
373
+
374
+ // ---- statements
375
+
376
+ S(s) {
377
+ switch (s.k) {
378
+ case 'let': if (s.init) this.E(s.init); else this.emit(OP.PUSH_UNDEF); this.emit(OP.STORE); this.u16(this.slot(s.name)); return;
379
+ case 'hoistvar': this.emit(OP.PUSH_UNDEF); this.emit(OP.STORE); this.u16(this.slot(s.name)); return;
380
+ case 'expr': this.E(s.e); this.emit(OP.POP); return;
381
+ case 'block': for (const x of s.body) this.S(x); return;
382
+ case 'if': {
383
+ this.C(s.test);
384
+ const toElse = this.jmp(OP.JF);
385
+ for (const x of s.then) this.S(x);
386
+ if (s.else) { const toEnd = this.jmp(OP.JMP); this.patch(toElse); for (const x of s.else) this.S(x); this.patch(toEnd); }
387
+ else this.patch(toElse);
388
+ return;
389
+ }
390
+ case 'while': {
391
+ const ctx = this.loop(s.label);
392
+ const top = this.pos(); ctx.continueTarget = top;
393
+ this.C(s.test); const exit = this.jmp(OP.JF);
394
+ for (const x of s.body) this.S(x);
395
+ this.jumpTo(OP.JMP, top);
396
+ this.patch(exit); this.endLoop(ctx);
397
+ return;
398
+ }
399
+ case 'dowhile': {
400
+ const ctx = this.loop(s.label);
401
+ const top = this.pos();
402
+ for (const x of s.body) this.S(x);
403
+ ctx.continueTarget = this.pos(); this.patchContinues(ctx);
404
+ this.C(s.test); this.jumpTo(OP.JT, top);
405
+ this.endLoop(ctx);
406
+ return;
407
+ }
408
+ case 'for': {
409
+ for (const x of s.init) this.S(x);
410
+ const ctx = this.loop(s.label);
411
+ const top = this.pos();
412
+ let exit = null;
413
+ if (s.test) { this.C(s.test); exit = this.jmp(OP.JF); }
414
+ for (const x of s.body) this.S(x);
415
+ ctx.continueTarget = this.pos(); this.patchContinues(ctx);
416
+ for (const x of s.update) this.S(x);
417
+ this.jumpTo(OP.JMP, top);
418
+ if (exit !== null) this.patch(exit);
419
+ this.endLoop(ctx);
420
+ return;
421
+ }
422
+ case 'forof':
423
+ case 'forin': {
424
+ const arr = this.temp(), idx = this.temp();
425
+ this.E(s.iter);
426
+ if (s.k === 'forin') this.emit(OP.KEYS);
427
+ this.emit(OP.ITER_INIT); this.u16(arr); this.u16(idx);
428
+ const ctx = this.loop(s.label);
429
+ const top = this.pos(); ctx.continueTarget = top;
430
+ this.emit(OP.ITER_NEXT); this.u16(arr); this.u16(idx); this.u16(this.slot(s.tmp)); const exit = this.pos(); this.i32(0);
431
+ for (const x of s.body) this.S(x);
432
+ this.jumpTo(OP.JMP, top);
433
+ this.patch(exit); this.endLoop(ctx);
434
+ return;
435
+ }
436
+ case 'break': { const ctx = this.findLabel(s.label); this.unwindTo(ctx.tryDepth); ctx.breaks.push(this.jmp(OP.JMP)); return; }
437
+ case 'continue': {
438
+ const ctx = this.findLabel(s.label); this.unwindTo(ctx.tryDepth);
439
+ if (ctx.continueTarget != null) this.jumpTo(OP.JMP, ctx.continueTarget); else ctx.continues.push(this.jmp(OP.JMP));
440
+ return;
441
+ }
442
+ case 'return': return this.ret(s);
443
+ case 'throw': {
444
+ this.E(s.e);
445
+ // a throw from a catch handler still runs that try's finalizer
446
+ for (let i = this.tries.length - 1; i >= 0; i--) { const t = this.tries[i]; if (t.phase === 'handler' && t.finalizer) this.finalizerCopy(t); }
447
+ this.emit(OP.THROW);
448
+ return;
449
+ }
450
+ case 'try': return this.tryStmt(s);
451
+ case 'switch': {
452
+ const d = this.temp();
453
+ this.E(s.disc); this.emit(OP.STORE); this.u16(d);
454
+ const ctx = this.loop(s.label); ctx.continueTarget = null; ctx.isSwitch = true;
455
+ const jumps = [];
456
+ let defaultIdx = -1;
457
+ s.cases.forEach((c, i) => {
458
+ if (c.test === null) { defaultIdx = i; return; }
459
+ this.emit(OP.LOAD); this.u16(d); this.E(c.test); this.emit(OP.CMP, CMP.strict_eq);
460
+ jumps.push([i, this.jmp(OP.JT)]);
461
+ });
462
+ const toDefault = this.jmp(OP.JMP);
463
+ const bodyStarts = [];
464
+ s.cases.forEach((c, i) => { bodyStarts[i] = this.pos(); for (const x of c.body) this.S(x); });
465
+ for (const [i, at] of jumps) this.patch(at, bodyStarts[i]);
466
+ this.patch(toDefault, defaultIdx >= 0 ? bodyStarts[defaultIdx] : this.pos());
467
+ this.endLoop(ctx);
468
+ return;
469
+ }
470
+ case 'letfn': {
471
+ // captures are shared slots (the region reads the live locals)
472
+ const off = this.region(s.fn);
473
+ this.inlines.set(s.name, off);
474
+ return;
475
+ }
476
+ default: this.bad(`IR statement ${s.k}`);
477
+ }
478
+ }
479
+
480
+ loop(label) { const ctx = { kind: 'loop', name: label, breaks: [], continues: [], continueTarget: null, tryDepth: this.tries.length }; this.labels.push(ctx); return ctx; }
481
+ patchContinues(ctx) { for (const at of ctx.continues) this.patch(at, ctx.continueTarget); ctx.continues = []; }
482
+ endLoop(ctx) { this.labels.pop(); for (const at of ctx.breaks) this.patch(at); if (ctx.continues.length) { if (ctx.continueTarget == null) this.bad('continue in switch'); this.patchContinues(ctx); } }
483
+ findLabel(name) {
484
+ for (let i = this.labels.length - 1; i >= 0; i--) { const l = this.labels[i]; if (l.kind === 'loop' && (!name || l.name === name)) return l; }
485
+ throw new BytecodeError(`bytecode: no loop for ${name || 'break/continue'}`);
486
+ }
487
+ /** Leaving `try` blocks on the way out of a loop/function: pop handlers, run finalizers. */
488
+ unwindTo(depth) {
489
+ for (let i = this.tries.length - 1; i >= depth; i--) {
490
+ const t = this.tries[i];
491
+ if (t.phase === 'body') this.emit(OP.TRY_POP);
492
+ if (t.phase !== 'final' && t.finalizer) this.finalizerCopy(t);
493
+ }
494
+ }
495
+ finalizerCopy(t) {
496
+ const saved = t.phase; t.phase = 'final';
497
+ for (const x of t.finalizer) this.S(x);
498
+ t.phase = saved;
499
+ }
500
+
501
+ ret(s) {
502
+ // value first (may itself throw inside the try), then unwind, then return
503
+ if (s.mode === 'node') { if (s.value) { this.E(s.value); this.emit(OP.POP); } this.unwindTo(0); this.emit(OP.PUSH_UNDEF, OP.RET); return; }
504
+ if (s.mode === 'web') {
505
+ if (!s.value) { this.unwindTo(0); this.emit(OP.PUSH_UNDEF, OP.RET); return; }
506
+ const t = this.temp();
507
+ this.E(s.value); this.emit(OP.STORE); this.u16(t);
508
+ this.unwindTo(0);
509
+ this.emit(OP.LOAD); this.u16(t); this.emit(OP.SEND, OP.PUSH_UNDEF, OP.RET);
510
+ return;
511
+ }
512
+ const t = this.temp();
513
+ if (s.value) this.E(s.value); else this.emit(OP.PUSH_UNDEF);
514
+ this.emit(OP.STORE); this.u16(t);
515
+ this.unwindTo(0);
516
+ this.emit(OP.LOAD); this.u16(t); this.emit(OP.RET);
517
+ }
518
+
519
+ tryStmt(s) {
520
+ const t = { finalizer: s.finalizer || null, phase: 'body' };
521
+ this.tries.push(t);
522
+ const toHandler = this.jmp(OP.TRY_PUSH);
523
+ for (const x of s.body) this.S(x);
524
+ this.emit(OP.TRY_POP);
525
+ t.phase = 'final';
526
+ if (s.finalizer) this.finalizerCopy(t);
527
+ const toEnd = this.jmp(OP.JMP);
528
+ this.patch(toHandler);
529
+ // exception value is on the stack
530
+ if (s.handler) {
531
+ t.phase = 'handler';
532
+ if (s.param) { this.emit(OP.CAUGHT); this.emit(OP.STORE); this.u16(this.slot(s.param)); } else this.emit(OP.POP);
533
+ for (const x of s.handler) this.S(x);
534
+ t.phase = 'final';
535
+ if (s.finalizer) this.finalizerCopy(t);
536
+ } else {
537
+ const e = this.temp();
538
+ this.emit(OP.STORE); this.u16(e);
539
+ t.phase = 'final';
540
+ if (s.finalizer) this.finalizerCopy(t);
541
+ this.emit(OP.LOAD); this.u16(e); this.emit(OP.THROW);
542
+ }
543
+ this.patch(toEnd);
544
+ this.tries.pop();
545
+ }
546
+
547
+ finish() { return { nlocals: this.nlocals, code: Buffer.from(this.code) }; }
548
+ }
549
+
550
+ /**
551
+ * Emit a module from lowered routes.
552
+ * @param {{ routes: Array<{ir: object}>, env: [string,string][] }} o routes as produced by lowerRoute (the `ir` field)
553
+ * @returns {Buffer}
554
+ */
555
+ export function emitModule({ routes, env = [] }) {
556
+ const consts = new Consts();
557
+ const mod = { fnIndex: new Map(), fns: [] };
558
+ const routeDefs = [];
559
+ // Pass 1: assign function indexes (helpers + consts + route fns) so calls can be forward.
560
+ const plan = [];
561
+ for (const r of routes) {
562
+ const ir = r.ir;
563
+ for (const h of ir.helpers) { mod.fnIndex.set(h.name, mod.fns.length); mod.fns.push(null); plan.push({ kind: 'helper', fn: h.fn }); }
564
+ for (const c of ir.consts) { mod.fnIndex.set(c.name, mod.fns.length); mod.fns.push(null); plan.push({ kind: 'const', e: c.e }); }
565
+ const def = { style: ir.style === 'node' ? 0 : 1, params: (ir.params || []).map((p) => [consts.add(p.name), p.catchAll ? 1 : 0]), nodeFn: 0, methods: [] };
566
+ if (ir.style === 'node') { def.nodeFn = mod.fns.length; mod.fns.push(null); plan.push({ kind: 'route', fn: ir.node }); }
567
+ else for (const m of Object.keys(ir.methods)) { if (!(m in METHOD_INDEX)) continue; def.methods.push([METHOD_INDEX[m], mod.fns.length]); mod.fns.push(null); plan.push({ kind: 'route', fn: ir.methods[m] }); }
568
+ routeDefs.push(def);
569
+ }
570
+ // Pass 2: emit.
571
+ plan.forEach((p, i) => {
572
+ const f = new Fn(consts, mod);
573
+ if (p.kind === 'const') { f.E(p.e); f.emit(OP.RET); }
574
+ else { for (const s of p.fn.params) f.S(s); for (const s of p.fn.body) f.S(s); f.emit(OP.PUSH_UNDEF, OP.RET); }
575
+ mod.fns[i] = f.finish();
576
+ });
577
+ const envPairs = env.map(([k, v]) => [consts.add(String(k)), consts.add(String(v))]);
578
+ const parts = [MAGIC, Buffer.from([VERSION]), consts.encode()];
579
+ parts.push(u16(envPairs.length));
580
+ for (const [k, v] of envPairs) parts.push(u16(k), u16(v));
581
+ parts.push(u16(mod.fns.length));
582
+ for (const fn of mod.fns) parts.push(u16(fn.nlocals), u32(fn.code.length), fn.code);
583
+ parts.push(Buffer.from([routeDefs.length]));
584
+ for (const d of routeDefs) {
585
+ parts.push(Buffer.from([d.style, d.params.length]));
586
+ for (const [n, ca] of d.params) parts.push(u16(n), Buffer.from([ca]));
587
+ if (d.style === 0) parts.push(u16(d.nodeFn));
588
+ else { parts.push(Buffer.from([d.methods.length])); for (const [m, fi] of d.methods) parts.push(Buffer.from([m]), u16(fi)); }
589
+ }
590
+ return Buffer.concat(parts);
591
+ }
@@ -0,0 +1,11 @@
1
+ // Host-call and helper ids shared with runtime/zoo-host/src/vm.rs and helpers.rs.
2
+ export const hostIds = {
3
+ req_method: 0, req_path: 1, req_url: 2, req_full_url: 3, req_query: 4, req_query_get: 5, req_headers: 6, req_header: 7,
4
+ req_text: 8, req_body: 9, req_json: 10, res_status: 11, res_header: 12, res_json: 13, res_send: 14, res_end: 15,
5
+ res_redirect: 16, env: 17, env_obj: 18, now_ms: 19, now_iso: 20, kv_get: 21, kv_exists: 22, kv_set: 23, kv_incrby: 24,
6
+ kv_del: 25, slot: 26, payer_address: 27, program_address: 28,
7
+ };
8
+ export const helperIds = {
9
+ cookies: 0, cookie_get: 1, cookie_has: 2, cookie_all: 3, header_has: 4, query_has: 5, query_get_all: 6, search: 7,
10
+ assign: 8, omit: 9, from_entries: 10, array_from: 11, slice_from: 12, url: 13, num: 14, set_headers: 15,
11
+ };
@@ -8,10 +8,12 @@ import { Ineligible, functionMetaReason, deploymentWarnings, displayFile } from
8
8
  import { readModule } from './parse.js';
9
9
  import { lowerRoute } from './ir.js';
10
10
  import { emitRoute, emitCrate, RustPrinter, DEFAULT_RUNTIME_PATH, sanitizeCrateName } from './rust.js';
11
+ import { emitModule } from './bytecode.js';
11
12
 
12
13
  export { readModule, parseModule, stripTypes } from './parse.js';
13
14
  export { lowerRoute, IR } from './ir.js';
14
15
  export { emitRoute, emitCrate, RustPrinter, DEFAULT_RUNTIME_PATH, sanitizeCrateName, rustStr } from './rust.js';
16
+ export { emitModule, OP } from './bytecode.js';
15
17
  export { Ineligible } from '../eligibility.js';
16
18
 
17
19
  export const MANIFEST_VERSION = 1;
@@ -65,6 +67,7 @@ export function transmute(deployment, opts = {}) {
65
67
  const report = { eligible: [], ineligible: [], warnings: deploymentWarnings(deployment) };
66
68
  const routes = [];
67
69
  const routeSources = [];
70
+ const loweredIrs = [];
68
71
  const mergedEnv = {};
69
72
  const referencedEnv = new Set();
70
73
  let envDynamic = false;
@@ -92,6 +95,7 @@ export function transmute(deployment, opts = {}) {
92
95
  continue;
93
96
  }
94
97
  routeSources.push(c.rust);
98
+ loweredIrs.push(c.ir);
95
99
  for (const k of c.env) referencedEnv.add(k);
96
100
  if (c.envDynamic) envDynamic = true;
97
101
  Object.assign(mergedEnv, fn.environment || {});
@@ -118,8 +122,16 @@ export function transmute(deployment, opts = {}) {
118
122
  `${deployment.source || 'deployment'}${deployment.framework ? ` (${deployment.framework})` : ''} — ${routes.length} route(s), ${(deployment.staticFiles || []).length} static file(s)`,
119
123
  ...routes.map((r) => `route ${r.index}: ${r.routePath}${r.methods ? ' [' + r.methods.join(',') + ']' : ''} ← ${r.name}`),
120
124
  ].join('\n');
121
- const crateFiles = emitCrate({ name, runtimePath, env, routes: routeSources, routeCount: routes.length, header });
122
- const crate = { 'Cargo.toml': crateFiles['Cargo.toml'], 'src/lib.rs': crateFiles['src/lib.rs'] };
125
+ const target = opts.target === 'shared' ? 'shared' : 'program';
126
+ let crate = null;
127
+ let code = null;
128
+ if (target === 'shared') {
129
+ // The shared runtime: a bytecode module (a few KB) instead of a crate.
130
+ code = emitModule({ routes: loweredIrs.map((ir) => ({ ir })), env });
131
+ } else {
132
+ const crateFiles = emitCrate({ name, runtimePath, env, routes: routeSources, routeCount: routes.length, header });
133
+ crate = { 'Cargo.toml': crateFiles['Cargo.toml'], 'src/lib.rs': crateFiles['src/lib.rs'] };
134
+ }
123
135
 
124
136
  const manifest = {
125
137
  version: MANIFEST_VERSION,
@@ -128,8 +140,9 @@ export function transmute(deployment, opts = {}) {
128
140
  static: (deployment.staticFiles || []).map((f) => ({ path: f.path, contentType: f.contentType, size: f.size })),
129
141
  env: envNames,
130
142
  config: deployment.config,
143
+ target,
131
144
  };
132
- return { crate, manifest, report, crateName: crateFiles.crateName };
145
+ return { crate, code, manifest, report, crateName: crate ? name : null, target };
133
146
  }
134
147
 
135
148
  /** Write a transmuted crate to `dir` (creates src/). Returns the paths written. */