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.
- package/LICENSE +674 -0
- package/README.md +351 -0
- package/dist/bmweb.js +2790 -0
- package/package.json +53 -0
- package/runtime/core/bestvm/codec.js +285 -0
- package/runtime/core/bestvm/environment.js +116 -0
- package/runtime/core/bestvm/executor.js +1483 -0
- package/runtime/core/bestvm/index.js +52 -0
- package/runtime/core/bestvm/machine.js +491 -0
- package/runtime/core/bestvm/operands.js +356 -0
- package/runtime/core/bestvm/registers.js +152 -0
- package/runtime/core/bestvm/write-guard.js +111 -0
- package/runtime/core/ipofile/compile.js +364 -0
- package/runtime/core/ipofile/decls.js +187 -0
- package/runtime/core/ipofile/emit.js +708 -0
- package/runtime/core/ipofile/exec.js +164 -0
- package/runtime/core/ipofile/lex.js +243 -0
- package/runtime/core/ipofile/parse.js +550 -0
- package/runtime/core/ipofile/pool.js +404 -0
- package/runtime/core/ipofile/walk.js +431 -0
- package/runtime/core/ipovm/builtin-helpers.js +182 -0
- package/runtime/core/ipovm/builtins-api.js +610 -0
- package/runtime/core/ipovm/builtins-screen.js +493 -0
- package/runtime/core/ipovm/builtins-table.js +166 -0
- package/runtime/core/ipovm/builtins-text.js +166 -0
- package/runtime/core/ipovm/emissions.js +138 -0
- package/runtime/core/ipovm/hosts.js +191 -0
- package/runtime/core/ipovm/operators.js +229 -0
- package/runtime/core/ipovm/structures.js +250 -0
- package/runtime/core/ipovm/suspensions.js +241 -0
- package/runtime/core/ipovm/tape.js +206 -0
- package/runtime/core/ipovm/values.js +241 -0
- package/runtime/core/ipovm/vm.js +1166 -0
- package/runtime/core/translate.js +526 -0
- package/runtime/core/webshim/api-router.js +592 -0
- package/runtime/core/webshim/bus.js +95 -0
- package/runtime/core/webshim/coding.js +82 -0
- package/runtime/core/webshim/data-fetch.js +66 -0
- package/runtime/core/webshim/exchange.js +288 -0
- package/runtime/core/webshim/framing.js +331 -0
- package/runtime/core/webshim/install.js +30 -0
- package/runtime/core/webshim/job-runner.js +319 -0
- package/runtime/core/webshim/native-bus.js +108 -0
- package/runtime/core/webshim/timers.js +82 -0
- package/runtime/core/webshim/trace.js +205 -0
- package/runtime/core/webshim/transport-base.js +128 -0
- package/runtime/core/webshim/variant-resolver.js +249 -0
- package/runtime/core/webshim/web-serial-bus.js +734 -0
- package/runtime/home/bmweb-home.ips +76 -0
- package/runtime/home/bmweb.h +26 -0
- package/runtime/screens/activations.js +258 -0
- package/runtime/screens/garage/diff.js +331 -0
- package/runtime/screens/garage/share.js +276 -0
- package/runtime/screens/garage/store.js +547 -0
- package/runtime/screens/ipo-runtime/cells.js +176 -0
- package/runtime/screens/ipo-runtime/dialogs.js +254 -0
- package/runtime/screens/ipo-runtime/home.js +358 -0
- package/runtime/screens/ipo-runtime/open.js +393 -0
- package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
- package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
- package/runtime/screens/ipo-runtime/print.js +281 -0
- package/runtime/screens/ipo-runtime/program.js +1337 -0
- package/runtime/screens/ipo-runtime/protocol.js +464 -0
- package/runtime/screens/ipo-runtime/script-scan.js +225 -0
- package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
- package/runtime/screens/ipo-runtime/ui.js +249 -0
- package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
- package/runtime/screens/ir.js +324 -0
- package/runtime/screens/search/data.js +153 -0
- package/runtime/screens/search/match.js +285 -0
- package/runtime/screens/search/open.js +66 -0
- 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
|
+
}
|