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,52 @@
1
+ /**
2
+ * @file The VM's public surface. Loaded two ways: as the last of the
3
+ * bestvm/ <script> tags in the app (publishes the globals other files
4
+ * call) and via require() in the verify suite (module.exports). Both must
5
+ * work from this one file.
6
+ *
7
+ * Load order, which index.html and the node requires below both follow:
8
+ * write-guard.js isWriteJob, the classifier the guard rests on
9
+ * codec.js Best2Codec, text/number/wire-parameter encodings
10
+ * machine.js Best2Vm, VmError, the run loop and result publishing
11
+ * registers.js the register file (extends the prototype)
12
+ * operands.js operand addressing modes (extends the prototype)
13
+ * environment.js arguments, pool, tables (extends the prototype)
14
+ * executor.js the opcode `step` switch (extends the prototype)
15
+ */
16
+
17
+ if (typeof require === 'function' && typeof module !== 'undefined') {
18
+ Object.assign(
19
+ globalThis,
20
+ require('./write-guard.js'),
21
+ require('./codec.js'),
22
+ require('./machine.js'),
23
+ require('./registers.js'),
24
+ require('./operands.js'),
25
+ require('./environment.js'),
26
+ require('./executor.js')
27
+ );
28
+ }
29
+
30
+ if (typeof window !== 'undefined') {
31
+ window.Best2Vm = Best2Vm;
32
+ window.VmError = VmError;
33
+ window.isWriteJob = isWriteJob;
34
+ }
35
+ if (typeof module !== 'undefined' && module.exports) {
36
+ module.exports = {
37
+ Best2Vm,
38
+ VmError,
39
+ Best2Codec,
40
+ STOP,
41
+ JUMP_TESTS,
42
+ REG_BYTES,
43
+ isWriteJob,
44
+ // the classifier's parts, exported so test_write_gate.js
45
+ // can compare each against its Python twin in
46
+ // tools/verify/sgbd_bulk_verify.py pattern-by-pattern
47
+ READ_TOKEN,
48
+ CONFIG_READ_TOKEN,
49
+ WRITE_TOKEN,
50
+ INFO_READ_TOKEN,
51
+ };
52
+ }
@@ -0,0 +1,491 @@
1
+ /**
2
+ * @file BEST2 virtual machine: EXECUTE ECU job programs in the browser.
3
+ *
4
+ * Lifting a job to a declarative spec tops out near 71% of results (the tail
5
+ * is structural: branch-chosen layouts, multi-telegram streaming, byte-by-byte
6
+ * strings); executing the program handles all of it, which is why EDIABAS is
7
+ * flawless. Input is tools/sgbd_code.py output (ops array, jumps as indices);
8
+ * telegram I/O is a callback, so one VM runs live cable / .sim / fixture.
9
+ * Semantics ported from vendored EdiabasLib (EdOperations.cs, EdiabasNet.cs),
10
+ * the engine tools/sgbd_bulk_verify.py diffs against and test_bestvm.js checks.
11
+ *
12
+ * THE REGISTER MODEL, which nothing else here makes sense without:
13
+ * B/I/L/A are VIEWS over one 32-byte array, LITTLE-endian within a view, so
14
+ * writing B0 changes what L0 reads. S registers are separate byte buffers
15
+ * (raw bytes that may contain NULs). F are doubles.
16
+ *
17
+ * This piece declares the class -- its state, the session/job run loop and
18
+ * result publishing. The pieces loaded after it extend the prototype, one
19
+ * concern each: registers.js (the register file), operands.js (operand
20
+ * addressing modes), environment.js (arguments, constant pool, tables),
21
+ * executor.js (the opcode `step` switch). index.js publishes the surface.
22
+ */
23
+
24
+ // Under node the pieces are separate modules and read each other's names
25
+ // as globals -- the same shape the browser's shared script scope gives them.
26
+ if (typeof require === 'function' && typeof module !== 'undefined') {
27
+ Object.assign(globalThis, require('./codec.js'), require('./write-guard.js'));
28
+ }
29
+
30
+ /**
31
+ * The register file is 32 bytes, overlaid THREE ways (EdiabasNet
32
+ * RegisterList): B0..BF = bytes 0..15 and A0..AF = bytes 16..31; I0..I7
33
+ * pair over the B range and I8..IF over the A range; L0..L3 quad over B
34
+ * and L4..L7 over A. So L1 IS bytes 4..7 IS I2+I3 IS B4..B7.
35
+ */
36
+ const REG_BYTES = 32;
37
+
38
+ /**
39
+ * The job's declared array size (ArrayMaxBufSize): the capacity of every
40
+ * string register. 1024 is EDIABAS's default and every E46 job fits it.
41
+ */
42
+ const DEFAULT_ARRAY_SIZE = 1024;
43
+
44
+ /** Opcodes a single job may execute before the VM gives up on it. */
45
+ const DEFAULT_MAX_STEPS = 2_000_000;
46
+
47
+ /**
48
+ * Values of the trap register, which `jt`/`jnt` test:
49
+ * TRAP_CLEAN no error pending
50
+ * TRAP_UNMAPPED an error with no mapped bit (EDIABAS_BIP_0001/0007)
51
+ * TRAP_FLOAT EDIABAS_BIP_0011, an infinite or NaN float quotient
52
+ * TRAP_TABLE EDIABAS_BIP_0010, table not found (SYS_0002 folds here)
53
+ * TRAP_USER the floor of the user-trap range set by `sett`/`generr`
54
+ * Mapped EDIABAS errors occupy 2..29; `jt target, 32` aliases the unmapped
55
+ * bit 0.
56
+ */
57
+ const TRAP_CLEAN = -1;
58
+ const TRAP_UNMAPPED = 0;
59
+ const TRAP_FLOAT = 8;
60
+ const TRAP_TABLE = 10;
61
+ const TRAP_USER = 0x40000000;
62
+
63
+ /** Returned by `step` for `eoj`: the job is finished. */
64
+ const STOP = Symbol('eoj');
65
+
66
+ /** An error raised by the VM itself (not by the ECU or the transport). */
67
+ class VmError extends Error {}
68
+
69
+ /**
70
+ * A parsed SGBD program, as tools/sgbd_code.py writes it.
71
+ * @typedef {Object} JobCode
72
+ * @property {Object<string, number>} jobs - Job name -> index into `ops` of
73
+ * its first instruction.
74
+ * @property {Array<[string, Operand[]]>} ops - Instructions: opcode name and
75
+ * its operands.
76
+ * @property {Array<string|number[]>} strings - The constant pool: a byte
77
+ * ARRAY for an exact literal (possibly containing NULs), a plain string
78
+ * for a result or table name.
79
+ */
80
+
81
+ /**
82
+ * One instruction operand, `[mode, ...payload]` as sgbd_code.py emits it.
83
+ * See operands.js (OpMode) for the modes and their payloads.
84
+ * @typedef {Array<*>} Operand
85
+ */
86
+
87
+ /**
88
+ * The ECU exchange: request bytes out, answer bytes back. Callers that
89
+ * replay captured answers ignore the second argument.
90
+ * @callback TelegramSink
91
+ * @param {number[]} request - The telegram to transmit.
92
+ * @param {?import('./codec.js').CommParams} comm - Wire parameters for this
93
+ * exchange (framing, checksum, pacing), or null before any `xsetpar`.
94
+ * @returns {Uint8Array|number[]|undefined} The answer frame off the wire.
95
+ */
96
+
97
+ /**
98
+ * A table row: column name -> cell text.
99
+ * @typedef {Object<string, string>} TableRow
100
+ */
101
+
102
+ /**
103
+ * One completed result set: UPPERCASED result name -> value (number, text,
104
+ * or a byte array for `ergy`).
105
+ * @typedef {Object<string, number|string|number[]>} ResultSet
106
+ */
107
+
108
+ /**
109
+ * The condition flags the arithmetic opcodes set and the jumps test.
110
+ * @typedef {Object} VmFlags
111
+ * @property {boolean} zero
112
+ * @property {boolean} sign
113
+ * @property {boolean} carry
114
+ * @property {boolean} overflow
115
+ * @property {boolean} tested
116
+ */
117
+
118
+ /**
119
+ * The table cursor set by `tabset` and moved by `tabseek`/`tabline`.
120
+ * @typedef {Object} TableCursor
121
+ * @property {string} name - The table name as the job asked for it.
122
+ * @property {TableRow[]} rows - The data rows.
123
+ * @property {?TableRow} row - The current row, null before any seek.
124
+ */
125
+
126
+ /**
127
+ * A string register: a FIXED-CAPACITY buffer plus a logical length, exactly
128
+ * like EdiabasNet's StringData.
129
+ * @typedef {Object} StringRegister
130
+ * @property {Uint8Array} buf - The whole buffer, `arraySize` bytes.
131
+ * @property {number} len - The logical length.
132
+ */
133
+
134
+ /**
135
+ * Construction options.
136
+ * @typedef {Object} VmOptions
137
+ * @property {TelegramSink} [send] - The ECU exchange. Required to transmit.
138
+ * @property {Object<string, TableRow[]>} [tables] - The SGBD's own tables.
139
+ * @property {Object<string, Object<string, TableRow[]>>} [extTables] -
140
+ * Tables in OTHER best files, reached by `tabsetex "Name", "file"` --
141
+ * group SGBDs pull ZuordnungsTabelle from t_grtb this way (128 of the
142
+ * 249 groups in data/groups). Keyed by the bare file name.
143
+ * @property {string} [args] - Job arguments, ';' separated.
144
+ * @property {number} [maxSteps] - Step budget per job.
145
+ * @property {number} [arraySize] - String register capacity.
146
+ * @property {Map<string, Uint8Array>} [shared] - Process-wide shared data
147
+ * (shmset/shmget), persists across jobs.
148
+ * @property {boolean} [inited] - INITIALISIERUNG already ran this session.
149
+ * @property {boolean} [allowWrites] - Permit a write job to transmit.
150
+ * @property {?import('./codec.js').CommParams} [comm] - Wire parameters
151
+ * carried over from the session's INITIALISIERUNG.
152
+ * @property {?Date} [now] - A fixed clock for `date`/`time`/`ticks`.
153
+ */
154
+
155
+ /**
156
+ * The BEST2 job interpreter. One instance runs one SGBD; `run` executes a
157
+ * job and returns its result sets.
158
+ */
159
+ class Best2Vm {
160
+ /**
161
+ * @param {JobCode} code - Parsed sgbd JSON from tools/sgbd_code.py.
162
+ * @param {VmOptions} [opts] - Construction options.
163
+ */
164
+ constructor(code, opts = {}) {
165
+ this.code = code;
166
+ this.send =
167
+ opts.send ||
168
+ (() => {
169
+ throw new VmError('no telegram sink');
170
+ });
171
+ this.tables = opts.tables || {};
172
+ this.extTables = opts.extTables || {};
173
+ this.argText = opts.args || '';
174
+ this.maxSteps = opts.maxSteps || DEFAULT_MAX_STEPS;
175
+ this.arraySize = opts.arraySize || DEFAULT_ARRAY_SIZE;
176
+ // process-wide shared data (shmset/shmget), persists across jobs
177
+ this.shared = opts.shared || new Map();
178
+ // A SESSION runs INITIALISIERUNG once when the SGBD is loaded, not once
179
+ // per job. Callers that keep a session (webshim) pass inited:true on
180
+ // every job after the first, so the implicit init below is skipped --
181
+ // which is both what the engine does and one less telegram per job.
182
+ this._inited = !!opts.inited;
183
+ // Permission to transmit for a job that CHANGES the ECU. Off by
184
+ // default: a caller has to say so, and saying so is the point where a
185
+ // UI can put a confirmation in front of the user.
186
+ // WRITES ARE PERMITTED BY DEFAULT (owner's decision, 2026-08-19).
187
+ //
188
+ // This used to default to false, so actuator tests -- STEUERN_E_LUEFTER
189
+ // and friends -- never reached the wire: the fan screen's "Activate at
190
+ // 15%" appeared to do nothing while the readback sat at the DME's own 92.
191
+ // The classifier cannot tell a temporary actuator drive from a permanent
192
+ // EEPROM write (both are "write jobs"), so unblocking one unblocks both.
193
+ //
194
+ // What that means in practice: CODIERDATEN_SCHREIBEN, FS_LOESCHEN and the
195
+ // FLASH_* family now transmit. Those are unrecoverable on a real module.
196
+ // Pass {allowWrites: false} to restore the old refuse-everything behaviour.
197
+ this.allowWrites = opts.allowWrites !== false;
198
+ // Wire parameters from xsetpar. Seeded from the SESSION: xsetpar lives
199
+ // in INITIALISIERUNG, which runs once per session -- a fresh VM for a
200
+ // later job never executes it, so the caller carries comm forward the
201
+ // same way it carries `shared`. Without this seed every ordinary job
202
+ // transmitted with comm=null, i.e. BMW-FAST 115200 8N1, and every
203
+ // K-line module got line noise.
204
+ this.comm = opts.comm || null;
205
+ // A fixed clock for date/time, when the caller needs determinism.
206
+ // webshim re-runs a job's bytecode once per telegram fetched, and the
207
+ // answer memo is keyed on request bytes -- a timestamp that ticks
208
+ // between passes changes the bytes, misses the memo, and re-transmits
209
+ // a telegram that already went out.
210
+ this.now = opts.now || null;
211
+ }
212
+
213
+ /**
214
+ * Job-start state. Per the reference: a job start clears the stack,
215
+ * flags, string registers, results and traps. It does NOT clear
216
+ * byte/float registers, and shared data is process-wide -- so neither
217
+ * is reset here.
218
+ * @returns {void}
219
+ */
220
+ reset() {
221
+ this.regBuf = this.regBuf || new Uint8Array(REG_BYTES);
222
+ /** @type {Map<string, StringRegister>} */
223
+ this.sregs = new Map();
224
+ /** @type {Map<string, number>} */
225
+ this.fregs = new Map();
226
+ /** @type {number[]} BYTE stack (push writes N bytes) */
227
+ this.stack = [];
228
+ /** @type {VmFlags} */
229
+ this.flags = {
230
+ zero: false,
231
+ sign: false,
232
+ carry: false,
233
+ overflow: false,
234
+ tested: false,
235
+ };
236
+ /** @type {ResultSet[]} completed result sets */
237
+ this.results = [];
238
+ /** @type {Map<string, number|string|number[]>} the set being built */
239
+ this.cur = new Map();
240
+ /** @type {?Set<string>} etag filter, null = everything */
241
+ this.wanted = null;
242
+ /** @type {?TableCursor} */
243
+ this.table = null;
244
+ // The trap register: -1 = clean, 0 = an error with no mapped bit,
245
+ // 2..29 = a mapped EDIABAS error (BIP_0010 -> 10 is the table error),
246
+ // >= 0x40000000 = a user trap from `sett`. jt/jnt test THIS, not a
247
+ // generic "tested" flag -- a tabset that SUCCEEDS must leave it clean,
248
+ // and mine left a stale flag so `jt err,#10` fired after a good tabset
249
+ // and 31 jobs reported ERROR_TABLE.
250
+ this.trapBit = TRAP_CLEAN;
251
+ this.trapMask = 0; // set_trap_mask (settmr/gettmr), see OpSettmr
252
+ this.answer = new Uint8Array(0);
253
+ this.tokenSep = ''; // setspc separators for stoken
254
+ this.tokenIdx = 0; // 1-based token number, 0 = unset
255
+ // this.comm is deliberately NOT cleared: xsetpar runs in
256
+ // INITIALISIERUNG, and a job start that wiped it sent every subsequent
257
+ // telegram with default (BMW-FAST) framing. Comm lives as long as the
258
+ // VM / session, like shared data.
259
+ this.steps = 0;
260
+ }
261
+
262
+ // ---- the loop -------------------------------------------------------
263
+
264
+ /**
265
+ * Run a job the way a SESSION does: EDIABAS executes the SGBD's
266
+ * INITIALISIERUNG job once before the first real job (ExecuteInitJob),
267
+ * and SGBDs use it to populate shared data that later jobs read -- MS450's
268
+ * AIF block size and free count arrive that way via shmset/shmget. Without
269
+ * it those results read zeros.
270
+ * @param {string} jobName - The job to run.
271
+ * @param {string} [args] - Its ';'-separated arguments; defaults to the
272
+ * constructor's `args`.
273
+ * @returns {ResultSet[]} The job's result sets, in order.
274
+ * @throws {VmError} A write job without `allowWrites`, a failed or
275
+ * unproven INITIALISIERUNG, an unknown job, or any execution fault.
276
+ */
277
+ run(jobName, args) {
278
+ // Refuse a write job BEFORE anything is transmitted -- including the
279
+ // implicit INITIALISIERUNG, which is itself only a read but still puts
280
+ // bytes on the wire. "Nothing was sent" is a much easier promise to
281
+ // reason about than "only harmless things were sent".
282
+ if (isWriteJob(jobName) && !this.allowWrites) {
283
+ throw new VmError(
284
+ `refusing to run write job ${jobName}: ` +
285
+ 'construct the VM with {allowWrites: true} to permit it'
286
+ );
287
+ }
288
+ const init = this.code.jobs.INITIALISIERUNG;
289
+ if (
290
+ init !== undefined &&
291
+ !this._inited &&
292
+ String(jobName).toUpperCase() !== 'INITIALISIERUNG'
293
+ ) {
294
+ this._inited = true;
295
+ // The init runs with NO arguments, but the real job's argument may
296
+ // already be sitting in argText (constructed with {args}); runOne(init,
297
+ // '') overwrote it and FS_LESEN_DETAIL then saw no F_CODE at all.
298
+ const jobArgs = args !== undefined ? args : this.argText;
299
+ let initSets;
300
+ try {
301
+ initSets = this.runOne(init, '');
302
+ } catch (e) {
303
+ // An init that fails FAILS THE JOB. ExecuteInitJob rethrows any
304
+ // exception and closes the SGBD to force a reload -- it does not
305
+ // shrug and continue. Swallowing here converted every loud init
306
+ // failure (unimplemented opcode, step limit, eerr) into a job that
307
+ // "succeeded" with empty shared data and published zeros as OKAY.
308
+ // The needAnswer sentinel (webshim fetching a telegram answer) also
309
+ // rethrows, and both paths clear _inited so init runs again on the
310
+ // next attempt.
311
+ this._inited = false;
312
+ throw e;
313
+ }
314
+ // ExecuteInitJob also demands the init PROVE itself: result set 1
315
+ // (our set 0; the engine's set 0 is synthetic) must carry DONE=1, or
316
+ // the engine reports EDIABAS_SYS_0010 and unloads the SGBD.
317
+ const done =
318
+ initSets && initSets.length && Number(initSets[0].DONE) === 1;
319
+ if (!done) {
320
+ this._inited = false;
321
+ throw new VmError(
322
+ 'INITIALISIERUNG did not report DONE=1 (EDIABAS_SYS_0010)'
323
+ );
324
+ }
325
+ this.argText = jobArgs;
326
+ }
327
+ return this.runOne(undefined, args, jobName);
328
+ }
329
+
330
+ /**
331
+ * Execute one job's bytecode from its entry to `eoj`, with no session
332
+ * bookkeeping. `run` is the entry point callers want.
333
+ * @param {number|undefined} entryIdx - Index into `ops` to start at, or
334
+ * undefined to look `jobName` up in the job table.
335
+ * @param {string|undefined} args - Arguments for this run; undefined
336
+ * keeps the current `argText`.
337
+ * @param {string} [jobName] - The job name (required when entryIdx is
338
+ * undefined); also what the write guard classifies.
339
+ * @returns {ResultSet[]} The result sets the job published.
340
+ * @throws {VmError} No such job, the step limit, or any execution fault.
341
+ */
342
+ runOne(entryIdx, args, jobName) {
343
+ const entry =
344
+ entryIdx !== undefined
345
+ ? entryIdx
346
+ : (this.code.jobs[jobName] ?? this.code.jobs[jobName?.toUpperCase()]);
347
+ if (entry === undefined) throw new VmError(`no job ${jobName}`);
348
+ this.jobName = jobName || this.jobName;
349
+ // INITIALISIERUNG runs implicitly before a real job; it is never a
350
+ // write, and must not inherit the target job's classification.
351
+ this.writeJob = entryIdx !== undefined ? false : isWriteJob(jobName);
352
+ this.reset();
353
+ if (args !== undefined) this.argText = args;
354
+ this.argBytes = Best2Codec.strBytes(this.argText);
355
+ this._args = undefined;
356
+ let pc = entry;
357
+ const ops = this.code.ops;
358
+ while (pc >= 0 && pc < ops.length) {
359
+ if (++this.steps > this.maxSteps) {
360
+ throw new VmError(`step limit at op ${pc}`);
361
+ }
362
+ const [name, a] = ops[pc];
363
+ const next = this.step(name, a, pc);
364
+ if (next === STOP) break;
365
+ pc = next === undefined ? pc + 1 : next;
366
+ }
367
+ this.flush();
368
+ return this.results;
369
+ }
370
+
371
+ /**
372
+ * Close the result set being built, if it holds anything.
373
+ * @returns {void}
374
+ */
375
+ flush() {
376
+ if (this.cur.size) {
377
+ this.results.push(Object.fromEntries(this.cur));
378
+ this.cur = new Map();
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Publish a result into the current set. The KEY IS UPPERCASED
384
+ * (SetResultData keys _resultDict on Name.ToUpper), so ZKE5's
385
+ * "STAT_IFFHMax_WERT" is published as STAT_IFFHMAX_WERT -- the values were
386
+ * already right, only the key case differed, and a caller looking up the
387
+ * engine's name found nothing. Last write wins, as the engine's dictionary
388
+ * assignment does. A `wanted` filter drops names outside it, except the
389
+ * JOB_* status results.
390
+ * @param {string} name - The result name.
391
+ * @param {number|string|number[]} value - The value to publish.
392
+ * @returns {void}
393
+ */
394
+ emit(name, value) {
395
+ const key = String(name).toUpperCase();
396
+ if (this.wanted && !this.wanted.has(key) && !key.startsWith('JOB_')) {
397
+ return;
398
+ }
399
+ this.cur.set(key, value);
400
+ }
401
+
402
+ // ---- codecs, re-exposed for callers that address them on the class ----
403
+
404
+ /**
405
+ * Text -> CP1252 bytes. See Best2Codec.strBytes.
406
+ * @param {*} s - The value to encode.
407
+ * @returns {Uint8Array} The bytes.
408
+ */
409
+ static strBytes(s) {
410
+ return Best2Codec.strBytes(s);
411
+ }
412
+
413
+ /**
414
+ * Text -> CP1252 bytes. See Best2Codec.strBytesCp1252.
415
+ * @param {string} str - The text.
416
+ * @returns {Uint8Array} The bytes.
417
+ */
418
+ static strBytesCp1252(str) {
419
+ return Best2Codec.strBytesCp1252(str);
420
+ }
421
+
422
+ /**
423
+ * CP1252 bytes -> text. See Best2Codec.bytesStr.
424
+ * @param {Uint8Array|number[]} b - The bytes.
425
+ * @returns {string} The text.
426
+ */
427
+ static bytesStr(b) {
428
+ return Best2Codec.bytesStr(b);
429
+ }
430
+
431
+ /**
432
+ * NUL-terminated text. See Best2Codec.cstr.
433
+ * @param {Uint8Array|number[]} b - The bytes.
434
+ * @returns {string} The text before the first NUL.
435
+ */
436
+ static cstr(b) {
437
+ return Best2Codec.cstr(b);
438
+ }
439
+
440
+ /**
441
+ * EDIABAS's StringToValue. See Best2Codec.strToValue.
442
+ * @param {*} s - The text.
443
+ * @returns {number} The integer, or 0.
444
+ */
445
+ static strToValue(s) {
446
+ return Best2Codec.strToValue(s);
447
+ }
448
+
449
+ /**
450
+ * A float constant. See Best2Codec.parseNum.
451
+ * @param {*} s - The text.
452
+ * @returns {number} The value, or 0.
453
+ */
454
+ static parseNum(s) {
455
+ return Best2Codec.parseNum(s);
456
+ }
457
+
458
+ /**
459
+ * The engine's float formatting. See Best2Codec.fltText.
460
+ * @param {number} value - The float.
461
+ * @returns {string} Its text.
462
+ */
463
+ static fltText(value) {
464
+ return Best2Codec.fltText(value);
465
+ }
466
+
467
+ /**
468
+ * set_communication_pars decoding. See Best2Codec.decodeCommParams.
469
+ * @param {number[]} words - The CommParameter words.
470
+ * @returns {import('./codec.js').CommParams} The wire parameters.
471
+ */
472
+ static decodeCommParams(words) {
473
+ return Best2Codec.decodeCommParams(words);
474
+ }
475
+ }
476
+
477
+ if (typeof module !== 'undefined' && module.exports) {
478
+ module.exports = {
479
+ Best2Vm,
480
+ VmError,
481
+ STOP,
482
+ REG_BYTES,
483
+ DEFAULT_ARRAY_SIZE,
484
+ DEFAULT_MAX_STEPS,
485
+ TRAP_CLEAN,
486
+ TRAP_UNMAPPED,
487
+ TRAP_FLOAT,
488
+ TRAP_TABLE,
489
+ TRAP_USER,
490
+ };
491
+ }