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,241 @@
1
+ /**
2
+ * @file Wire-mode suspensions: which builtins the DRIVEN executor must hand
3
+ * to the renderer instead of answering itself, and the pending-action shape
4
+ * each produces. A live actuator (INPA's %STATE loop) parks at a state,
5
+ * shows a picker, and on the user's pick resumes past the yield into the
6
+ * job; a live input asks the user; a live job goes on the wire. The
7
+ * renderer does the async part and calls resume() to continue.
8
+ */
9
+
10
+ /**
11
+ * What a driven run hands back when it cannot continue on its own. `kind`
12
+ * says why; `out` is the emissions so far (a parked machine's drawn screen
13
+ * IS the picker).
14
+ * @typedef {object} IpoStep
15
+ * @property {'done'|'yield'|'job'|'wait'|'input'|'message'|'toggle'|'print'|'select'|'exit'} kind -
16
+ * done: the proc finished; yield: parked at a %STATE; job: a wire job to
17
+ * run and feed back; wait: a timed wartezeit; input: an INPA prompt;
18
+ * message: a blocking messagebox; toggle: the component picker; print:
19
+ * printscreen; select: INPA's line filter; exit: the script ended itself
20
+ * @property {Emissions} [out] - emissions so far
21
+ * @property {string} [name] - yield: the state label
22
+ * @property {string} [job] - job: the job name
23
+ * @property {string|null} [sgbd] - job: the SGBD the script addressed
24
+ * @property {string|null} [arg] - job: the argument
25
+ * @property {number} [ms] - wait: how long
26
+ * @property {string[]} [prompts] - input: the prompt strings, title first
27
+ * @property {number} [refs] - input: how many out-refs (fields) it fills
28
+ * @property {number|null} [lo] - input: accepted range low, when declared
29
+ * @property {number|null} [hi] - input: accepted range high, when declared
30
+ * @property {IpoValue[]} [stack] - input/toggle: the call's arguments, kept for the resume store
31
+ * @property {string} [title] - message: the box title
32
+ * @property {string|null} [body] - message: the box text
33
+ * @property {boolean} [multiple] - toggle/select: MultipleSelectFlag
34
+ * @property {boolean} [argnum] - toggle: ArgNumFlag (line numbers instead of keys)
35
+ */
36
+
37
+ /**
38
+ * The step kinds that park the driven loop until the renderer resumes it.
39
+ * @type {Set<string>}
40
+ */
41
+ const IPO_SUSPEND_KINDS = new Set([
42
+ 'file', // the save-as dialog (structures.js ipoDllFileDialog)
43
+ 'job',
44
+ 'wait',
45
+ 'input',
46
+ 'message',
47
+ 'toggle',
48
+ 'pick', // bmweb_pick: the host lists, the user chooses (home script)
49
+ 'print',
50
+ 'select',
51
+ 'exit',
52
+ ]);
53
+
54
+ /** The job builtins whose sends the driven executor hands to the renderer. */
55
+ const IPO_JOB_BUILTINS = new Set([
56
+ 'INPAapiJob',
57
+ 'INP1apiJob',
58
+ 'INPAapiJobData',
59
+ ]);
60
+
61
+ /** delay(ms) -- Inpa.h's name; the disassembler's builtin_1b. */
62
+ const IPO_WAIT_BUILTIN = 'delay';
63
+
64
+ /** builtin_16 = togglelist. */
65
+ const IPO_TOGGLELIST_BUILTIN = 'builtin_16';
66
+
67
+ /** The input builtins by name, for the ones the table maps without a name. */
68
+ const IPO_INPUT_NAME_RE = /^input(int|real|hex|string)?$/;
69
+
70
+ /**
71
+ * The text of a job argument on the stack: a plain or bound string, or a
72
+ * number's text; null for anything else.
73
+ * @param {IpoValue} x - the argument
74
+ * @returns {string|null}
75
+ */
76
+ function jobArgText(x) {
77
+ if (isPlainStr(x)) return x;
78
+ if (isBound(x)) return x.s;
79
+ if (typeof x === 'number') return String(x);
80
+ if (isFloat(x)) return String(x.v);
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * The 0/1 flag an argument carries (a boxed float, a bound value or a number).
86
+ * @param {IpoValue} v - the argument
87
+ * @returns {boolean}
88
+ */
89
+ function flagArg(v) {
90
+ return !!(isFloat(v) ? v.v : isBound(v) ? Number(v.s) : Number(v));
91
+ }
92
+
93
+ /**
94
+ * INPA's `stop` keyword as the disassembler names it: builtin 0x14.
95
+ * @type {string}
96
+ */
97
+ const IPO_STOP_BUILTIN = 'builtin_14';
98
+
99
+ /**
100
+ * Builtin dispatch for the resumable path. Identical to `_builtin` EXCEPT
101
+ * that, in wire mode, the builtins the renderer must answer are not run
102
+ * offline -- they return a pending action instead. Which jobs must reach the
103
+ * renderer even when the VM is not driving the wire: the ones that CHANGE
104
+ * the car, by the write classifier's verdict (token-based, default-deny, the
105
+ * same one the job route enforces -- not a name prefix: ANSTEUERN_*,
106
+ * LAMPEN_TEST, SG_RESET are writes too).
107
+ * @param {IpoVm} vm - the running VM
108
+ * @param {IpoToken} t - the call token
109
+ * @param {IpoValue[]} stack - the call's arguments
110
+ * @returns {IpoStep|null} the pending action, or null when the builtin ran
111
+ */
112
+ function ipoDriveBuiltin(vm, t, stack) {
113
+ const name = ipoBuiltinName(t);
114
+ if (IPO_JOB_BUILTINS.has(name)) {
115
+ const sgbd = stack.length > 0 ? jobArgText(stack[0]) : null;
116
+ const job = stack.length > 1 ? jobArgText(stack[1]) : null;
117
+ const arg = stack.length > 2 ? jobArgText(stack[2]) : null;
118
+ // classifier absent: never assume safe
119
+ const isDrive = typeof isWriteJob === 'function' ? isWriteJob(job) : true;
120
+ if (job && (vm.wireJobs || isDrive)) {
121
+ // hand the drive to the renderer; it confirms, registers for release,
122
+ // sends on the wire, and hands the result sets back through resume()
123
+ return {
124
+ kind: 'job',
125
+ job,
126
+ sgbd: sgbd || null,
127
+ arg: arg || null,
128
+ out: vm.out,
129
+ };
130
+ }
131
+ }
132
+ // input builtins: INPA asks the user and parks until getinputstate says
133
+ // confirmed. Offline they store '0' -- which a LIVE run must never send
134
+ // (LLERH's Select would command idle target 0). Suspend instead; the
135
+ // renderer shows INPA's own prompt and resume() stores the typed value.
136
+ // inputdigital too: offline it stores the CONFIRMING placeholder (so the
137
+ // static lift keeps the yes-branch), but live that would answer INPA's
138
+ // "Are you sure?" for the user -- and IHKA46's compressor-lock key sends
139
+ // on that answer. Suspend, ask, store what was pressed.
140
+ if (
141
+ vm.wireJobs &&
142
+ (BUILTINS[name] === bInput ||
143
+ BUILTINS[name] === bInputDigital ||
144
+ IPO_INPUT_NAME_RE.test(name))
145
+ ) {
146
+ const prompts = stack.filter((x) => isPlainStr(x) && x.trim());
147
+ // bounds may be ints (inputint) or doubles (builtin_40: MS43 pushes
148
+ // 512.0/1600.0); take the last two numerics either way
149
+ const nums = stack
150
+ .map((x) => (isPlainInt(x) ? x : isFloat(x) ? x.v : null))
151
+ .filter((x) => x != null && Number.isFinite(x));
152
+ return {
153
+ kind: 'input',
154
+ name,
155
+ prompts,
156
+ refs: stack.filter(isRef).length,
157
+ lo: nums.length > 1 ? nums[nums.length - 2] : null,
158
+ hi: nums.length > 1 ? nums[nums.length - 1] : null,
159
+ stack,
160
+ out: vm.out,
161
+ };
162
+ }
163
+ // A LIVE messagebox BLOCKS the script until OK, like INPA's own: the
164
+ // renderer shows it in sequence and resumes. Offline it is only recorded.
165
+ if (vm.wireJobs && BUILTINS[name] === bMessage) {
166
+ vm._builtin(t, stack, null);
167
+ const M = vm.out.messages;
168
+ const m = M.length ? M[M.length - 1] : { title: '', body: null };
169
+ return { kind: 'message', title: m.title, body: m.body, out: vm.out };
170
+ }
171
+ // INPA's Select key: select(MultipleSelectFlag) lists the current
172
+ // screen's named logical lines and shows only the ones picked. Park;
173
+ // the renderer offers the list and keeps the choice.
174
+ if (vm.wireJobs && name === 'select') {
175
+ const flag = stack.length ? stack[0] : 0;
176
+ return {
177
+ kind: 'select',
178
+ multiple: !!(isFloat(flag) ? flag.v : Number(flag)),
179
+ out: vm.out,
180
+ };
181
+ }
182
+ // INPA's Print key (printscreen): live, the renderer prints the page --
183
+ // the browser's print dialog stands in for INPA's printer
184
+ if (vm.wireJobs && name === 'printscreen') {
185
+ return { kind: 'print', out: vm.out };
186
+ }
187
+ // A LIVE togglelist is INPA's component picker: park until the renderer
188
+ // hands back the pick ({ort, ein}); resume re-runs the builtin with it.
189
+ // togglelist(MultipleSelectFlag, ArgNumFlag, ->ApiToggleString)
190
+ if (vm.wireJobs && name === IPO_TOGGLELIST_BUILTIN && vm._pickInput == null) {
191
+ return {
192
+ kind: 'toggle',
193
+ stack,
194
+ multiple: stack.length > 0 && flagArg(stack[0]),
195
+ argnum: stack.length > 1 && flagArg(stack[1]),
196
+ out: vm.out,
197
+ };
198
+ }
199
+ // BMWeb's own picker: bmweb_pick("chassis"|"module"|"vehicle", arg,
200
+ // ->choice). Park like a togglelist; the renderer asks the host for the
201
+ // list and resumes with the choice, which the re-run builtin stores.
202
+ if (vm.wireJobs && name === 'bmweb_pick' && vm._pickInput == null) {
203
+ const strs = stack.filter((x) => !isRef(x)).map((x) => asStr(x));
204
+ return {
205
+ kind: 'pick',
206
+ stack,
207
+ what: strs[0] || '',
208
+ arg: strs[1] || '',
209
+ out: vm.out,
210
+ };
211
+ }
212
+ if (vm.wireJobs && BUILTINS[name] === bExit) {
213
+ vm._builtin(t, stack, null);
214
+ return { kind: 'exit', out: vm.out };
215
+ }
216
+ if (vm.wireJobs && IPO_STRUCT_FNS.has(name)) {
217
+ ipoStructureCall(vm, name, stack);
218
+ return null;
219
+ }
220
+ // wartezeit: offline a noop; a guided run honours it -- the S_ZUHEIZ
221
+ // Pruefung waits 2000ms after DIAGNOSE_ENDE and 10000ms for the heater's
222
+ // run-on, and rushing those changes what the ECU answers.
223
+ if (vm.wireJobs && name === IPO_WAIT_BUILTIN) {
224
+ const ms = stack.find((x) => isPlainInt(x));
225
+ return { kind: 'wait', ms: ms != null ? ms : 0, out: vm.out };
226
+ }
227
+ // INPA's `stop`: end the body being run (this LINE, this ITEM, this
228
+ // proc) here. The corpus puts it after an error messagebox ("box, then
229
+ // stop, else read the results") and at the head of a screen that has
230
+ // nothing selected yet -- MS45's injector screen guards its STEUERN_EV_n
231
+ // send with `if (sel == 0) stop`, and falling through sent a job named
232
+ // after the on-time. Offline it stays a noop (the derived twin's stamp).
233
+ if (vm.wireJobs && name === IPO_STOP_BUILTIN) return 'stop';
234
+ // not a drive: run it exactly as the offline builtin would
235
+ vm._builtin(t, stack, null);
236
+ return null;
237
+ }
238
+
239
+ if (typeof module !== 'undefined' && module.exports) {
240
+ module.exports = { IPO_SUSPEND_KINDS, ipoDriveBuiltin };
241
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * @file Static scans over a proc's token tape, used without executing it:
3
+ * the byte-offset index a jump seeks through, where a proc's decoded tokens
4
+ * end, the segment re-basing of jump targets past a %STATE label, the
5
+ * keypress-guard flags an item body tests, and the quit-mode confirmation
6
+ * box a state machine pops.
7
+ */
8
+
9
+ /**
10
+ * One decoded token of a proc, as tools/export/ipo_exec.py emits it. Only
11
+ * the fields the VM reads are listed; which ones are present depends on `op`.
12
+ * @typedef {object} IpoToken
13
+ * @property {string} op - frame | const | var | procref | store | binop | jfalse |
14
+ * jump | ITEM | LINE | call | calluser | dllcall | state | ret | unk | ...
15
+ * @property {number} [at] - byte offset of the token in the .IPO
16
+ * @property {string} [t] - a const's pool tag ('s' string, 'i' int, 'd' double, 'b' bool)
17
+ * @property {*} [v] - a const's value
18
+ * @property {number} [sc] - a var/store's scope (absent = GLOBAL)
19
+ * @property {number} [n] - a var/store's slot, a procref/calluser's id, a call's builtin number
20
+ * @property {boolean} [ref] - a store through a by-reference parameter
21
+ * @property {number} [kind] - a procref's kind (see IPO_REF_*)
22
+ * @property {string} [name] - a binop's operator or a call's builtin name
23
+ * @property {number} [to] - a jump's target byte offset
24
+ * @property {number} [nr] - an ITEM's F-key number
25
+ * @property {string} [label] - an ITEM's caption or a LINE's name
26
+ * @property {string} [keys] - a LINE's component key string (for the picker)
27
+ */
28
+
29
+ /**
30
+ * The {byteOffset -> tokenIndex} index of a proc, so a resolved jump target is
31
+ * a seek.
32
+ * @param {IpoToken[]} toks - the proc's tokens
33
+ * @returns {Map<number, number>}
34
+ */
35
+ function ipoByteIndex(toks) {
36
+ const index = new Map();
37
+ for (let i = 0; i < toks.length; i++) {
38
+ if (toks[i].at != null) index.set(toks[i].at, i);
39
+ }
40
+ return index;
41
+ }
42
+
43
+ /**
44
+ * Where a proc's decoded tokens end: the first undecoded byte. A proc's block
45
+ * header sizes only its own section, so garbage past the real end (unk
46
+ * tokens) would spin.
47
+ * @param {IpoToken[]} toks - the proc's tokens
48
+ * @returns {number} token index of the first `unk`, or toks.length
49
+ */
50
+ function ipoProcEnd(toks) {
51
+ for (let j = 0; j < toks.length; j++) {
52
+ if (toks[j].op === 'unk') return j;
53
+ }
54
+ return toks.length;
55
+ }
56
+
57
+ /**
58
+ * JUMP TARGETS PAST A STATE ARE SEGMENT-RELATIVE. The compiler emits a jump's
59
+ * u16 as a dword index from its enclosing BLOCK -- the proc body, an
60
+ * ITEM/LINE body, or (the part the walker does not model) a %STATE segment:
61
+ * each state label opens a new block whose dwords count from the token after
62
+ * the state's own exit jump (the body the driven resume enters).
63
+ *
64
+ * The walker resolves every target against the proc/ITEM base, so a target
65
+ * inside a state segment lands short by the states' label bytes -- across the
66
+ * corpus only 41% of intra-segment jumps hit a real token that way, while
67
+ * re-basing per segment resolves 95.8% (and S_ZUHEIZ's Pruefung becomes
68
+ * semantically exact: the measure loop's jfalse skips ONE store, the Weiter
69
+ * guard exits to the %ENDE block). Offline execution never runs past the
70
+ * first state (it halts there), so only the DRIVEN path needs this; the
71
+ * offline executor stays byte-identical to ipo_vm.py.
72
+ *
73
+ * A corrected target may land ON a `state` token: that is a generic park
74
+ * (the current machine segment is what setstatemachine last set), which the
75
+ * driven loop's yield handling already provides.
76
+ * @param {IpoToken[]} toks - the proc's tokens
77
+ * @returns {Map<number, number>} jump token index -> re-based target byte offset
78
+ */
79
+ function ipoSegRemap(toks) {
80
+ const remap = new Map();
81
+ if (!toks.length || toks[0].at == null) return remap;
82
+ const at = (i) => toks[i].at;
83
+ let walker = at(0) + 4; // dword 0 of the proc's own block
84
+ let seg = walker;
85
+ for (let i = 0; i < toks.length; i++) {
86
+ const t = toks[i];
87
+ if (t.op === 'ITEM' || t.op === 'LINE') {
88
+ const nxt = i + 1 < toks.length ? at(i + 1) : at(i) + 4;
89
+ walker = nxt;
90
+ seg = nxt;
91
+ } else if (t.op === 'state') {
92
+ let j = i + 1;
93
+ if (j < toks.length && toks[j].op === 'jump') j += 1; // exit edge
94
+ seg = j < toks.length ? at(j) : at(i);
95
+ } else if (t.to != null) {
96
+ const u16 = t.to - walker;
97
+ if (u16 >= 0 && u16 % 4 === 0 && seg !== walker) {
98
+ remap.set(i, seg + u16);
99
+ }
100
+ }
101
+ }
102
+ return remap;
103
+ }
104
+
105
+ /**
106
+ * Global slots an item body gates its whole action on -- keypress flags.
107
+ * INPA wraps a key's action in `if flag == 1 ...`; pressing the key means the
108
+ * flag is true, so run_item must force it -- but ONLY a guard flag (a GLOBAL
109
+ * compared by `eq` feeding a `jfalse`, before the body stores anything),
110
+ * never a global read as real data. Ported from ipo_vm.py _keypress_guards.
111
+ * @param {IpoToken[]} toks - the menu proc's tokens
112
+ * @param {number} start - first token index of the body
113
+ * @param {number} end - token index the body ends at (exclusive)
114
+ * @param {{numericOnly?: boolean}} [opts] - numericOnly: a slot compared
115
+ * against a STRING ("ON") is state the body keeps, not a flag -- a preset
116
+ * of 1 can never satisfy that compare and only destroys the value (the
117
+ * live item VM's concern; the offline derivation keeps the Python twin's
118
+ * exact behaviour)
119
+ * @returns {Set<number>} the guard flags' global slot numbers
120
+ */
121
+ function keypressGuards(toks, start, end, opts = {}) {
122
+ const guards = new Set(),
123
+ stored = new Set();
124
+ const lim = Math.min(end, toks.length);
125
+ for (let k = start; k < lim; k++) {
126
+ const t = toks[k],
127
+ op = t.op;
128
+ if (op === 'store' && (t.sc == null ? GLOBAL : t.sc) === GLOBAL) {
129
+ stored.add(t.n);
130
+ }
131
+ if (
132
+ op === 'var' &&
133
+ (t.sc == null ? GLOBAL : t.sc) === GLOBAL &&
134
+ !stored.has(t.n)
135
+ ) {
136
+ const b = k + 1 < end ? toks[k + 1] : {};
137
+ const c = k + 2 < end ? toks[k + 2] : {};
138
+ if (
139
+ opts.numericOnly &&
140
+ b.op === 'const' &&
141
+ b.t === 's' &&
142
+ !/^-?\d+$/.test(String(b.v).trim())
143
+ )
144
+ continue;
145
+ if (b.op === 'const' && c.op === 'binop' && c.name === 'eq') {
146
+ let gated = false;
147
+ for (let j = k + 3; j < Math.min(k + 6, end); j++) {
148
+ if (toks[j].op === 'jfalse') {
149
+ gated = true;
150
+ break;
151
+ }
152
+ }
153
+ if (gated) guards.add(t.n);
154
+ }
155
+ }
156
+ }
157
+ return guards;
158
+ }
159
+
160
+ /**
161
+ * The quit-mode confirmation a machine pops after a successful drive:
162
+ * if (slotN == 1) messagebox(title, prefix + <ORT>)
163
+ * (ZKE5 sm_steuern: slot 29 = the "with Quitting" toggle, box "ACTIVATED
164
+ * DIGITAL VALUE / Signal : <ORT>"). Scanned from the bytecode so the slot and
165
+ * the words are INPA's own, never invented.
166
+ * @param {IpoToken[]} toks - the state machine's tokens
167
+ * @returns {{slot: number, title: string, prefix: string}|null} null when
168
+ * the machine has no such box
169
+ */
170
+ function scanQuitBox(toks) {
171
+ if (!Array.isArray(toks)) return null;
172
+ for (let i = 0; i + 4 < toks.length; i++) {
173
+ const a = toks[i],
174
+ b = toks[i + 1],
175
+ c = toks[i + 2];
176
+ if (a.op !== 'var' || b.op !== 'const' || b.v !== 1) continue;
177
+ if (c.op !== 'binop' || c.name !== 'eq') continue;
178
+ // guarded body within reach: frame, const title, const prefix, ...,
179
+ // call messagebox
180
+ for (let j = i + 3; j < Math.min(i + 14, toks.length); j++) {
181
+ const t = toks[j];
182
+ if (t.op === 'call' && t.name === 'messagebox') {
183
+ const strs = [];
184
+ for (let k = i + 3; k < j; k++) {
185
+ if (toks[k].op === 'const' && toks[k].t === 's') strs.push(toks[k].v);
186
+ }
187
+ if (strs.length >= 2) {
188
+ return { slot: a.n, title: strs[0], prefix: strs[1] };
189
+ }
190
+ break;
191
+ }
192
+ if (t.op === 'state' || t.op === 'jump') break;
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+
198
+ if (typeof module !== 'undefined' && module.exports) {
199
+ module.exports = {
200
+ ipoByteIndex,
201
+ ipoProcEnd,
202
+ ipoSegRemap,
203
+ keypressGuards,
204
+ scanQuitBox,
205
+ };
206
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * @file The .IPO virtual machine's value model: the boxes a VM value travels
3
+ * in, the predicates that tell them apart, and the two errors the executor
4
+ * throws.
5
+ *
6
+ * The VM (core/ipovm/vm.js) EXECUTES .IPO screen/menu programs in the
7
+ * browser. It is the JS twin of tools/decompile/ipo_vm.py: it runs the same
8
+ * decoded token tape (tools/export/ipo_exec.py output: {procs, byid, pool},
9
+ * a derivation of the .IPO with constants inlined and jumps resolved to byte
10
+ * offsets) and produces the same emissions (drawn lines, menu items, jobs,
11
+ * dialogs), so screens can be executed rather than frozen.
12
+ *
13
+ * THE MODEL, ported from ipo_vm.py (not bestvm.js -- BEST2 is a byte-register
14
+ * machine; .IPO is a STACK machine):
15
+ * - a per-call-frame operand stack; `frame` (0x0f) clears it, a call
16
+ * consumes it. Scope 0 = script globals, 2/3 = the frame's own slots.
17
+ * - jumps are token seeks: t.to is a byte offset, resolved through a
18
+ * {byteOffset -> tokenIndex} index built per proc.
19
+ * - builtins take an OUT-PARAMETER (a procref on the stack), never return a
20
+ * value -- INPAapiResultInt(->dest, KEY, i), inttostring(src, ->dest).
21
+ * - host interaction (job results, dialog state) is injected, like
22
+ * bestvm's `send`. The default OkHost answers offline; production swaps
23
+ * in a live host.
24
+ *
25
+ * VALUE IDENTITY. A drawn value must remember which result KEY filled it so
26
+ * the poller can refresh it live; a scaled/concatenated value must keep that
27
+ * key through the arithmetic. ipo_vm.py carries this on _Bound (a str
28
+ * subclass) and _Slot; JS has no str subclass, so values that carry identity
29
+ * are boxed as {__bound:true, ...} / {__slot:true, ...}. Bare strings/ints
30
+ * stay bare, so `typeof x === 'string'` still tests "a plain literal".
31
+ */
32
+
33
+ /**
34
+ * Variable scope of a `var`/`store` token: 0 = script globals. (The other
35
+ * scopes, 2 and 3, are the current frame's own slots -- see LOCAL.)
36
+ * @type {number}
37
+ */
38
+ const GLOBAL = 0;
39
+ /**
40
+ * Variable scope of a frame-local slot (scope 3 is treated the same by
41
+ * mkSlot: only "global or not" matters to a slot's identity).
42
+ * @type {number}
43
+ */
44
+ const LOCAL = 2;
45
+
46
+ /**
47
+ * Procref kinds pushed by a `procref` token: what the reference names.
48
+ * A local ref (IPO_REF_LOCAL) remembers the frame it was taken in; the
49
+ * others are indices into the exec's `byid` table by type.
50
+ */
51
+ const IPO_REF_LOCAL = 2;
52
+ const IPO_REF_SCREEN = 64;
53
+ const IPO_REF_MENU = 65;
54
+ const IPO_REF_STATE = 66;
55
+ const IPO_REF_STATE_ALT = 67;
56
+
57
+ /**
58
+ * An unset slot: reads as empty text but still names its slot, so a later
59
+ * draw can look up the Result* key bound to it. Mirrors ipo_vm.py's _Slot.
60
+ * @typedef {object} IpoSlot
61
+ * @property {true} __slot - box tag
62
+ * @property {number} sc - scope, GLOBAL or LOCAL
63
+ * @property {number} n - slot number
64
+ */
65
+
66
+ /**
67
+ * Text that remembers where it came from. Mirrors ipo_vm.py's _Bound (a str
68
+ * subclass); here it is a box and the display text is `s`.
69
+ * @typedef {object} IpoBound
70
+ * @property {true} __bound - box tag
71
+ * @property {string} s - the text
72
+ * @property {IpoSlot|null} slot - the slot it was built from
73
+ * @property {string|null} key - the Result* key bound to it
74
+ * @property {string[]} extra - other keys folded into it by concatenation
75
+ * @property {Record<string, string>|null} amap - a lookup table it came from
76
+ * (StrArrayRead), so a painter can map the value to a caption
77
+ */
78
+
79
+ /**
80
+ * A FLOAT, boxed. JS has one number type, but INPA distinguishes int from
81
+ * real and several builtins select arguments BY TYPE: analogout takes the
82
+ * first two *ints* as (row, col) and reads its min/max/warn bounds from the
83
+ * *floats* after them. An unboxed 120.0 is Number.isInteger-true and would
84
+ * be misread as a col. Pool doubles (tag 'd') are boxed so the int/float
85
+ * split survives; every numeric helper unwraps them.
86
+ * @typedef {object} IpoFloat
87
+ * @property {true} __float - box tag
88
+ * @property {number} v - the value
89
+ */
90
+
91
+ /**
92
+ * A procref on the stack: ['ref', kind, n, ownerFrame]. A local ref (kind
93
+ * IPO_REF_LOCAL) remembers the frame it was taken in, so a callee writing
94
+ * through its by-reference parameter lands in the CALLER's slot, not its own.
95
+ * @typedef {['ref', number, number, (Map<number, IpoValue>|null)]} IpoRef
96
+ */
97
+
98
+ /**
99
+ * Anything a VM slot or stack cell can hold.
100
+ * @typedef {string|number|boolean|null|IpoSlot|IpoBound|IpoFloat|IpoRef} IpoValue
101
+ */
102
+
103
+ /** A malformed program or a missing proc: the caller's mistake, not a halt. */
104
+ class IpoError extends Error {}
105
+
106
+ /**
107
+ * Control-flow stop thrown by the offline executor: a `state` yield, or the
108
+ * step budget. Never an error -- run() swallows it and returns the emissions.
109
+ */
110
+ class Halt extends Error {}
111
+
112
+ // ---------------------------------------------------------------- boxes --
113
+
114
+ /**
115
+ * Box an unset slot so its identity survives until a draw looks it up.
116
+ * @param {number} sc - scope of the slot
117
+ * @param {number} n - slot number
118
+ * @returns {IpoSlot}
119
+ */
120
+ function mkSlot(sc, n) {
121
+ return { __slot: true, sc: sc === LOCAL ? LOCAL : GLOBAL, n };
122
+ }
123
+
124
+ /**
125
+ * Is x an unset-slot box?
126
+ * @param {IpoValue} x - any VM value
127
+ * @returns {x is IpoSlot}
128
+ */
129
+ function isSlot(x) {
130
+ return x != null && x.__slot === true;
131
+ }
132
+
133
+ /**
134
+ * Box text that carries its origin.
135
+ * @param {IpoSlot|null|undefined} slot - the slot it was built from
136
+ * @param {*} text - display text (stringified; null reads as '')
137
+ * @param {string|null|undefined} key - the Result* key bound to it
138
+ * @returns {IpoBound}
139
+ */
140
+ function mkBound(slot, text, key) {
141
+ return {
142
+ __bound: true,
143
+ s: text == null ? '' : String(text),
144
+ slot: slot || null,
145
+ key: key || null,
146
+ extra: [],
147
+ amap: null,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Is x a bound-text box?
153
+ * @param {IpoValue} x - any VM value
154
+ * @returns {x is IpoBound}
155
+ */
156
+ function isBound(x) {
157
+ return x != null && x.__bound === true;
158
+ }
159
+
160
+ /**
161
+ * str(x): the display text of any VM value. A slot reads '', a bound value
162
+ * its text, a boxed float its number.
163
+ * @param {IpoValue} x - any VM value
164
+ * @returns {string}
165
+ */
166
+ function asStr(x) {
167
+ if (x == null) return '';
168
+ if (isBound(x)) return x.s;
169
+ if (isSlot(x)) return '';
170
+ if (isFloat(x)) return String(x.v);
171
+ return String(x);
172
+ }
173
+
174
+ /**
175
+ * Is x a plain string literal (not a bound box, not a slot)?
176
+ * @param {IpoValue} x - any VM value
177
+ * @returns {x is string}
178
+ */
179
+ function isPlainStr(x) {
180
+ return typeof x === 'string';
181
+ }
182
+
183
+ /**
184
+ * Is x a plain integer (an unboxed whole number)?
185
+ * @param {IpoValue} x - any VM value
186
+ * @returns {x is number}
187
+ */
188
+ function isPlainInt(x) {
189
+ return typeof x === 'number' && Number.isInteger(x);
190
+ }
191
+
192
+ /**
193
+ * Is x a procref pushed on the stack?
194
+ * @param {IpoValue} x - any VM value
195
+ * @returns {x is IpoRef}
196
+ */
197
+ function isRef(x) {
198
+ return Array.isArray(x) && x.length >= 3 && x[0] === 'ref';
199
+ }
200
+
201
+ /**
202
+ * Box a real so it keeps its float identity (see IpoFloat).
203
+ * @param {number} v - the value
204
+ * @returns {IpoFloat}
205
+ */
206
+ function mkFloat(v) {
207
+ return { __float: true, v };
208
+ }
209
+
210
+ /**
211
+ * Is x a boxed float?
212
+ * @param {IpoValue} x - any VM value
213
+ * @returns {x is IpoFloat}
214
+ */
215
+ function isFloat(x) {
216
+ return x != null && x.__float === true;
217
+ }
218
+
219
+ if (typeof module !== 'undefined' && module.exports) {
220
+ module.exports = {
221
+ GLOBAL,
222
+ LOCAL,
223
+ IPO_REF_LOCAL,
224
+ IPO_REF_SCREEN,
225
+ IPO_REF_MENU,
226
+ IPO_REF_STATE,
227
+ IPO_REF_STATE_ALT,
228
+ IpoError,
229
+ Halt,
230
+ mkSlot,
231
+ isSlot,
232
+ mkBound,
233
+ isBound,
234
+ asStr,
235
+ isPlainStr,
236
+ isPlainInt,
237
+ isRef,
238
+ mkFloat,
239
+ isFloat,
240
+ };
241
+ }