bmweb-cli 0.1.0

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.
Files changed (72) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +351 -0
  3. package/dist/bmweb.js +2790 -0
  4. package/package.json +53 -0
  5. package/runtime/core/bestvm/codec.js +285 -0
  6. package/runtime/core/bestvm/environment.js +116 -0
  7. package/runtime/core/bestvm/executor.js +1483 -0
  8. package/runtime/core/bestvm/index.js +52 -0
  9. package/runtime/core/bestvm/machine.js +491 -0
  10. package/runtime/core/bestvm/operands.js +356 -0
  11. package/runtime/core/bestvm/registers.js +152 -0
  12. package/runtime/core/bestvm/write-guard.js +111 -0
  13. package/runtime/core/ipofile/compile.js +364 -0
  14. package/runtime/core/ipofile/decls.js +187 -0
  15. package/runtime/core/ipofile/emit.js +708 -0
  16. package/runtime/core/ipofile/exec.js +164 -0
  17. package/runtime/core/ipofile/lex.js +243 -0
  18. package/runtime/core/ipofile/parse.js +550 -0
  19. package/runtime/core/ipofile/pool.js +404 -0
  20. package/runtime/core/ipofile/walk.js +431 -0
  21. package/runtime/core/ipovm/builtin-helpers.js +182 -0
  22. package/runtime/core/ipovm/builtins-api.js +610 -0
  23. package/runtime/core/ipovm/builtins-screen.js +493 -0
  24. package/runtime/core/ipovm/builtins-table.js +166 -0
  25. package/runtime/core/ipovm/builtins-text.js +166 -0
  26. package/runtime/core/ipovm/emissions.js +138 -0
  27. package/runtime/core/ipovm/hosts.js +191 -0
  28. package/runtime/core/ipovm/operators.js +229 -0
  29. package/runtime/core/ipovm/structures.js +250 -0
  30. package/runtime/core/ipovm/suspensions.js +241 -0
  31. package/runtime/core/ipovm/tape.js +206 -0
  32. package/runtime/core/ipovm/values.js +241 -0
  33. package/runtime/core/ipovm/vm.js +1166 -0
  34. package/runtime/core/translate.js +526 -0
  35. package/runtime/core/webshim/api-router.js +592 -0
  36. package/runtime/core/webshim/bus.js +95 -0
  37. package/runtime/core/webshim/coding.js +82 -0
  38. package/runtime/core/webshim/data-fetch.js +66 -0
  39. package/runtime/core/webshim/exchange.js +288 -0
  40. package/runtime/core/webshim/framing.js +331 -0
  41. package/runtime/core/webshim/install.js +30 -0
  42. package/runtime/core/webshim/job-runner.js +319 -0
  43. package/runtime/core/webshim/native-bus.js +108 -0
  44. package/runtime/core/webshim/timers.js +82 -0
  45. package/runtime/core/webshim/trace.js +205 -0
  46. package/runtime/core/webshim/transport-base.js +128 -0
  47. package/runtime/core/webshim/variant-resolver.js +249 -0
  48. package/runtime/core/webshim/web-serial-bus.js +734 -0
  49. package/runtime/home/bmweb-home.ips +76 -0
  50. package/runtime/home/bmweb.h +26 -0
  51. package/runtime/screens/activations.js +258 -0
  52. package/runtime/screens/garage/diff.js +331 -0
  53. package/runtime/screens/garage/share.js +276 -0
  54. package/runtime/screens/garage/store.js +547 -0
  55. package/runtime/screens/ipo-runtime/cells.js +176 -0
  56. package/runtime/screens/ipo-runtime/dialogs.js +254 -0
  57. package/runtime/screens/ipo-runtime/home.js +358 -0
  58. package/runtime/screens/ipo-runtime/open.js +393 -0
  59. package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
  60. package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
  61. package/runtime/screens/ipo-runtime/print.js +281 -0
  62. package/runtime/screens/ipo-runtime/program.js +1337 -0
  63. package/runtime/screens/ipo-runtime/protocol.js +464 -0
  64. package/runtime/screens/ipo-runtime/script-scan.js +225 -0
  65. package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
  66. package/runtime/screens/ipo-runtime/ui.js +249 -0
  67. package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
  68. package/runtime/screens/ir.js +324 -0
  69. package/runtime/screens/search/data.js +153 -0
  70. package/runtime/screens/search/match.js +285 -0
  71. package/runtime/screens/search/open.js +66 -0
  72. package/runtime/vendor/fflate.min.js +1 -0
