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,610 @@
1
+ /**
2
+ * @file Builtins that talk to the diagnostic API and the user: INPAapiJob and
3
+ * the INPAapiResult* reads, string arrays, binary results, the input
4
+ * dialogs, the component picker, files, timers and state-machine entry.
5
+ */
6
+
7
+ /** getinputstate's "confirmed" answer: the guard tests `== 0`. */
8
+ const IPO_INPUT_CONFIRMED = 0;
9
+
10
+ /** What an offline input stores: never sent live (see ipoDriveBuiltin). */
11
+ const IPO_INPUT_PLACEHOLDER = '0';
12
+
13
+ /**
14
+ * INPAapiJob(sgbd, job, arg): record the job, attribute it to the item, mark
15
+ * the per-line arg boundary, and run it through the host. Python's _b_job
16
+ * tests isinstance(x, str), TRUE for _Bound: sgbd/job/arg are frequently
17
+ * concatenations (a bound value), so accept those and read their text --
18
+ * otherwise the job arg (LWS5's coding block index, LAR;0x;0) and an
19
+ * empty-string sgbd are lost.
20
+ * @type {IpoBuiltin}
21
+ */
22
+ function bJob(vm, stack, item) {
23
+ const strv = (x) => (isPlainStr(x) ? x : isBound(x) ? x.s : null);
24
+ const sgbd = stack.length > 0 ? strv(stack[0]) : null;
25
+ const job = stack.length > 1 ? strv(stack[1]) : null;
26
+ const arg = stack.length > 2 ? strv(stack[2]) : null;
27
+ if (job == null) return;
28
+ /** @type {IpoJobRecord} */
29
+ const rec = { job, sgbd: sgbd == null ? null : sgbd };
30
+ if (arg && !vm.inputFed) rec.arg = arg;
31
+ vm.out.jobs.push(rec);
32
+ if (item && !('job' in item)) {
33
+ item.job = job;
34
+ if (rec.arg) item.jobArg = rec.arg;
35
+ }
36
+ // Per-line arg boundary (per-wheel loop draws one line per job arg).
37
+ if (rec.arg) {
38
+ let last = vm.out.lines.length
39
+ ? vm.out.lines[vm.out.lines.length - 1]
40
+ : null;
41
+ const drawn = !!(last && last.elements && last.elements.length);
42
+ if (drawn && last.jobArg != null && last.jobArg !== rec.arg) {
43
+ vm.out.lines.push({ label: null, elements: [] });
44
+ last = vm.out.lines[vm.out.lines.length - 1];
45
+ }
46
+ if (last && !('jobArg' in last)) last.jobArg = rec.arg;
47
+ }
48
+ vm.globals.set('__last_job__', vm.host.job(sgbd, job, arg, null));
49
+ }
50
+
51
+ /**
52
+ * INPAapiFsMode: the item switches the fault-read mode.
53
+ * @type {IpoBuiltin}
54
+ */
55
+ function bFsmode(vm, stack, item) {
56
+ if (item) item.faultRead = true;
57
+ }
58
+
59
+ /**
60
+ * INPAapiCheckJobStatus: the host's verdict on the last job.
61
+ * @type {IpoBuiltin}
62
+ */
63
+ function bCheckStatus(vm) {
64
+ vm.globals.set('__status__', vm.host.status());
65
+ }
66
+
67
+ /**
68
+ * INPAapiResult*(->dest, KEY, set[, format]): read a result key into the last
69
+ * out-ref, bound to the key; a two-ref form (INP1api...) gets rc = 1 in the
70
+ * first. The set number is the one numeric argument (a literal or a bound
71
+ * variable); the format is a string.
72
+ * @param {IpoVm} vm - the running VM
73
+ * @param {IpoValue[]} stack - the call's arguments
74
+ * @param {IpoItem|null} item - the item being executed
75
+ * @param {boolean} [integer] - read as a number (INPAapiResultInt)
76
+ * @returns {void}
77
+ */
78
+ function bResult(vm, stack, item, integer) {
79
+ const key = stack.find(isPlainStr) || null;
80
+ const setArg = stack.find((x) => !isRef(x) && !isPlainStr(x));
81
+ const set = setArg != null ? Math.trunc(num(setArg)) : undefined;
82
+ let val = key ? vm.host.result(key, { integer, set }) : '';
83
+ const refs = stack.filter(isRef);
84
+ if (refs.length >= 2) storeOut(vm, [refs[0]], 1);
85
+ const dest = refs.length ? refs[refs.length - 1] : null;
86
+ if (dest != null) {
87
+ const { dsc: sc, n, map } = vm._refTarget(dest, vm.frame);
88
+ if (key) {
89
+ val = mkBound(mkSlot(sc, n), val == null ? '' : asStr(val), key);
90
+ vm.setBind(sc, n, key);
91
+ }
92
+ vm.globals.set(`ref:${n}`, val);
93
+ map.set(n, val);
94
+ }
95
+ if (key) {
96
+ vm.globals.set('__lastkey__', key);
97
+ vm.out.reads.push(key);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * INPAapiResultInt: bResult reading a number.
103
+ * @type {IpoBuiltin}
104
+ */
105
+ function bResultInt(vm, stack, item) {
106
+ bResult(vm, stack, item, true);
107
+ }
108
+
109
+ /**
110
+ * INP1apiErrorCode: no error.
111
+ * @type {IpoBuiltin}
112
+ */
113
+ function bErrorCode(vm, stack) {
114
+ storeOut(vm, stack, 0);
115
+ }
116
+
117
+ /**
118
+ * INP1apiErrorText: no error text.
119
+ * @type {IpoBuiltin}
120
+ */
121
+ function bErrorText(vm, stack) {
122
+ storeOut(vm, stack, '');
123
+ }
124
+
125
+ /**
126
+ * INPAapiResultSets(->n) / INP1apiResultSets(->rc, ->n): how many sets the
127
+ * job produced -- the wire's own count when a feed carries it (a fault list
128
+ * is one set per fault plus the JOB_STATUS set: INPA subtracts one and
129
+ * loops); 1 for the offline hosts.
130
+ * @type {IpoBuiltin}
131
+ */
132
+ function bResultSets(vm, stack) {
133
+ const refs = stack.filter(isRef);
134
+ if (refs.length >= 2) storeOut(vm, [refs[0]], 1);
135
+ const n =
136
+ vm.host && typeof vm.host.count === 'function' ? vm.host.count() : 1;
137
+ storeOut(vm, refs.length ? [refs[refs.length - 1]] : [], n);
138
+ }
139
+
140
+ /**
141
+ * StrArrayCreate(->rc, ->handle): a fresh string array.
142
+ * @type {IpoBuiltin}
143
+ */
144
+ function bStrArrayCreate(vm, stack) {
145
+ const refs = stack.filter(isRef);
146
+ vm._arrN += 1;
147
+ vm.strArrays.set(vm._arrN, new Map());
148
+ if (refs.length >= 2) storeOut(vm, [refs[0]], 1);
149
+ storeOut(vm, refs.length ? [refs[refs.length - 1]] : [], vm._arrN);
150
+ }
151
+
152
+ /**
153
+ * StrArrayWrite(handle, index, text).
154
+ * @type {IpoBuiltin}
155
+ */
156
+ function bStrArrayWrite(vm, stack) {
157
+ // StrArrayWrite(array, index, text): the text is usually computed (a
158
+ // module's name + its fault count), not a literal
159
+ const args = stack.filter((x) => !isRef(x));
160
+ if (args.length < 3) return;
161
+ const id = Math.trunc(num(args[0]));
162
+ const idx = Math.trunc(num(args[1]));
163
+ const txt = asStr(args[2]);
164
+ if (vm.strArrays.has(id)) vm.strArrays.get(id).set(idx, txt);
165
+ }
166
+
167
+ /**
168
+ * StrArrayRead(handle, index, ->text): the entry, bound to the index's key
169
+ * and carrying the whole array as a lookup map so a painter can caption the
170
+ * value.
171
+ * @type {IpoBuiltin}
172
+ */
173
+ function bStrArrayRead(vm, stack) {
174
+ const vals = stack.filter((x) => !isRef(x));
175
+ const arr = vals.length
176
+ ? vm.strArrays.get(Math.trunc(num(vals[0]))) || new Map()
177
+ : new Map();
178
+ const text = vals.length > 1 ? arr.get(Math.trunc(num(vals[1]))) || '' : '';
179
+ const out = mkBound(null, text, keyed(stack));
180
+ const amap = {};
181
+ for (const [k, v] of arr) amap[String(k)] = v;
182
+ out.amap = Object.keys(amap).length ? amap : null;
183
+ storeOut(vm, stack, out, out.key);
184
+ }
185
+
186
+ /**
187
+ * INPAapiResultBinary(KEY): a coding image; the next store (or
188
+ * GetBinaryDataString) binds to the key.
189
+ * @type {IpoBuiltin}
190
+ */
191
+ /**
192
+ * builtin_90(array, &n): how many entries a string array holds. The
193
+ * whole-vehicle scripts fill an array with the modules that answered with
194
+ * faults and walk it `for (i = 0; i < n; i++)` to write the short overview
195
+ * of the protocol, so n must be the real count, not a slot.
196
+ * @param {Best2Vm} vm
197
+ * @param {*[]} stack
198
+ */
199
+ function bStrArraySize(vm, stack) {
200
+ const vals = stack.filter((x) => !isRef(x));
201
+ const arr = vals.length
202
+ ? vm.strArrays.get(Math.trunc(num(vals[0]))) || new Map()
203
+ : new Map();
204
+ storeOut(vm, stack, arr.size);
205
+ }
206
+
207
+ function bResultBinary(vm, stack) {
208
+ // INPAapiResultBinary(&rc, key, set): the key is a literal or a variable
209
+ // holding one (protokoll_hexcode passes its parameter), the set a number
210
+ let key = null;
211
+ let set = null;
212
+ for (const a of stack) {
213
+ if (isRef(a)) continue;
214
+ const s = asStr(a);
215
+ if (key == null && /^[A-Za-z_]/.test(s)) key = s;
216
+ else if (set == null && s !== '' && Number.isFinite(Number(s)))
217
+ set = Math.trunc(Number(s));
218
+ }
219
+ if (key) {
220
+ vm.globals.set('__pending_binary__', key);
221
+ // the set it was asked of, for the live read behind GetBinaryDataString
222
+ vm.globals.set('__pending_binary_set__', set);
223
+ vm.out.reads.push(key);
224
+ }
225
+ // rc: the script prints "????" for a code it could not read; live that
226
+ // is a result the wire did not carry, offline every read succeeds
227
+ const refs = stack.filter(isRef);
228
+ if (refs.length) {
229
+ const have =
230
+ !vm.wireJobs ||
231
+ (key &&
232
+ vm.host &&
233
+ typeof vm.host.raw === 'function' &&
234
+ vm.host.raw(key, { set: set == null ? undefined : set }) != null);
235
+ storeOut(vm, [refs[0]], have ? 1 : 0);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * A binary result as the hex text INPA's GetBinaryDataString hands the
241
+ * script ("27C3"): bytes from a typed array, a plain array, or the object
242
+ * a typed array turns into through JSON; a string with its separators
243
+ * dropped ("27-C3", "0x27C3"); a number as four digits.
244
+ * @param {*} v - the result value
245
+ * @returns {string}
246
+ */
247
+ function ipoBinaryHex(v) {
248
+ if (v == null || v === '') return '';
249
+ const byte = (b) =>
250
+ (Number(b) & 0xff).toString(16).toUpperCase().padStart(2, '0');
251
+ if (Array.isArray(v) || ArrayBuffer.isView(v))
252
+ return Array.from(v, byte).join('');
253
+ if (typeof v === 'number')
254
+ return Number.isFinite(v)
255
+ ? (v >>> 0).toString(16).toUpperCase().padStart(4, '0')
256
+ : '';
257
+ if (typeof v === 'object') {
258
+ const keys = Object.keys(v).filter((k) => /^\d+$/.test(k));
259
+ if (keys.length)
260
+ return keys
261
+ .sort((a, b) => a - b)
262
+ .map((k) => byte(v[k]))
263
+ .join('');
264
+ }
265
+ return String(v)
266
+ .replace(/^0x/i, '')
267
+ .replace(/[^0-9a-fA-F]/g, '')
268
+ .toUpperCase();
269
+ }
270
+
271
+ /**
272
+ * GetBinaryDataString(->dst, ->src, ...): the destination takes the source's
273
+ * binding (or the pending binary key) so a sliced display stays bound.
274
+ * @type {IpoBuiltin}
275
+ */
276
+ function bGetBinaryDataString(vm, stack) {
277
+ const refs = stack.filter(isRef);
278
+ if (refs.length < 2) return;
279
+ const dst = refs[0],
280
+ src = refs[1];
281
+ // a live run: GetBinaryDataString(&text, &length) gives the script the
282
+ // bytes of the last INPAapiResultBinary as hex text, and their length in
283
+ // characters (E46.IPO's protokoll_hexcode walks it two characters a byte)
284
+ if (vm.wireJobs) {
285
+ const key = vm.globals.get('__pending_binary__');
286
+ const set = vm.globals.get('__pending_binary_set__');
287
+ if (key && vm.host && typeof vm.host.raw === 'function') {
288
+ const hex = ipoBinaryHex(
289
+ vm.host.raw(key, { set: set == null ? undefined : set })
290
+ );
291
+ vm.globals.delete('__pending_binary__');
292
+ vm.globals.delete('__pending_binary_set__');
293
+ storeOut(vm, [dst], mkBound(null, hex, key), key);
294
+ storeOut(vm, [src], hex.length);
295
+ return;
296
+ }
297
+ }
298
+ const dsc = dst[1] === IPO_REF_LOCAL && vm.frame != null ? LOCAL : GLOBAL;
299
+ const ssc = src[1] === IPO_REF_LOCAL && vm.frame != null ? LOCAL : GLOBAL;
300
+ let key = vm.bindKey(ssc, src[2]);
301
+ if (!key) {
302
+ key = vm.globals.get('__pending_binary__');
303
+ vm.globals.delete('__pending_binary__');
304
+ }
305
+ if (!key) return;
306
+ const val = mkBound(mkSlot(dsc, dst[2]), '0', key);
307
+ vm.setBind(dsc, dst[2], key);
308
+ if (dsc === GLOBAL) vm.globals.set(dst[2], val);
309
+ else if (vm.frame) vm.frame.set(dst[2], val);
310
+ }
311
+
312
+ /**
313
+ * getinputstate(->state): when the state machine is being DRIVEN (a resume
314
+ * fed the user's on/off), that answer is the input state; the guard tests
315
+ * `== 0`, so a valid pick is 0. Offline it falls to the host's placeholder.
316
+ * @type {IpoBuiltin}
317
+ */
318
+ function bGetInputState(vm, stack) {
319
+ const iv = vm._driveInput;
320
+ storeOut(vm, stack, iv != null ? iv : vm.host.inputstate());
321
+ }
322
+
323
+ /**
324
+ * builtin_16 = togglelist. Offline a noop (the picked row is runtime-only,
325
+ * so the diff harness sees nothing). When DRIVEN, it writes the user's
326
+ * picked component (an ORT key from the BITS table) into its out-var,
327
+ * exactly as the widget does at runtime -- so the INPAapiJob that reads that
328
+ * slot sends the pick as its argument (STEUERN_DIGITAL <ORT>).
329
+ * @type {IpoBuiltin}
330
+ */
331
+ function bToggleList(vm, stack) {
332
+ if (vm._pickInput == null) return;
333
+ storeOut(vm, stack, vm._pickInput);
334
+ }
335
+
336
+ /**
337
+ * bmweb_pick(what, arg, ->choice): BMWeb's own picker builtin (home script).
338
+ * Offline a noop; driven, the renderer parks the script (suspensions.js
339
+ * 'pick'), asks the host for the list named by `what`, and resumes with the
340
+ * user's choice, which lands in the out-string; a cancelled pick leaves it
341
+ * as it was, and the script's own `if (choice != "")` is the guard.
342
+ * @type {IpoBuiltin}
343
+ */
344
+ function bBmwebPick(vm, stack) {
345
+ if (vm._pickInput == null) return;
346
+ storeOut(vm, stack, asStr(vm._pickInput));
347
+ }
348
+
349
+ /**
350
+ * bmweb_status(->text): one line from the host (cable state, version) for
351
+ * the home screen. The host hangs it on the VM as `homeStatus`; without one
352
+ * the line is empty, which the screen tolerates.
353
+ * @type {IpoBuiltin}
354
+ */
355
+ function bBmwebStatus(vm, stack) {
356
+ const table = typeof IPO_HOME_HOST !== 'undefined' ? IPO_HOME_HOST : null;
357
+ const fn = (table && table.status) || (vm.host && vm.host.homeStatus);
358
+ let text = '';
359
+ try {
360
+ text = typeof fn === 'function' ? String(fn() || '') : '';
361
+ } catch (e) {
362
+ text = '';
363
+ }
364
+ storeOut(vm, stack, text);
365
+ }
366
+
367
+ /**
368
+ * The input family (inputint, inputhex, input2int, ...): record the prompts
369
+ * on the item, store the placeholder through every out-ref, and taint the
370
+ * next job (mirrors Python _b_input: a job sent after an ask has no static
371
+ * arg).
372
+ * @type {IpoBuiltin}
373
+ */
374
+ function bInput(vm, stack, item) {
375
+ const prompts = stack.filter((x) => isPlainStr(x) && x.trim());
376
+ if (item && prompts.length) {
377
+ const have = item.prompt || (item.prompt = []);
378
+ for (const p of prompts) if (!have.includes(p)) have.push(p);
379
+ }
380
+ for (const ref of stack.filter(isRef))
381
+ storeOut(vm, [ref], IPO_INPUT_PLACEHOLDER);
382
+ vm.inputFed = true;
383
+ }
384
+
385
+ /**
386
+ * inputdigital: capture+taint via bInput, then the CONFIRMING choice as the
387
+ * placeholder -- the lift keeps the yes-branch's job (mirrors
388
+ * _b_inputdigital).
389
+ * @type {IpoBuiltin}
390
+ */
391
+ function bInputDigital(vm, stack, item) {
392
+ bInput(vm, stack, item);
393
+ for (const ref of stack.filter(isRef)) storeOut(vm, [ref], 1);
394
+ }
395
+
396
+ /**
397
+ * fileopen(path..., mode): open an in-memory file; the path is every
398
+ * string/slot argument that is not the mode letter.
399
+ * @type {IpoBuiltin}
400
+ */
401
+ function bFileopen(vm, stack) {
402
+ // fileopen(path, mode): the path is usually computed (folder + name +
403
+ // extension), so take whatever string value the expression produced
404
+ const args = stack.filter((x) => !isRef(x));
405
+ let mode = 'r';
406
+ const last = args.length > 1 ? asStr(args[args.length - 1]) : '';
407
+ if (['r', 'w', 'a'].includes(last)) {
408
+ mode = last;
409
+ args.pop();
410
+ }
411
+ const path = args.map(asStr).join('');
412
+ if (mode === 'w') vm.files.set(path, []);
413
+ else if (mode === 'a' && !vm.files.has(path)) vm.files.set(path, []);
414
+ if (mode !== 'r') vm.lastWritten = path;
415
+ vm.fh = { path, mode, line: 0 };
416
+ }
417
+
418
+ /**
419
+ * fileclose.
420
+ * @type {IpoBuiltin}
421
+ */
422
+ function bFileclose(vm) {
423
+ vm.fh = null;
424
+ }
425
+
426
+ /**
427
+ * viewopen(path): INPA opens the file the script just wrote in its viewer
428
+ * window (the whole-vehicle fault protocol). The runtime shows the same
429
+ * text as the screen and prints it as the sheet.
430
+ * @param {Best2Vm} vm
431
+ * @param {*[]} stack
432
+ */
433
+ function bViewopen(vm, stack) {
434
+ // viewopen(path, title): the path is computed (folder + name + extension),
435
+ // the title is the window caption
436
+ const args = stack.filter((x) => !isRef(x)).map(asStr);
437
+ let path = args[0] || '';
438
+ if (!vm.files.has(path) && vm.lastWritten && vm.files.has(vm.lastWritten))
439
+ path = vm.lastWritten;
440
+ vm.out.view = {
441
+ path,
442
+ title: args.length > 1 ? args[args.length - 1] : '',
443
+ lines: [...(vm.files.get(path) || [])],
444
+ };
445
+ }
446
+
447
+ /**
448
+ * filewrite(text): append a line to the open file.
449
+ * @type {IpoBuiltin}
450
+ */
451
+ function bFilewrite(vm, stack) {
452
+ if (!vm.fh || vm.fh.mode === 'r') return;
453
+ // the line is usually computed (caption + value), not a literal
454
+ const args = stack.filter((x) => !isRef(x));
455
+ const txt = args.length ? asStr(args[0]) : '';
456
+ if (!vm.files.has(vm.fh.path)) vm.files.set(vm.fh.path, []);
457
+ vm.files.get(vm.fh.path).push(txt);
458
+ }
459
+
460
+ /**
461
+ * fileread(->line): the next line of the open file, '' at its end.
462
+ * @type {IpoBuiltin}
463
+ */
464
+ function bFileread(vm, stack) {
465
+ // fileread(&line, &status): status 0 while a line came, else the end --
466
+ // the save keys copy the protocol with `while (status == 0)`
467
+ let line = '';
468
+ let status = 1;
469
+ if (vm.fh && vm.fh.mode === 'r') {
470
+ const lines = vm.files.get(vm.fh.path) || [];
471
+ if (vm.fh.line < lines.length) {
472
+ line = lines[vm.fh.line];
473
+ vm.fh.line += 1;
474
+ status = 0;
475
+ }
476
+ }
477
+ const refs = stack.filter(isRef);
478
+ if (refs.length) storeOut(vm, [refs[0]], line);
479
+ if (refs.length > 1) storeOut(vm, [refs[1]], status);
480
+ }
481
+
482
+ /**
483
+ * settimer(n, ms): arm one of INPA's per-script timer slots. A screen INIT
484
+ * arms one and a LINE tests it every cycle; without them the test never
485
+ * fires and a timed refresh runs on every tick instead.
486
+ * @type {IpoBuiltin}
487
+ */
488
+ function bSettimer(vm, stack) {
489
+ const ints = allInts(stack);
490
+ if (ints.length < 2) return;
491
+ if (!vm.timers) vm.timers = new Map();
492
+ const now = typeof vm.now === 'function' ? vm.now() : Date.now();
493
+ vm.timers.set(ints[0], now + Math.max(0, ints[1]));
494
+ }
495
+
496
+ /**
497
+ * testtimer(n, ->expired): 1 once the timer's deadline has passed.
498
+ * @type {IpoBuiltin}
499
+ */
500
+ function bTesttimer(vm, stack) {
501
+ const ints = allInts(stack);
502
+ const n = ints.length ? ints[0] : 0;
503
+ const now = typeof vm.now === 'function' ? vm.now() : Date.now();
504
+ const due = vm.timers && vm.timers.has(n) ? vm.timers.get(n) : null;
505
+ const expired = due != null && now >= due ? 1 : 0;
506
+ storeOut(vm, stack, expired);
507
+ }
508
+
509
+ /**
510
+ * setstate/start(&sm): enter a state machine. LIVE the machine is a guided
511
+ * procedure (togglelist -> job -> parks): running it through the offline
512
+ * executor here stopped at its first %STATE and the driver never saw the
513
+ * picker, so it is emitted for the driver to run. Offline it runs to its
514
+ * first yield and the job it fired is attributed to the item (the simple
515
+ * case; the togglelist/fixed-job recovery of ipo_vm.py's _b_setstate is a
516
+ * refinement not needed here).
517
+ * @type {IpoBuiltin}
518
+ */
519
+ /**
520
+ * callstatemachine(sm): run another machine and come back to this one
521
+ * when it calls returnstatemachine(). The runtime enters the callee like
522
+ * a setstatemachine; the caller's name waits on a stack for the return.
523
+ * @type {IpoBuiltin}
524
+ */
525
+ function bCallStatemachine(vm, stack, item) {
526
+ if (!vm.machineReturn) vm.machineReturn = [];
527
+ vm.machineReturn.push(vm.currentMachine || null);
528
+ bSetstate(vm, stack, item);
529
+ }
530
+
531
+ /**
532
+ * returnstatemachine(): back to the machine that called this one, when
533
+ * one did; otherwise the machine simply ends.
534
+ * @type {IpoBuiltin}
535
+ */
536
+ function bReturnStatemachine(vm) {
537
+ const back = vm.machineReturn ? vm.machineReturn.pop() : null;
538
+ if (back && vm.procs[back] && vm.wireJobs) vm.out.stateEnter = back;
539
+ }
540
+
541
+ /**
542
+ * An interface the browser has no counterpart for -- the factory line's
543
+ * PLC (SPS*), the order system (ELDI*), the test management (DTM*, PEM*):
544
+ * every out-parameter reads 0 / "", so the script takes the path it has
545
+ * for "not available" instead of running on with unset slots.
546
+ * @type {IpoBuiltin}
547
+ */
548
+ function bUnavailable(vm, stack) {
549
+ for (const r of stack.filter(isRef)) storeOut(vm, [r], 0);
550
+ }
551
+
552
+ function bSetstate(vm, stack, item) {
553
+ const ref = stack.find(
554
+ (x) => isRef(x) && (x[1] === IPO_REF_STATE || x[1] === IPO_REF_STATE_ALT)
555
+ );
556
+ if (ref == null) return;
557
+ const name = vm.byid('state', ref[2]);
558
+ if (!name || !vm.procs[name]) return;
559
+ if (item) item.stateEnter = name;
560
+ if (vm.wireJobs) {
561
+ vm.out.stateEnter = name;
562
+ return;
563
+ }
564
+ if (vm.entered.has(name)) return;
565
+ vm.entered.add(name);
566
+ const before = vm.out.jobs.length;
567
+ try {
568
+ vm._exec(vm.procs[name], new Map());
569
+ } catch (e) {
570
+ if (!(e instanceof Halt)) throw e;
571
+ }
572
+ if (item && !('job' in item) && vm.out.jobs.length > before) {
573
+ const j = vm.out.jobs[before];
574
+ item.job = j.job;
575
+ item.stateJob = true;
576
+ if (j.arg && !item.jobArg) item.jobArg = j.arg;
577
+ }
578
+ }
579
+
580
+ if (typeof module !== 'undefined' && module.exports) {
581
+ module.exports = {
582
+ IPO_INPUT_CONFIRMED,
583
+ bBmwebPick,
584
+ bBmwebStatus,
585
+ bJob,
586
+ bFsmode,
587
+ bCheckStatus,
588
+ bResult,
589
+ bResultInt,
590
+ bErrorCode,
591
+ bErrorText,
592
+ bResultSets,
593
+ bStrArrayCreate,
594
+ bStrArrayWrite,
595
+ bStrArrayRead,
596
+ bResultBinary,
597
+ bGetBinaryDataString,
598
+ bGetInputState,
599
+ bToggleList,
600
+ bInput,
601
+ bInputDigital,
602
+ bFileopen,
603
+ bFileclose,
604
+ bFilewrite,
605
+ bFileread,
606
+ bSettimer,
607
+ bTesttimer,
608
+ bSetstate,
609
+ };
610
+ }