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,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
|
+
}
|