@@ -0,0 +1,1166 @@
1
+ /**
2
+ * @file The .IPO executor. Two ways to run a proc: the OFFLINE executor
3
+ * (run / runItem) halts at a `state` yield -- enough to draw a screen or
4
+ * recover a job, byte-identical to ipo_vm.py; and the RESUMABLE driver
5
+ * (stepStart / stepStartItem / stepStartRange + resume) SUSPENDS instead,
6
+ * saving (toks, i, stack, frame) and returning a pending action, so the
7
+ * renderer can show the picker, confirm, send on the wire through the ONE
8
+ * audited safe path, and continue from the next token. Between suspensions
9
+ * it is the same synchronous machine, so headless parity is unaffected (the
10
+ * diff harness never resumes).
11
+ *
12
+ * The public API the app and the harnesses load is exported at the end of
13
+ * this file; it is the last piece of core/ipovm/ in load order.
14
+ */
15
+
16
+ /** The offline executor's default step budget: a runaway guard per VM. */
17
+ const IPO_DEFAULT_BUDGET = 200000;
18
+
19
+ /** How many reference hops _refTarget follows before giving up. */
20
+ /**
21
+ * Whether the argument at `i` belongs to a structure read call: the tokens
22
+ * after it up to the call are the remaining arguments.
23
+ * @param {IpoToken[]} toks - the proc's tokens
24
+ * @param {number} i - the argument token's index
25
+ * @returns {boolean}
26
+ */
27
+ function ipoArgFeedsStructRead(toks, i) {
28
+ for (let j = i + 1; j < toks.length; j++) {
29
+ const op = toks[j].op;
30
+ if (op === 'var' || op === 'const' || op === 'procref') continue;
31
+ return (
32
+ op === 'call' &&
33
+ typeof IPO_STRUCT_FNS !== 'undefined' &&
34
+ IPO_STRUCT_FNS.has(toks[j].name)
35
+ );
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /** the comparisons a never-written local takes its default in */
41
+ const IPO_CMP_OPS = new Set(['eq', 'ne', 'lt', 'gt', 'le', 'ge']);
42
+
43
+ /**
44
+ * What an unwritten local is worth beside `other` at run time: '' beside a
45
+ * string, 0 otherwise.
46
+ * @param {IpoValue} other - the operand on the other side
47
+ * @returns {IpoValue}
48
+ */
49
+ function ipoSlotDefault(other) {
50
+ return isPlainStr(other) || isBound(other) ? '' : 0;
51
+ }
52
+
53
+ const IPO_REF_MAX_HOPS = 8;
54
+
55
+ /** How far back of a Result* call _harvestReads looks for its key. */
56
+ const IPO_HARVEST_LOOKBACK = 10;
57
+
58
+ /**
59
+ * The saved state of a suspended run.
60
+ * @typedef {object} IpoSuspension
61
+ * @property {IpoToken[]} toks - the tape being run
62
+ * @property {Map<number, IpoValue>} frame - the current frame's slots
63
+ * @property {number} i - token index to continue at
64
+ * @property {IpoValue[]} stack - the operand stack
65
+ * @property {IpoCallRecord[]} callers - the driven call stack
66
+ * @property {Map<number, number>} index - byte offset -> token index
67
+ * @property {number} end - token index the run ends at
68
+ * @property {Map<number, number>} remap - segment-rebased jump targets
69
+ * @property {string|null} [pending] - the kind of action it is parked on
70
+ * @property {IpoValue[]|null} [pendingStack] - the parked call's arguments
71
+ */
72
+
73
+ /**
74
+ * A driven caller, saved while its callee runs.
75
+ * @typedef {object} IpoCallRecord
76
+ * @property {IpoToken[]} toks - the caller's tape
77
+ * @property {number} i - the calluser token's index
78
+ * @property {number} end - the caller's end
79
+ * @property {Map<number, number>} index - the caller's byte index
80
+ * @property {Map<number, number>} remap - the caller's segment remap
81
+ * @property {Map<number, IpoValue>} frame - the caller's frame
82
+ * @property {string|null} inKey - the key carried in through the arguments
83
+ * @property {IpoRef[]} outs - the out-refs the callee may leave unfilled
84
+ */
85
+
86
+ /**
87
+ * The picker's answer to a toggle/yield resume: a bare ORT string (component
88
+ * only) or {ort, ein}.
89
+ * @typedef {string|{ort?: string, ein?: number}} IpoPick
90
+ */
91
+
92
+ /** Executes {procs, byid, pool} from ipo_exec.py. byid keys are "type:id". */
93
+ class IpoVm {
94
+ /**
95
+ * @param {{procs?: Record<string, IpoToken[]>, byid?: Record<string, string>, pool?: *[]}} exec -
96
+ * the decoded script
97
+ * @param {object} [opts] - options
98
+ * @param {IpoHost} [opts.host] - where the car's answers come from (default OkHost)
99
+ * @param {boolean} [opts.wireJobs] - the driven executor suspends on EVERY
100
+ * INPAapiJob (and on wartezeit), not only writes -- the driver runs each
101
+ * on the real wire and feeds the results back. Never set by the headless
102
+ * diff harness, so offline parity is untouched.
103
+ * @param {(text: string) => void} [opts.onText] - live tap on every printed
104
+ * string (ftextout/messagebox) for the driver's running log -- emissions
105
+ * can be repainted away by the machine's own setscreen before the driver
106
+ * looks, the tap cannot.
107
+ * @param {number} [opts.budget] - step budget per run
108
+ */
109
+ constructor(exec, opts = {}) {
110
+ this.procs = exec.procs || {};
111
+ this.byidRaw = exec.byid || {};
112
+ this.pool = exec.pool || [];
113
+ /** @type {IpoHost} */
114
+ this.host = opts.host || new OkHost();
115
+ this.wireJobs = !!opts.wireJobs;
116
+ this.onText = typeof opts.onText === 'function' ? opts.onText : null;
117
+ this.budget = opts.budget || IPO_DEFAULT_BUDGET;
118
+
119
+ /** @type {Map<number|string, IpoValue>} slot n (or a bookkeeping key) -> value */
120
+ this.globals = new Map();
121
+ /** @type {Emissions} */
122
+ this.out = new Emissions();
123
+ this.steps = 0;
124
+ /** @type {Set<string>} state machines the offline setstate already ran */
125
+ this.entered = new Set();
126
+ /** @type {Map<string, string>} "sc:n" -> the result key bound to that slot */
127
+ this.binds = new Map();
128
+ /** @type {Map<string, string[]>} in-memory files by path */
129
+ this.files = new Map();
130
+ /** @type {{path: string, mode: string, line: number}|null} the open file */
131
+ this.fh = null;
132
+ /** @type {Map<number, Map<number, string>>} StrArray handle -> entries */
133
+ this.strArrays = new Map();
134
+ this._arrN = 0;
135
+ /** @type {Map<number, IpoValue>|null} the frame of the proc being executed */
136
+ this.frame = null;
137
+ /** @type {IpoItem|null} the item runItem is executing */
138
+ this._itemTarget = null;
139
+ /** @type {boolean} an input dialog ran: the next job's arg is not static */
140
+ this.inputFed = false;
141
+ /** @type {IpoSuspension|null} the parked driven run */
142
+ this._susp = null;
143
+ /** @type {string|null} the component the picker handed back, for togglelist */
144
+ this._pickInput = null;
145
+ /** @type {number|null} the on/off the picker handed back, for getinputstate */
146
+ this._driveInput = null;
147
+ /** @type {Map<number, IpoStructure>|null} live binary structures by handle */
148
+ this.structs = null;
149
+ /** @type {number|null} SetStructureMode's last setting */
150
+ this._structMode = null;
151
+ /** @type {Map<number, number>|null} settimer deadlines by timer number */
152
+ this.timers = null;
153
+ /** @type {object|null} the last wire job's fed results */
154
+ this._lastJobSets = null;
155
+
156
+ // highest global slot touched, so run_item can preseed presence flags
157
+ this._maxglobal = 0;
158
+ for (const toks of Object.values(this.procs)) {
159
+ for (const t of toks) {
160
+ if (
161
+ (t.op === 'var' || t.op === 'store') &&
162
+ (t.sc == null ? GLOBAL : t.sc) === GLOBAL &&
163
+ t.n > this._maxglobal
164
+ ) {
165
+ this._maxglobal = t.n;
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * The name of a proc by type and id.
173
+ * @param {string} type - func | menu | screen | state
174
+ * @param {number} id - the id the procref carries
175
+ * @returns {string|undefined}
176
+ */
177
+ byid(type, id) {
178
+ return this.byidRaw[`${type}:${id}`];
179
+ }
180
+
181
+ /**
182
+ * Resolve a procref on the stack to a proc name.
183
+ * @param {IpoValue} ref - the stack value
184
+ * @param {string} kindName - the proc type to look up
185
+ * @returns {string|null}
186
+ */
187
+ target(ref, kindName) {
188
+ if (isRef(ref)) return this.byid(kindName, ref[2]);
189
+ return null;
190
+ }
191
+
192
+ /**
193
+ * The result key bound to a slot.
194
+ * @param {number} sc - scope
195
+ * @param {number} n - slot
196
+ * @returns {string|undefined}
197
+ */
198
+ bindKey(sc, n) {
199
+ return this.binds.get(`${sc}:${n}`);
200
+ }
201
+
202
+ /**
203
+ * Bind a slot to a result key.
204
+ * @param {number} sc - scope
205
+ * @param {number} n - slot
206
+ * @param {string} key - result key
207
+ * @returns {void}
208
+ */
209
+ setBind(sc, n, key) {
210
+ this.binds.set(`${sc}:${n}`, key);
211
+ }
212
+
213
+ /**
214
+ * Forget a slot's binding.
215
+ * @param {number} sc - scope
216
+ * @param {number} n - slot
217
+ * @returns {void}
218
+ */
219
+ delBind(sc, n) {
220
+ this.binds.delete(`${sc}:${n}`);
221
+ }
222
+
223
+ // ------------------------------------------------------------- run --
224
+
225
+ /**
226
+ * Run a proc offline to its end or first yield.
227
+ * @param {string} proc - proc name
228
+ * @param {IpoValue[]} [args] - the frame's initial slots
229
+ * @returns {Emissions}
230
+ * @throws {IpoError} when the proc does not exist
231
+ */
232
+ run(proc, args) {
233
+ const toks = this.procs[proc];
234
+ if (!toks) throw new IpoError(`no proc ${proc}`);
235
+ const frame = new Map();
236
+ if (args) args.forEach((v, i) => frame.set(i, v));
237
+ try {
238
+ this._exec(toks, frame);
239
+ } catch (e) {
240
+ if (!(e instanceof Halt)) throw e;
241
+ }
242
+ return this.out;
243
+ }
244
+
245
+ /**
246
+ * Execute one menu item's body offline as if its F-key were pressed.
247
+ * start/end are TOKEN INDICES into the full menu token list (so the item's
248
+ * jumps resolve to real targets). Presets keypress/enable flags true.
249
+ * Mirrors run_item.
250
+ * @param {IpoToken[]} menuToks - the menu proc's tokens
251
+ * @param {number} start - first token of the body
252
+ * @param {number} end - token index the body ends at
253
+ * @param {IpoItem} item - the item to attribute emissions to
254
+ * @param {boolean} [presence] - preset every untouched global to true
255
+ * @returns {IpoItem}
256
+ */
257
+ runItem(menuToks, start, end, item, presence = true) {
258
+ if (presence) {
259
+ const guards = keypressGuards(menuToks, start, end);
260
+ for (let g = 0; g <= this._maxglobal; g++) {
261
+ if (!this.globals.has(g)) {
262
+ this.globals.set(g, true);
263
+ } else if (guards.has(g)) {
264
+ const cur = this.globals.get(g);
265
+ if (cur === 0 || cur === false || cur == null) {
266
+ this.globals.set(g, true);
267
+ }
268
+ }
269
+ }
270
+ }
271
+ this._itemTarget = item;
272
+ try {
273
+ this._exec(menuToks, new Map(), start, end);
274
+ } catch (e) {
275
+ if (!(e instanceof Halt)) throw e;
276
+ } finally {
277
+ this._itemTarget = null;
278
+ }
279
+ return item;
280
+ }
281
+
282
+ /**
283
+ * Execute a tape offline in the given frame, restoring the previous frame
284
+ * after.
285
+ * @param {IpoToken[]} toks - the tape
286
+ * @param {Map<number, IpoValue>} frame - the frame's slots
287
+ * @param {number} [startAt] - first token index
288
+ * @param {number|null} [stopAt] - token index to stop at (exclusive)
289
+ * @returns {void}
290
+ */
291
+ _exec(toks, frame, startAt = 0, stopAt = null) {
292
+ const prev = this.frame;
293
+ this.frame = frame;
294
+ try {
295
+ return this._execIn(toks, frame, startAt, stopAt);
296
+ } finally {
297
+ this.frame = prev;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * The offline executor's loop, byte-identical in behaviour to ipo_vm.py:
303
+ * it records predicate reads and harvests the reads of skipped branches
304
+ * (for the static lift), attributes a record index to the item, and HALTS
305
+ * at a `state` yield.
306
+ * @param {IpoToken[]} toks - the tape
307
+ * @param {Map<number, IpoValue>} frame - the frame's slots
308
+ * @param {number} [startAt] - first token index
309
+ * @param {number|null} [stopAt] - token index to stop at (exclusive)
310
+ * @returns {void}
311
+ * @throws {Halt} at a yield or when the step budget runs out
312
+ */
313
+ _execIn(toks, frame, startAt = 0, stopAt = null) {
314
+ const index = ipoByteIndex(toks);
315
+ const end = Math.min(
316
+ stopAt == null ? toks.length : stopAt,
317
+ ipoProcEnd(toks)
318
+ );
319
+
320
+ let stack = [];
321
+ let i = startAt;
322
+ let curItem = this._itemTarget;
323
+
324
+ while (i < end) {
325
+ this.steps += 1;
326
+ if (this.steps > this.budget) throw new Halt('step budget');
327
+ const t = toks[i];
328
+ const op = t.op;
329
+
330
+ if (op === 'frame') {
331
+ stack = [];
332
+ } else if (op === 'const') {
333
+ // A pool double keeps its float identity so analogout's int/float
334
+ // argument split matches Python (see mkFloat).
335
+ stack.push(t.t === 'd' ? mkFloat(t.v) : t.v);
336
+ } else if (op === 'var') {
337
+ let val = this._read(t, frame);
338
+ // see the driven path: a by-reference parameter handed on to a
339
+ // structure read stays a reference in a live VM (user functions
340
+ // called from a driven run execute here)
341
+ if (this.wireJobs && ipoArgFeedsStructRead(toks, i)) {
342
+ const raw = this._rawSlot(t, frame);
343
+ if (isRef(raw)) val = raw;
344
+ }
345
+ if (val == null) val = mkSlot(t.sc == null ? GLOBAL : t.sc, t.n);
346
+ stack.push(val);
347
+ } else if (op === 'procref') {
348
+ stack.push([
349
+ 'ref',
350
+ t.kind,
351
+ t.n,
352
+ t.kind === IPO_REF_LOCAL ? frame : null,
353
+ ]);
354
+ } else if (op === 'store') {
355
+ let val = stack.length ? stack.pop() : null;
356
+ const sc = t.sc == null ? GLOBAL : t.sc;
357
+ // A record index the item selects: `const <v>; store <global>`.
358
+ if (
359
+ curItem != null &&
360
+ sc === GLOBAL &&
361
+ !('_sel' in curItem) &&
362
+ (isPlainInt(val) || isPlainStr(val))
363
+ ) {
364
+ curItem._sel = [t.n, val];
365
+ }
366
+ val = this._bindPendingBinary(sc, t.n, val);
367
+ this._write(t, frame, val);
368
+ } else if (op === 'binop') {
369
+ // neg (0x6d) is UNARY: it takes the top value only. Popping two ate
370
+ // the operand underneath (`3, 43, 10.0 neg` negated the 43 and lost
371
+ // the 10.0), shifting every later argument of the call.
372
+ const b = stack.length ? stack.pop() : null;
373
+ const a = t.name === 'neg' ? null : stack.length ? stack.pop() : null;
374
+ if (IPO_COMPARE_OPS.includes(t.name)) {
375
+ for (const x of [a, b]) {
376
+ if (isBound(x) && x.key) this.out.predicateReads.add(x.key);
377
+ }
378
+ }
379
+ stack.push(binop(t.name, a, b));
380
+ } else if (op === 'jfalse') {
381
+ const cond = stack.length ? stack.pop() : false;
382
+ if (!truthy(cond)) {
383
+ const nxt = index.has(t.to) ? index.get(t.to) : end;
384
+ this._harvestReads(toks, i + 1, nxt);
385
+ if (nxt >= end) return;
386
+ i = nxt;
387
+ continue;
388
+ }
389
+ } else if (op === 'jump') {
390
+ const nxt = index.has(t.to) ? index.get(t.to) : end;
391
+ if (nxt >= end) return;
392
+ i = nxt;
393
+ continue;
394
+ } else if (op === 'ITEM') {
395
+ curItem = { nr: t.nr, label: t.label };
396
+ this.out.items.push(curItem);
397
+ this.inputFed = false;
398
+ stack = [];
399
+ } else if (op === 'LINE') {
400
+ this.out.lines.push({ label: t.label, elements: [] });
401
+ stack = [];
402
+ } else if (op === 'call') {
403
+ this._builtin(t, stack, curItem);
404
+ stack = [];
405
+ } else if (op === 'calluser') {
406
+ this._callUser(t, stack);
407
+ stack = [];
408
+ } else if (op === 'state') {
409
+ // A yield, not a loop: park at the named state and stop.
410
+ this.out.states.push(t.name);
411
+ throw new Halt(`yield at ${t.name}`);
412
+ } else if (op === 'ret') {
413
+ return;
414
+ }
415
+ // block/decl/stmt/unk/dllcall/endproc: no runtime effect here
416
+ i += 1;
417
+ }
418
+ }
419
+
420
+ /**
421
+ * A coding image stored for sliced display (the INPAapiResultBinary path):
422
+ * the first string stored after the read is bound to the pending key.
423
+ * @param {number} sc - scope of the store
424
+ * @param {number} n - slot of the store
425
+ * @param {IpoValue} val - the value being stored
426
+ * @returns {IpoValue} the value, boxed when it took the pending key
427
+ */
428
+ _bindPendingBinary(sc, n, val) {
429
+ const pend = this.globals.get('__pending_binary__');
430
+ if (pend && isPlainStr(val)) {
431
+ val = mkBound(mkSlot(sc, n), val, pend);
432
+ this.setBind(sc, n, pend);
433
+ this.globals.delete('__pending_binary__');
434
+ }
435
+ return val;
436
+ }
437
+
438
+ /**
439
+ * The key carried in through a call's arguments, and the out-refs the
440
+ * callee may leave unfilled: a conversion helper turns a keyed value into
441
+ * a display string through an out-ref, so the input key is carried onto an
442
+ * out-slot the callee left anonymous (mirrors ipo_vm.py calluser).
443
+ * @param {IpoValue[]} stack - the call's arguments
444
+ * @returns {{inKey: string|null, outs: IpoRef[], frame: Map<number, IpoValue>}}
445
+ */
446
+ _callArgs(stack) {
447
+ const inKey = keyed(stack);
448
+ const outs = stack.filter(isRef);
449
+ const frame = new Map();
450
+ stack.forEach((v, i) => frame.set(i, v));
451
+ return { inKey, outs, frame };
452
+ }
453
+
454
+ /**
455
+ * Bind the callee's unfilled out-refs to the key its arguments carried.
456
+ * @param {string|null} inKey - the carried key
457
+ * @param {IpoRef[]} outs - the call's out-refs
458
+ * @returns {void}
459
+ */
460
+ _carryKeyToOuts(inKey, outs) {
461
+ if (!inKey) return;
462
+ for (const ref of outs) {
463
+ const { dsc, n, map } = this._refTarget(ref, this.frame);
464
+ const cur = map.get(n);
465
+ if (isBound(cur) && cur.key) continue;
466
+ // Offline the callee's arithmetic is opaque and the lift only needs
467
+ // the key on the slot. A live run computed the value (E46.IPO's
468
+ // inttohexstring formats F_ORT_NR through a DLL printf), so the key
469
+ // rides along with it instead of replacing it.
470
+ const keep = this.wireJobs && cur != null && cur !== '';
471
+ const val = mkBound(mkSlot(dsc, n), keep ? asStr(cur) : '0', inKey);
472
+ this.setBind(dsc, n, inKey);
473
+ map.set(n, val);
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Call a user function offline. A call is not a new budget; only the frame
479
+ * is per-call. An unknown function is a noop.
480
+ * @param {IpoToken} t - the calluser token
481
+ * @param {IpoValue[]} stack - the call's arguments
482
+ * @returns {void}
483
+ */
484
+ _callUser(t, stack) {
485
+ const name = this.byid('func', t.n);
486
+ if (!name || !this.procs[name]) return;
487
+ const { inKey, outs, frame } = this._callArgs(stack);
488
+ this._exec(this.procs[name], frame);
489
+ this._carryKeyToOuts(inKey, outs);
490
+ }
491
+
492
+ // ------------------------------------------- resumable state driver --
493
+
494
+ /**
495
+ * Reset the per-run step budget and fetch a proc's tape. The budget is a
496
+ * runaway guard PER RUN: a persistent VM (the live runtime cycles a
497
+ * frequent screen for as long as the module is open) must not hit it by
498
+ * accumulation.
499
+ * @param {string} name - proc name
500
+ * @returns {IpoToken[]}
501
+ * @throws {IpoError} when the proc does not exist
502
+ */
503
+ _beginRun(name) {
504
+ this.steps = 0;
505
+ const toks = this.procs[name];
506
+ if (!toks) throw new IpoError(`no proc ${name}`);
507
+ return toks;
508
+ }
509
+
510
+ /**
511
+ * Begin a resumable run of proc `name`.
512
+ * @param {string} name - proc name
513
+ * @returns {IpoStep} the first pending action, or {kind:'done'}
514
+ * @throws {IpoError} when the proc does not exist
515
+ */
516
+ stepStart(name) {
517
+ const toks = this._beginRun(name);
518
+ return this._beginRange(toks, 0, ipoProcEnd(toks));
519
+ }
520
+
521
+ /**
522
+ * INPA's KEYPRESS: run ONE item's body inside its menu proc. Starts after
523
+ * the ITEM token, stops at the next one; the body's keypress-guard flags
524
+ * are preset (pressing the key IS the flag); jumps resolve against the
525
+ * whole proc, which slicing the body out would lose.
526
+ *
527
+ * Only the FLAGS (`if (flag == 1)`) are preset, never a slot the body
528
+ * compares against a string: IHKA46's digital keys toggle
529
+ * `if (v37 == "ON") v37 = "OFF" else v37 = "ON"` and send v37, and a
530
+ * preset of 1 on every press sent ON forever. This VM persists across
531
+ * presses; that state is the point.
532
+ * @param {string} procName - the menu proc
533
+ * @param {number} nr - the item's F-key number
534
+ * @returns {IpoStep}
535
+ * @throws {IpoError} when the proc or the item does not exist
536
+ */
537
+ stepStartItem(procName, nr) {
538
+ const toks = this._beginRun(procName);
539
+ const idx = toks.findIndex((t) => t.op === 'ITEM' && t.nr === nr);
540
+ if (idx < 0) throw new IpoError(`no item ${nr} in ${procName}`);
541
+ let end = ipoProcEnd(toks);
542
+ for (let j = idx + 1; j < end; j++) {
543
+ if (toks[j].op === 'ITEM') {
544
+ end = j;
545
+ break;
546
+ }
547
+ }
548
+ for (const g of keypressGuards(toks, idx + 1, end, { numericOnly: true }))
549
+ this.globals.set(g, 1);
550
+ return this._beginRange(toks, idx + 1, end);
551
+ }
552
+
553
+ /**
554
+ * A slice of a proc, driven (a menu's prologue: its title and defaults).
555
+ * @param {string} procName - proc name
556
+ * @param {number} i0 - first token index
557
+ * @param {number} end - token index to stop at (exclusive)
558
+ * @returns {IpoStep}
559
+ * @throws {IpoError} when the proc does not exist
560
+ */
561
+ stepStartRange(procName, i0, end) {
562
+ const toks = this._beginRun(procName);
563
+ return this._beginRange(toks, i0, Math.min(end, ipoProcEnd(toks)));
564
+ }
565
+
566
+ /**
567
+ * Park a fresh suspension on a tape range and drive it.
568
+ * @param {IpoToken[]} toks - the tape
569
+ * @param {number} i0 - first token index
570
+ * @param {number} end - token index to stop at (exclusive)
571
+ * @returns {IpoStep}
572
+ */
573
+ _beginRange(toks, i0, end) {
574
+ this._susp = {
575
+ toks,
576
+ frame: new Map(),
577
+ i: i0,
578
+ stack: [],
579
+ callers: [],
580
+ index: ipoByteIndex(toks),
581
+ end,
582
+ remap: ipoSegRemap(toks),
583
+ };
584
+ return this._drive();
585
+ }
586
+
587
+ /**
588
+ * Take the picker's answer: builtin_16 writes the component (ORT) into the
589
+ * toggle out-var, getinputstate returns the on/off. `ein` defaults to the
590
+ * confirming 0 (the guard `getinputstate == 0` opens on a valid pick).
591
+ * @param {IpoPick} value - the pick
592
+ * @returns {void}
593
+ */
594
+ _takePick(value) {
595
+ if (typeof value === 'object' && !Array.isArray(value)) {
596
+ this._pickInput = value.ort != null ? value.ort : null;
597
+ this._driveInput = value.ein != null ? value.ein : IPO_INPUT_CONFIRMED;
598
+ } else {
599
+ this._pickInput = value;
600
+ this._driveInput = IPO_INPUT_CONFIRMED;
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Store an input dialog's answer through the parked call's out-refs. A hex
606
+ * ask answers with a STRING (stored verbatim); a two-field ask answers
607
+ * with an ARRAY, one element per out-ref -- storing a single number
608
+ * through the first ref only left every second field (input2hexnum's
609
+ * number, input2int's Jahr) unset. The out-refs target the SUSPENDED
610
+ * body's locals; resume runs outside _drive, so the store switches to that
611
+ * frame.
612
+ * @param {IpoSuspension} s - the suspension
613
+ * @param {string|number|Array<string|number>} value - the answer
614
+ * @returns {void}
615
+ */
616
+ _storeInputAnswer(s, value) {
617
+ const prevFrame = this.frame;
618
+ this.frame = s.frame;
619
+ const store1 = (ref, v) => {
620
+ if (typeof v === 'string') {
621
+ storeOut(this, [ref], v, null);
622
+ return;
623
+ }
624
+ const n = Math.trunc(Number(v));
625
+ storeOut(this, [ref], Number.isFinite(n) ? n : 0, null);
626
+ };
627
+ const refs = s.pendingStack.filter(isRef);
628
+ if (Array.isArray(value)) {
629
+ refs.forEach((r, k) => {
630
+ if (k < value.length) store1(r, value[k]);
631
+ });
632
+ } else if (refs.length) {
633
+ refs.forEach((r) => store1(r, value));
634
+ }
635
+ this.frame = prevFrame;
636
+ }
637
+
638
+ /**
639
+ * Resume a suspended run with the result of its pending action: the picked
640
+ * togglelist row (for a yield that opened a picker, or a toggle), the typed
641
+ * answer (for an input), or a job's result sets (for a wire job).
642
+ * @param {*} [value] - the pending action's result
643
+ * @returns {IpoStep} the next pending action or {kind:'done'}
644
+ */
645
+ resume(value) {
646
+ if (!this._susp) return { kind: 'done' };
647
+ const s = this._susp;
648
+ const wasYield = s.pending === 'yield';
649
+ if (wasYield && value != null) {
650
+ // the pick drives the machine
651
+ this._takePick(value);
652
+ } else if (s.pending === 'input') {
653
+ if (s.pendingStack) {
654
+ this._storeInputAnswer(s, value);
655
+ s.pendingStack = null;
656
+ }
657
+ } else if (s.pending === 'file') {
658
+ // the picker's answer: the chosen name, '' when cancelled
659
+ if (s.pendingStack) {
660
+ const prevFrame = this.frame;
661
+ this.frame = s.frame;
662
+ try {
663
+ ipoDllFileAnswer(this, s.pendingStack, value);
664
+ } finally {
665
+ this.frame = prevFrame;
666
+ }
667
+ s.pendingStack = null;
668
+ }
669
+ } else if (s.pending === 'toggle' || s.pending === 'pick') {
670
+ // the pick: run the parked togglelist (or bmweb_pick) with it, then
671
+ // step past it
672
+ if (value != null) {
673
+ this._takePick(value);
674
+ const prevFrame = this.frame;
675
+ this.frame = s.frame;
676
+ try {
677
+ this._builtin(s.toks[s.i], s.pendingStack || [], null);
678
+ } finally {
679
+ this.frame = prevFrame;
680
+ this._pickInput = null; // one pick per togglelist call
681
+ }
682
+ }
683
+ s.pendingStack = null;
684
+ } else if (s.pending === 'exit') {
685
+ this._susp = null;
686
+ return { kind: 'done', out: this.out };
687
+ } else if (s.pending === 'job') {
688
+ // the wire answered; fold its result keys in so a later read sees them
689
+ this._lastJobSets = value || {};
690
+ // ...and serve them to INPAapiResult*: the reads go through the host,
691
+ // so a host that can be fed (FeedHost) is what closes the loop
692
+ if (this.host && typeof this.host.feed === 'function') {
693
+ this.host.feed(value);
694
+ }
695
+ }
696
+ s.pending = null;
697
+ s.i += 1; // step past the suspending token
698
+ // A yield is followed by its WAIT-LOOP back-edge: `state %Z; jump <exit>`
699
+ // is INPA's %WARTEN -- following that jump leaves the machine. Being
700
+ // DRIVEN forward (the user's pick) runs the segment body AFTER the jump
701
+ // instead. So on a yield-resume, step over the immediate unconditional
702
+ // jump into the body; a job-resume lands mid-body and needs no skip.
703
+ if (wasYield) {
704
+ const jt = s.toks[s.i];
705
+ if (jt && jt.op === 'jump') s.i += 1;
706
+ }
707
+ return this._drive();
708
+ }
709
+
710
+ /**
711
+ * Keypress-flag globals the CURRENT wait segment tests (var N == const ->
712
+ * jfalse before anything stores N): INPA's "Weiter"/"Start" keys set
713
+ * these. The driver shows a Continue control when the parked segment has
714
+ * one.
715
+ * @returns {Set<number>}
716
+ */
717
+ pendingGuards() {
718
+ const s = this._susp;
719
+ if (!s) return new Set();
720
+ let end = s.end;
721
+ for (let k = s.i + 1; k < s.end; k++) {
722
+ if (s.toks[k].op === 'state') {
723
+ end = k;
724
+ break;
725
+ }
726
+ }
727
+ return keypressGuards(s.toks, s.i + 1, end);
728
+ }
729
+
730
+ /**
731
+ * Press a machine key: set its guard flag so the wait segment opens.
732
+ * @param {number} n - the flag's global slot
733
+ * @returns {void}
734
+ */
735
+ pressKey(n) {
736
+ this.globals.set(n, 1);
737
+ }
738
+
739
+ /**
740
+ * The resumable loop. Runs synchronously until it suspends or finishes.
741
+ * Reads s.toks/s.index/s.end afresh each pass: a driven calluser SWITCHES
742
+ * the suspension onto the callee, and stale locals would keep stepping the
743
+ * caller's tape.
744
+ * @returns {IpoStep}
745
+ * @throws {Halt} when the step budget runs out
746
+ */
747
+ _drive() {
748
+ const s = this._susp;
749
+ const prevFrame = this.frame;
750
+ try {
751
+ for (;;) {
752
+ this.frame = s.frame;
753
+ if (s.i >= s.end) {
754
+ if (s.callers && s.callers.length) {
755
+ this._popCall(s);
756
+ continue;
757
+ }
758
+ break;
759
+ }
760
+ this.steps += 1;
761
+ if (this.steps > this.budget) throw new Halt('step budget');
762
+ const t = s.toks[s.i];
763
+ const op = t.op;
764
+ if (op === 'state') {
765
+ // suspend: the drawn screen so far IS the picker
766
+ this.out.states.push(t.name);
767
+ s.pending = 'yield';
768
+ return { kind: 'yield', name: t.name, out: this.out };
769
+ }
770
+ const sig = this._stepOne(t, op, s, s.index, s.end);
771
+ if (sig === 'ret') {
772
+ if (s.callers && s.callers.length) {
773
+ this._popCall(s);
774
+ continue;
775
+ }
776
+ break;
777
+ }
778
+ if (sig === 'jumped') continue; // _stepOne already moved s.i
779
+ if (sig === 'called') continue; // switched into a callee
780
+ if (sig === 'stop') {
781
+ // INPA's `stop`: leave the body being run. In a helper that is
782
+ // its return; at the top it skips to the next ITEM/LINE of the
783
+ // proc (a screen's other lines still draw), or to the end.
784
+ if (s.callers && s.callers.length) {
785
+ this._popCall(s);
786
+ continue;
787
+ }
788
+ const nxt = this._segmentEnd(s);
789
+ if (nxt >= s.end) break;
790
+ s.i = nxt;
791
+ continue;
792
+ }
793
+ if (sig && IPO_SUSPEND_KINDS.has(sig.kind)) {
794
+ // a wire job (the renderer runs it), a timed wait (it sleeps), a
795
+ // user prompt / message / picker (it asks and hands the answer
796
+ // back), or the script's own exit
797
+ s.pending = sig.kind;
798
+ if (
799
+ sig.kind === 'input' ||
800
+ sig.kind === 'toggle' ||
801
+ sig.kind === 'pick' ||
802
+ sig.kind === 'file'
803
+ )
804
+ s.pendingStack = sig.stack;
805
+ return sig;
806
+ }
807
+ s.i += 1;
808
+ }
809
+ } finally {
810
+ this.frame = prevFrame;
811
+ }
812
+ this._susp = null;
813
+ return { kind: 'done', out: this.out };
814
+ }
815
+
816
+ /**
817
+ * One token of the resumable loop. Mirrors _execIn's handling of every op
818
+ * EXCEPT `state` (handled in _drive), minus the offline-only bookkeeping
819
+ * (predicate reads, harvested reads, item record indices), plus the live
820
+ * dllcall and the driven calluser.
821
+ * @param {IpoToken} t - the token
822
+ * @param {string} op - its op
823
+ * @param {IpoSuspension} s - the suspension ({stack, i, frame} are advanced in place)
824
+ * @param {Map<number, number>} index - the tape's byte index
825
+ * @param {number} end - the run's end
826
+ * @returns {'ret'|'jumped'|'called'|'stop'|IpoStep|undefined} how the loop
827
+ * should continue: undefined = advance normally
828
+ */
829
+ _stepOne(t, op, s, index, end) {
830
+ const stack = s.stack;
831
+ if (op === 'frame') {
832
+ s.stack = [];
833
+ } else if (op === 'const') {
834
+ stack.push(t.t === 'd' ? mkFloat(t.v) : t.v);
835
+ } else if (op === 'var') {
836
+ let val = this._read(t, s.frame);
837
+ // a by-reference parameter handed on to a structure read is the
838
+ // reference itself, as a procref would carry it: chr() reads the
839
+ // byte it packed back into the CALLER's string through its parameter
840
+ if (this.wireJobs && ipoArgFeedsStructRead(s.toks, s.i)) {
841
+ const raw = this._rawSlot(t, s.frame);
842
+ if (isRef(raw)) val = raw;
843
+ }
844
+ if (val == null) val = mkSlot(t.sc == null ? GLOBAL : t.sc, t.n);
845
+ stack.push(val);
846
+ } else if (op === 'procref') {
847
+ stack.push([
848
+ 'ref',
849
+ t.kind,
850
+ t.n,
851
+ t.kind === IPO_REF_LOCAL ? s.frame : null,
852
+ ]);
853
+ } else if (op === 'store') {
854
+ let val = stack.length ? stack.pop() : null;
855
+ const sc = t.sc == null ? GLOBAL : t.sc;
856
+ val = this._bindPendingBinary(sc, t.n, val);
857
+ this._write(t, s.frame, val);
858
+ } else if (op === 'binop') {
859
+ let b = stack.length ? stack.pop() : null;
860
+ let a = t.name === 'neg' ? null : stack.length ? stack.pop() : null; // unary
861
+ // a local the script never wrote is 0 (or '' beside a string) when it
862
+ // runs, as INPA zero-fills its declarations; the offline lift keeps
863
+ // the slot so a compare on it stays undecided there
864
+ if (this.wireJobs && IPO_CMP_OPS.has(t.name)) {
865
+ if (isSlot(a)) a = ipoSlotDefault(b);
866
+ if (isSlot(b)) b = ipoSlotDefault(a);
867
+ }
868
+ stack.push(binop(t.name, a, b));
869
+ } else if (op === 'jfalse') {
870
+ const cond = stack.length ? stack.pop() : false;
871
+ if (!truthy(cond)) {
872
+ const nxt = this._jumpIndex(s, index, end);
873
+ if (nxt >= end) return 'ret';
874
+ s.i = nxt;
875
+ return 'jumped';
876
+ }
877
+ } else if (op === 'jump') {
878
+ const nxt = this._jumpIndex(s, index, end);
879
+ if (nxt >= end) return 'ret';
880
+ s.i = nxt;
881
+ return 'jumped';
882
+ } else if (op === 'ITEM') {
883
+ s.stack = [];
884
+ } else if (op === 'LINE') {
885
+ this.out.lines.push({ label: t.label, elements: [] });
886
+ s.stack = [];
887
+ } else if (op === 'call') {
888
+ const sig = ipoDriveBuiltin(this, t, stack);
889
+ s.stack = [];
890
+ if (sig) return sig; // a pending action
891
+ } else if (op === 'dllcall') {
892
+ // an import32 call: only a live run answers it (see ipoDllCall); the
893
+ // save-as dialog among them is a pending action like an input
894
+ const sig = this.wireJobs ? ipoDllCall(this, stack) : null;
895
+ s.stack = [];
896
+ if (sig) return sig;
897
+ } else if (op === 'calluser') {
898
+ const entered = this._pushCall(s, t, stack);
899
+ s.stack = [];
900
+ if (entered) return 'called';
901
+ } else if (op === 'ret') {
902
+ return 'ret';
903
+ }
904
+ return undefined;
905
+ }
906
+
907
+ /**
908
+ * Where the body being run ends: the next ITEM or LINE token after the
909
+ * current one, or the run's end when this is the last body.
910
+ * @param {IpoSuspension} s - the suspension
911
+ * @returns {number} token index
912
+ */
913
+ _segmentEnd(s) {
914
+ for (let j = s.i + 1; j < s.end; j++) {
915
+ const op = s.toks[j].op;
916
+ if (op === 'ITEM' || op === 'LINE') return j;
917
+ }
918
+ return s.end;
919
+ }
920
+
921
+ /**
922
+ * DRIVE INTO THE CALLEE. The offline _callUser executes a user function
923
+ * through the offline executor, so a job inside a helper never suspends --
924
+ * and INPA puts the send inside helpers routinely (ms450's llerh(delta)
925
+ * computes the setpoint and calls START_SYSTEMCHECK_LLERH itself). The
926
+ * driven path keeps a call stack instead: switch the suspension onto the
927
+ * callee's tokens; _drive pops back on ret/end. An unknown function is a
928
+ * noop, exactly as offline.
929
+ * @param {IpoSuspension} s - the suspension
930
+ * @param {IpoToken} t - the calluser token
931
+ * @param {IpoValue[]} stack - the call's arguments
932
+ * @returns {boolean} whether the suspension switched into a callee
933
+ */
934
+ _pushCall(s, t, stack) {
935
+ const name = this.byid('func', t.n);
936
+ if (!name || !this.procs[name]) return false;
937
+ const { inKey, outs, frame } = this._callArgs(stack);
938
+ if (!s.callers) s.callers = [];
939
+ s.callers.push({
940
+ toks: s.toks,
941
+ i: s.i,
942
+ end: s.end,
943
+ index: s.index,
944
+ remap: s.remap,
945
+ frame: s.frame,
946
+ inKey,
947
+ outs,
948
+ });
949
+ const toks = this.procs[name];
950
+ s.toks = toks;
951
+ s.i = 0;
952
+ s.end = ipoProcEnd(toks);
953
+ s.index = ipoByteIndex(toks);
954
+ s.remap = ipoSegRemap(toks);
955
+ s.frame = frame;
956
+ return true;
957
+ }
958
+
959
+ /**
960
+ * Return from a driven callee: the same conversion-helper key carry the
961
+ * offline _callUser performs, then the caller's suspension is restored.
962
+ * @param {IpoSuspension} s - the suspension
963
+ * @returns {void}
964
+ */
965
+ _popCall(s) {
966
+ const c = s.callers.pop();
967
+ this.frame = c.frame;
968
+ this._carryKeyToOuts(c.inKey, c.outs);
969
+ s.toks = c.toks;
970
+ s.i = c.i + 1;
971
+ s.end = c.end;
972
+ s.index = c.index;
973
+ s.remap = c.remap;
974
+ s.frame = c.frame;
975
+ }
976
+
977
+ /**
978
+ * The driven executor's jump resolution: the segment-rebased target first
979
+ * (see ipoSegRemap), the walker's raw byte target as the fallback for the
980
+ * few machines outside the model. A target neither resolves = exit.
981
+ * @param {IpoSuspension} s - the suspension (its current token is the jump)
982
+ * @param {Map<number, number>} index - the tape's byte index
983
+ * @param {number} end - the run's end
984
+ * @returns {number} the token index to continue at, or `end`
985
+ */
986
+ _jumpIndex(s, index, end) {
987
+ const t = s.toks[s.i];
988
+ const ct = s.remap ? s.remap.get(s.i) : undefined;
989
+ if (ct !== undefined && index.has(ct)) return index.get(ct);
990
+ return index.has(t.to) ? index.get(t.to) : end;
991
+ }
992
+
993
+ // ------------------------------------------------------------ reads --
994
+
995
+ /**
996
+ * Record result keys an unexecuted branch reads, without running it, so
997
+ * the union of keys across arms matches a full static decode of every
998
+ * branch. Follows calluser into the callee (guarded against recursion).
999
+ * Ported from _harvest_reads.
1000
+ * @param {IpoToken[]} toks - the tape
1001
+ * @param {number} lo - first token index of the skipped branch
1002
+ * @param {number} hi - token index it ends at (exclusive)
1003
+ * @param {Set<string>} [seen] - callees already harvested
1004
+ * @returns {void}
1005
+ */
1006
+ _harvestReads(toks, lo, hi, seen) {
1007
+ if (!seen) seen = new Set();
1008
+ hi = Math.min(hi, toks.length);
1009
+ for (let k = lo; k < hi; k++) {
1010
+ const t = toks[k],
1011
+ op = t.op;
1012
+ if (op === 'calluser') {
1013
+ const name = this.byid('func', t.n);
1014
+ if (name && this.procs[name] && !seen.has(name)) {
1015
+ seen.add(name);
1016
+ const body = this.procs[name];
1017
+ this._harvestReads(body, 0, body.length, seen);
1018
+ }
1019
+ continue;
1020
+ }
1021
+ if (op !== 'call' || !(t.name || '').includes('Result')) continue;
1022
+ for (let b = k - 1; b >= Math.max(lo, k - IPO_HARVEST_LOOKBACK); b--) {
1023
+ const tb = toks[b];
1024
+ const v = tb.op === 'const' ? tb.v : null;
1025
+ if (typeof v === 'string' && v) {
1026
+ if (!this.out.reads.includes(v)) this.out.reads.push(v);
1027
+ break;
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ /**
1034
+ * Read a `var` token's slot. A by-reference parameter READS the caller's
1035
+ * value: `strlen(->n, s)` inside `stringappend(string &s, ...)` measures
1036
+ * the string, not the reference. Without this the pad loop never ended
1037
+ * (length 0 forever).
1038
+ * @param {IpoToken} t - the var token
1039
+ * @param {Map<number, IpoValue>} frame - the current frame
1040
+ * @returns {IpoValue|null}
1041
+ */
1042
+ /**
1043
+ * A slot's content as stored, a reference included (what `_read` would
1044
+ * follow).
1045
+ * @param {IpoToken} t - the var token
1046
+ * @param {Map<number, IpoValue>|null} frame - the current frame
1047
+ * @returns {IpoValue}
1048
+ */
1049
+ _rawSlot(t, frame) {
1050
+ const sc = t.sc == null ? GLOBAL : t.sc;
1051
+ if (sc === GLOBAL)
1052
+ return this.globals.has(t.n) ? this.globals.get(t.n) : null;
1053
+ return frame && frame.has(t.n) ? frame.get(t.n) : null;
1054
+ }
1055
+
1056
+ _read(t, frame) {
1057
+ const sc = t.sc == null ? GLOBAL : t.sc;
1058
+ let v;
1059
+ if (sc === GLOBAL) v = this.globals.has(t.n) ? this.globals.get(t.n) : null;
1060
+ else v = frame.has(t.n) ? frame.get(t.n) : null;
1061
+ if (isRef(v)) {
1062
+ const tg = this._refTarget(v, frame);
1063
+ v = tg.map.has(tg.n) ? tg.map.get(tg.n) : null;
1064
+ }
1065
+ return v;
1066
+ }
1067
+
1068
+ /**
1069
+ * Where a reference points: the owning frame (or the globals) and the
1070
+ * slot, following a reference stored in a referenced slot (a by-ref
1071
+ * parameter handed on by reference).
1072
+ * @param {IpoRef} ref - the reference
1073
+ * @param {Map<number, IpoValue>|null} frame - the current frame
1074
+ * @returns {{dsc: number, n: number, map: Map<number, IpoValue>}}
1075
+ */
1076
+ _refTarget(ref, frame) {
1077
+ let cur = ref;
1078
+ let map = null,
1079
+ dsc = GLOBAL,
1080
+ n = 0;
1081
+ for (let hops = 0; hops < IPO_REF_MAX_HOPS && isRef(cur); hops++) {
1082
+ const owner = cur[1] === IPO_REF_LOCAL ? cur[3] || frame : null;
1083
+ if (owner) {
1084
+ dsc = LOCAL;
1085
+ map = owner;
1086
+ } else {
1087
+ dsc = GLOBAL;
1088
+ map = this.globals;
1089
+ }
1090
+ n = cur[2];
1091
+ const next = map.has(n) ? map.get(n) : null;
1092
+ if (!isRef(next)) break;
1093
+ cur = next;
1094
+ }
1095
+ return { dsc, n, map: map || this.globals };
1096
+ }
1097
+
1098
+ /**
1099
+ * Store through a `store` token. A store through an out-parameter (the
1100
+ * slot holds a ref) lands in the CALLER's slot and moves the binding with
1101
+ * it. Mirrors ipo_vm.py _write.
1102
+ * @param {IpoToken} t - the store token
1103
+ * @param {Map<number, IpoValue>|null} frame - the current frame
1104
+ * @param {IpoValue} val - the value
1105
+ * @returns {void}
1106
+ */
1107
+ _write(t, frame, val) {
1108
+ const sc = t.sc == null ? GLOBAL : t.sc;
1109
+ if (t.ref) {
1110
+ const src =
1111
+ sc !== GLOBAL ? (frame ? frame.get(t.n) : null) : this.globals.get(t.n);
1112
+ if (isRef(src)) {
1113
+ const { dsc, n, map } = this._refTarget(src, frame);
1114
+ const key = isBound(val) && val.key ? val.key : null;
1115
+ if (key) this.setBind(dsc, n, key);
1116
+ else this.delBind(dsc, n);
1117
+ map.set(n, val);
1118
+ return;
1119
+ }
1120
+ }
1121
+ if (sc === GLOBAL) this.globals.set(t.n, val);
1122
+ else frame.set(t.n, val);
1123
+ }
1124
+
1125
+ // --------------------------------------------------------- builtins --
1126
+
1127
+ /**
1128
+ * Run a `call` token's builtin offline (a name the table lacks is recorded
1129
+ * and skipped).
1130
+ * @param {IpoToken} t - the call token
1131
+ * @param {IpoValue[]} stack - the call's arguments
1132
+ * @param {IpoItem|null} curItem - the item being executed
1133
+ * @returns {void}
1134
+ */
1135
+ _builtin(t, stack, curItem) {
1136
+ const name = ipoBuiltinName(t);
1137
+ this.out.calls.push(name);
1138
+ const fn = BUILTINS[name];
1139
+ if (fn) fn(this, stack, curItem);
1140
+ }
1141
+ }
1142
+
1143
+ // Loaded two ways: as <script>s in the app (globals) and through the verify
1144
+ // harnesses' classic-script loader (module.exports, unioned across pieces).
1145
+ if (typeof window !== 'undefined') {
1146
+ window.IpoVm = IpoVm;
1147
+ window.IpoError = IpoError;
1148
+ window.OkHost = OkHost;
1149
+ window.FeedHost = FeedHost;
1150
+ window.scanQuitBox = scanQuitBox;
1151
+ }
1152
+ if (typeof module !== 'undefined' && module.exports) {
1153
+ module.exports = {
1154
+ IpoVm,
1155
+ IpoError,
1156
+ OkHost,
1157
+ FeedHost,
1158
+ scanQuitBox,
1159
+ Emissions,
1160
+ mkBound,
1161
+ mkSlot,
1162
+ isBound,
1163
+ isSlot,
1164
+ binop,
1165
+ };
1166
+ }