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,356 @@
1
+ /**
2
+ * @file Operand addressing modes: how an instruction's `[mode, ...payload]`
3
+ * operand is read as a number, as bytes, as text or as a float, how it is
4
+ * written back, and how wide it is. Extends Best2Vm (machine.js).
5
+ */
6
+
7
+ if (typeof require === 'function' && typeof module !== 'undefined') {
8
+ Object.assign(globalThis, require('./machine.js'));
9
+ }
10
+
11
+ /**
12
+ * Operand addressing modes, numbered as sgbd_code.py emits them (the
13
+ * engine's OpAddrMode order), with the payload each carries after the mode:
14
+ * REG_* [name] a register by name (S/B/A/I/L/F)
15
+ * IMM8/16/32 [value] an immediate; IMM32 also holds
16
+ * jump targets (an ops index)
17
+ * IMM_STR [poolIndex] a constant-pool entry
18
+ * IDX_IMM [reg, idx] reg[#idx], one byte
19
+ * IDX_REG [reg, idxReg] reg[idxReg]
20
+ * IDX_REG_IMM [reg, idxReg, off] reg[idxReg + #off]
21
+ * IDX_*_LEN_* [reg, idx, len] a byte range; idx and len are
22
+ * each an immediate or a register
23
+ * name as the mode says
24
+ */
25
+ const OpMode = Object.freeze({
26
+ NONE: 0,
27
+ REG_S: 1,
28
+ REG_AB: 2,
29
+ REG_I: 3,
30
+ REG_L: 4,
31
+ IMM8: 5,
32
+ IMM16: 6,
33
+ IMM32: 7,
34
+ IMM_STR: 8,
35
+ IDX_IMM: 9,
36
+ IDX_REG: 10,
37
+ IDX_REG_IMM: 11,
38
+ IDX_IMM_LEN_IMM: 12,
39
+ IDX_IMM_LEN_REG: 13,
40
+ IDX_REG_LEN_IMM: 14,
41
+ IDX_REG_LEN_REG: 15,
42
+ });
43
+
44
+ /**
45
+ * Is the mode a plain register (REG_S..REG_L)?
46
+ * @param {number} m - The operand mode.
47
+ * @returns {boolean}
48
+ */
49
+ function opIsReg(m) {
50
+ return m >= OpMode.REG_S && m <= OpMode.REG_L;
51
+ }
52
+
53
+ /**
54
+ * Is the mode a numeric immediate (IMM8..IMM32)?
55
+ * @param {number} m - The operand mode.
56
+ * @returns {boolean}
57
+ */
58
+ function opIsImm(m) {
59
+ return m >= OpMode.IMM8 && m <= OpMode.IMM32;
60
+ }
61
+
62
+ /**
63
+ * Is the mode a single indexed byte (IDX_IMM..IDX_REG_IMM)?
64
+ * @param {number} m - The operand mode.
65
+ * @returns {boolean}
66
+ */
67
+ function opIsIndexed(m) {
68
+ return m >= OpMode.IDX_IMM && m <= OpMode.IDX_REG_IMM;
69
+ }
70
+
71
+ /**
72
+ * Is the mode a byte range (the four IDX_*_LEN_* forms)?
73
+ * @param {number} m - The operand mode.
74
+ * @returns {boolean}
75
+ */
76
+ function opIsRange(m) {
77
+ return m >= OpMode.IDX_IMM_LEN_IMM && m <= OpMode.IDX_REG_LEN_REG;
78
+ }
79
+
80
+ /**
81
+ * Is the operand a whole string register (`S0`, not `S0[i]`)?
82
+ * @param {import('./machine.js').Operand} op - The operand.
83
+ * @returns {boolean}
84
+ */
85
+ function opIsStringReg(op) {
86
+ return opIsReg(op[0]) && !!op[1] && op[1][0] === 'S';
87
+ }
88
+
89
+ /**
90
+ * Is the operand a numeric register (B/A/I/L/F by name)? These are the
91
+ * operands `mult` and `div` write their second result (high half,
92
+ * remainder) back into.
93
+ * @param {import('./machine.js').Operand|undefined} op - The operand.
94
+ * @returns {boolean}
95
+ */
96
+ function opIsNumReg(op) {
97
+ return !!op && opIsReg(op[0]) && String(op[1])[0] !== 'S';
98
+ }
99
+
100
+ Object.assign(Best2Vm.prototype, {
101
+ /**
102
+ * The index of an indexed or ranged operand: an immediate for the
103
+ * `IDX_IMM*` modes, else the named register's value.
104
+ * @param {number} mode - The operand mode.
105
+ * @param {number|string} a - The index payload (number or register name).
106
+ * @returns {number} The index.
107
+ */
108
+ resolveIdx(mode, a) {
109
+ // ranged/indexed modes name either an immediate index or a register
110
+ if (
111
+ mode === OpMode.IDX_IMM ||
112
+ mode === OpMode.IDX_IMM_LEN_IMM ||
113
+ mode === OpMode.IDX_IMM_LEN_REG
114
+ ) {
115
+ return a; // imm
116
+ }
117
+ return this.getReg(a); // reg
118
+ },
119
+
120
+ /**
121
+ * The length of a ranged operand: an immediate for the `*_LEN_IMM`
122
+ * modes, else the named register's value.
123
+ * @param {number} mode - The operand mode.
124
+ * @param {number|string} a - The length payload (number or register name).
125
+ * @returns {number} The length.
126
+ */
127
+ resolveLen(mode, a) {
128
+ if (mode === OpMode.IDX_IMM_LEN_IMM || mode === OpMode.IDX_REG_LEN_IMM) {
129
+ return a; // imm
130
+ }
131
+ return this.getReg(a);
132
+ },
133
+
134
+ /**
135
+ * Numeric read. `width` is the DESTINATION's width, and for byte-array
136
+ * sources it decides how many bytes are folded -- Operand.GetValueData
137
+ * takes dataLen from the caller and assembles that many bytes
138
+ * LITTLE-endian, zero-padding when the slice is short.
139
+ *
140
+ * This matters far beyond arithmetic: `move I2, S2[B2]` reads TWO bytes
141
+ * of the response into I2. Reading one byte made every response-length
142
+ * guard of the form `move I2,S2[B2] / and / comp I5,I4` compare the wrong
143
+ * number, so jobs reported ERROR_ECU_INCORRECT_LEN and emitted nothing.
144
+ * @param {import('./machine.js').Operand} op - The operand.
145
+ * @param {number} [width] - Bytes to fold for byte-array sources; the
146
+ * whole source when omitted (one byte for an indexed operand).
147
+ * @returns {number} The unsigned value.
148
+ * @throws {VmError} A mode that has no value.
149
+ */
150
+ val(op, width) {
151
+ const [m, a, b, c] = op;
152
+ if (opIsImm(m)) return a;
153
+ if (m === OpMode.IMM_STR) return 0; // a string literal as number
154
+ if (opIsReg(m)) {
155
+ if (a[0] === 'S') {
156
+ const buf = this.getS(a);
157
+ return Best2Codec.leValue(buf, 0, width || buf.length);
158
+ }
159
+ return this.getReg(a);
160
+ }
161
+ if (opIsIndexed(m)) {
162
+ const buf = this.getSraw(a); // complete buffer, stale included
163
+ let i = m === OpMode.IDX_IMM ? b : this.getReg(b);
164
+ if (m === OpMode.IDX_REG_IMM) i += c || 0;
165
+ return Best2Codec.leValue(buf, i, width || 1);
166
+ }
167
+ if (opIsRange(m)) {
168
+ const buf = this.bytes(op);
169
+ return Best2Codec.leValue(buf, 0, width || buf.length);
170
+ }
171
+ throw new VmError(`operand mode ${m} as value`);
172
+ },
173
+
174
+ /**
175
+ * Byte-array read: string registers, indexed bytes, ranges, pool
176
+ * literals, or an immediate's low byte.
177
+ * @param {import('./machine.js').Operand} op - The operand.
178
+ * @returns {Uint8Array} The bytes (a view or a copy; callers treat it as
179
+ * read-only).
180
+ * @throws {VmError} A mode that has no bytes.
181
+ */
182
+ bytes(op) {
183
+ const [m, a, b, c] = op;
184
+ if (m === OpMode.IMM_STR) {
185
+ // a pool entry is either a byte ARRAY (an exact literal, possibly
186
+ // containing NULs) or a plain string (a result/table name)
187
+ const lit = this.code.strings[a];
188
+ return Array.isArray(lit)
189
+ ? Uint8Array.from(lit)
190
+ : Best2Codec.strBytes(lit ?? '');
191
+ }
192
+ if (opIsReg(m)) {
193
+ if (a[0] === 'S') return this.getS(a);
194
+ const span = Best2Vm.regSpan(a);
195
+ return this.regBuf.slice(span[0], span[0] + span[1]);
196
+ }
197
+ if (m === OpMode.IDX_IMM || m === OpMode.IDX_REG) {
198
+ const buf = this.getSraw(a);
199
+ const i = m === OpMode.IDX_IMM ? b : this.getReg(b);
200
+ return i < buf.length ? buf.slice(i, i + 1) : new Uint8Array(0);
201
+ }
202
+ if (opIsRange(m)) {
203
+ const buf = this.getSraw(a);
204
+ const i = this.resolveIdx(m, b);
205
+ const n = this.resolveLen(m, c);
206
+ // reads past the current length yield what exists, not an error --
207
+ // the engine's Operand does the same, and jobs rely on it
208
+ return buf.slice(i, i + Math.max(0, n));
209
+ }
210
+ if (opIsImm(m)) {
211
+ return Uint8Array.from([a & 0xff]);
212
+ }
213
+ throw new VmError(`operand mode ${m} as bytes`);
214
+ },
215
+
216
+ /**
217
+ * Write back to an operand: a register, an indexed byte, or a range.
218
+ * @param {import('./machine.js').Operand} op - The destination operand.
219
+ * @param {number|Uint8Array|number[]} value - The value: bytes when
220
+ * `asBytes`, else a number.
221
+ * @param {boolean} [asBytes] - Store `value` as bytes rather than as a
222
+ * number.
223
+ * @returns {void}
224
+ * @throws {VmError} A mode that cannot be written.
225
+ */
226
+ store(op, value, asBytes) {
227
+ const [m, a, b, c] = op;
228
+ if (opIsReg(m)) {
229
+ if (asBytes) {
230
+ if (a[0] === 'S') {
231
+ this.setS(a, value);
232
+ return;
233
+ }
234
+ const span = Best2Vm.regSpan(a);
235
+ for (let i = 0; i < span[1]; i++) {
236
+ this.regBuf[span[0] + i] = i < value.length ? value[i] : 0;
237
+ }
238
+ return;
239
+ }
240
+ this.setReg(a, value);
241
+ return;
242
+ }
243
+ if (opIsIndexed(m)) {
244
+ let i = m === OpMode.IDX_IMM ? b : this.getReg(b);
245
+ if (m === OpMode.IDX_REG_IMM) i += c || 0;
246
+ const d = this.sd(a);
247
+ if (i >= d.buf.length) return; // over capacity: no write
248
+ // a number lands as ONE byte: an indexed destination is a byte wide
249
+ const src = asBytes ? value : Uint8Array.from([Number(value) & 0xff]);
250
+ for (let k = 0; k < src.length && i + k < d.buf.length; k++) {
251
+ d.buf[i + k] = src[k];
252
+ }
253
+ d.len = Math.max(d.len, i + src.length); // grows, never shrinks
254
+ return;
255
+ }
256
+ if (opIsRange(m)) {
257
+ const i = this.resolveIdx(m, b);
258
+ const n = this.resolveLen(m, c);
259
+ const src = asBytes ? value : Uint8Array.from([Number(value) & 0xff]);
260
+ const d = this.sd(a);
261
+ for (let k = 0; k < n && i + k < d.buf.length; k++) {
262
+ d.buf[i + k] = k < src.length ? src[k] : 0;
263
+ }
264
+ d.len = Math.max(d.len, Math.min(i + Math.max(0, n), d.buf.length));
265
+ return;
266
+ }
267
+ throw new VmError(`operand mode ${m} as destination`);
268
+ },
269
+
270
+ /**
271
+ * Store TEXT into a string register the way Operand.SetStringData does:
272
+ * the bytes PLUS one appended NUL when non-empty (an empty string stores a
273
+ * zero-length array with no NUL). This is observable, not cosmetic --
274
+ * `scmp` is byte-exact, and the compiler's literals carry the terminator
275
+ * ("6\0"), so a stored "6" without one never matched and MS420's VANOS
276
+ * jobs fell through to ERROR_FUNCTION_*.
277
+ * @param {import('./machine.js').Operand} op - The destination operand.
278
+ * @param {*} txt - The text to store.
279
+ * @returns {void}
280
+ */
281
+ storeText(op, txt) {
282
+ const b = Best2Codec.strBytes(String(txt ?? ''));
283
+ if (b.length === 0) {
284
+ this.store(op, b, true);
285
+ return;
286
+ }
287
+ const out = new Uint8Array(b.length + 1);
288
+ out.set(b);
289
+ this.store(op, out, true);
290
+ },
291
+
292
+ /**
293
+ * THE width rule: GetArgsValueLength returns arg0.GetDataLen(TRUE) and
294
+ * ignores arg1 entirely, so every arithmetic/move width comes from the
295
+ * DESTINATION in write mode. write=true is what makes a plain indexed
296
+ * destination (S0[i]) exactly one byte wide.
297
+ * @param {import('./machine.js').Operand} op - The operand.
298
+ * @returns {number} Its width in bytes (0 for a mode without one).
299
+ */
300
+ widthOf(op) {
301
+ const m = op[0];
302
+ if (opIsReg(m)) {
303
+ const r = op[1];
304
+ if (r[0] === 'S') return this.getS(r).length;
305
+ const span = Best2Vm.regSpan(r);
306
+ return span ? span[1] : 4;
307
+ }
308
+ if (m === OpMode.IMM8) return 1;
309
+ if (m === OpMode.IMM16) return 2;
310
+ if (m === OpMode.IMM32) return 4;
311
+ if (m === OpMode.IMM_STR) return this.bytes(op).length;
312
+ if (opIsIndexed(m)) return 1; // write mode
313
+ if (opIsRange(m)) return this.bytes(op).length;
314
+ return 0;
315
+ },
316
+
317
+ /**
318
+ * An operand as TEXT (arg.GetStringData()): a pool literal as its
319
+ * NUL-terminated text, anything else as the NUL-terminated text of its
320
+ * bytes. This is how result names, table names, column names and string
321
+ * comparands are read -- and a name can equally be a REGISTER: MS420's
322
+ * VANOS jobs name a result from a register still holding response bytes,
323
+ * and the engine faithfully publishes the resulting garbage key.
324
+ * @param {import('./machine.js').Operand} op - The operand.
325
+ * @returns {string} The text.
326
+ */
327
+ textOf(op) {
328
+ if (op[0] === OpMode.IMM_STR) return this.lit(op[1]);
329
+ return Best2Codec.cstr(this.bytes(op));
330
+ },
331
+
332
+ /**
333
+ * An operand as a FLOAT (arg.GetFloatData()): a pool literal parses as a
334
+ * number, an F register reads its double, anything else its numeric
335
+ * value.
336
+ * @param {import('./machine.js').Operand} op - The operand.
337
+ * @returns {number} The value.
338
+ */
339
+ floatOf(op) {
340
+ if (op[0] === OpMode.IMM_STR) return Best2Codec.parseNum(this.lit(op[1]));
341
+ if (op[1] && String(op[1])[0] === 'F') return this.getReg(op[1]);
342
+ return this.val(op);
343
+ },
344
+ });
345
+
346
+ if (typeof module !== 'undefined' && module.exports) {
347
+ module.exports = {
348
+ OpMode,
349
+ opIsReg,
350
+ opIsImm,
351
+ opIsIndexed,
352
+ opIsRange,
353
+ opIsStringReg,
354
+ opIsNumReg,
355
+ };
356
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @file The register file: the 32-byte B/I/L/A overlay, the F doubles and
3
+ * the fixed-capacity S string registers. Extends Best2Vm (machine.js).
4
+ */
5
+
6
+ if (typeof require === 'function' && typeof module !== 'undefined') {
7
+ Object.assign(globalThis, require('./machine.js'));
8
+ }
9
+
10
+ /** Byte index where the A registers start in the register file. */
11
+ const A_REG_OFFSET = 16;
12
+
13
+ /**
14
+ * Where a numeric register lives in the 32-byte file.
15
+ * @param {string} name - A register name: B0..BF, A0..AF, I0..IF, L0..L7.
16
+ * @returns {?[number, number]} `[byteOffset, width]`, or null for a name
17
+ * that is not a numeric register.
18
+ */
19
+ Best2Vm.regSpan = function regSpan(name) {
20
+ const kind = name[0];
21
+ const idx = parseInt(name.slice(1), 16);
22
+ if (!Number.isFinite(idx)) return null;
23
+ // A registers are byte registers at index+16, NOT a wider type
24
+ if (kind === 'B') return [idx, 1];
25
+ if (kind === 'A') return [A_REG_OFFSET + idx, 1];
26
+ if (kind === 'I') return [idx * 2, 2];
27
+ if (kind === 'L') return [idx * 4, 4];
28
+ return null;
29
+ };
30
+
31
+ Object.assign(Best2Vm.prototype, {
32
+ /**
33
+ * Read a register by name: a float, the logical bytes of a string
34
+ * register, or a numeric register's unsigned value.
35
+ * @param {string} name - The register name.
36
+ * @returns {number|Uint8Array} The value (bytes for an S register).
37
+ * @throws {VmError} An unknown register name.
38
+ */
39
+ getReg(name) {
40
+ if (name[0] === 'F') return this.fregs.get(name) || 0;
41
+ if (name[0] === 'S') return this.getS(name);
42
+ const span = Best2Vm.regSpan(name);
43
+ if (!span) throw new VmError(`unknown register ${name}`);
44
+ // LITTLE-endian within the view: byte 0 is the LOW byte
45
+ // (Register.GetValueData -- reg[off] + reg[off+1]<<8 + ...). Reading
46
+ // these big-endian made `move B0,x` show up as x*256 in I0, so a
47
+ // one-byte flag published as 256.
48
+ return Best2Codec.leValue(this.regBuf, span[0], span[1]);
49
+ },
50
+
51
+ /**
52
+ * Write a register by name. A numeric register takes the value truncated
53
+ * to an integer, two's complement in its own width, low byte first.
54
+ * @param {string} name - The register name.
55
+ * @param {number|Uint8Array|number[]} value - The value (bytes for an S
56
+ * register).
57
+ * @returns {void}
58
+ * @throws {VmError} An unknown register name.
59
+ */
60
+ setReg(name, value) {
61
+ if (name[0] === 'F') {
62
+ this.fregs.set(name, value);
63
+ return;
64
+ }
65
+ if (name[0] === 'S') {
66
+ this.setS(name, value);
67
+ return;
68
+ }
69
+ const span = Best2Vm.regSpan(name);
70
+ if (!span) throw new VmError(`unknown register ${name}`);
71
+ let v = Math.trunc(Number(value));
72
+ if (v < 0) v += 2 ** (8 * span[1]); // two's complement in-width
73
+ this.regBuf.set(Best2Codec.leBytes(v, span[1]), span[0]);
74
+ },
75
+
76
+ /**
77
+ * The string register record, created on first use. A string register is
78
+ * a FIXED-CAPACITY buffer plus a logical length, exactly like EdiabasNet's
79
+ * StringData -- not a JS array that shrinks. The distinction is
80
+ * observable: `clear` zeroes the length but reads at an index past it
81
+ * still see whatever bytes are in the buffer (Operand.GetRawData uses
82
+ * GetArrayData(TRUE), the complete buffer), and MS450's IDENT publishes
83
+ * ID_SG_ADR from exactly such a stale byte.
84
+ * @param {string} name - The S register name.
85
+ * @returns {import('./machine.js').StringRegister} The register record.
86
+ */
87
+ sd(name) {
88
+ let d = this.sregs.get(name);
89
+ if (!d) {
90
+ d = { buf: new Uint8Array(this.arraySize), len: 0 };
91
+ this.sregs.set(name, d);
92
+ }
93
+ return d;
94
+ },
95
+
96
+ /**
97
+ * A string register's logical contents.
98
+ * @param {string} name - The S register name.
99
+ * @returns {Uint8Array} A view of the first `len` bytes.
100
+ */
101
+ getS(name) {
102
+ const d = this.sd(name);
103
+ return d.buf.subarray(0, d.len);
104
+ },
105
+
106
+ /**
107
+ * A string register's complete buffer, stale bytes included -- what
108
+ * indexed reads see.
109
+ * @param {string} name - The S register name.
110
+ * @returns {Uint8Array} The whole buffer.
111
+ */
112
+ getSraw(name) {
113
+ return this.sd(name).buf;
114
+ },
115
+
116
+ /**
117
+ * Replace a string register's contents.
118
+ * @param {string} name - The S register name.
119
+ * @param {Uint8Array|number[]} bytes - The new contents.
120
+ * @param {boolean} [keepLength] - Leave the logical length untouched.
121
+ * @returns {void}
122
+ */
123
+ setS(name, bytes, keepLength) {
124
+ const d = this.sd(name);
125
+ const src =
126
+ bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes || []);
127
+ if (src.length > d.buf.length) {
128
+ // over capacity: StringData.SetData raises EDIABAS_BIP_0001 (no
129
+ // mapped trap bit -> 0) and does NOT write. Returning silently left
130
+ // the register holding stale bytes with a clean trap register.
131
+ this.trapBit = TRAP_UNMAPPED;
132
+ return;
133
+ }
134
+ d.buf.set(src, 0);
135
+ if (!keepLength) d.len = src.length;
136
+ },
137
+
138
+ /**
139
+ * `clear` on a string register zeroes the whole buffer AND the length.
140
+ * @param {string} name - The S register name.
141
+ * @returns {void}
142
+ */
143
+ clearS(name) {
144
+ const d = this.sd(name);
145
+ d.buf.fill(0);
146
+ d.len = 0;
147
+ },
148
+ });
149
+
150
+ if (typeof module !== 'undefined' && module.exports) {
151
+ module.exports = { A_REG_OFFSET };
152
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @file The write classifier: is a job a WRITE (it changes the ECU) or a
3
+ * READ (it queries it)? This is the decision the VM's write guard rests on
4
+ * (machine.js `run` and the transmit check in executor.js), and the UI's
5
+ * per-screen confirmation prompts read it too.
6
+ *
7
+ * BEST2 gives us no flag -- the bytecode builds the telegram dynamically, so
8
+ * the service byte isn't statically knowable, and the old prefix regex
9
+ * (^STEUERN|^FLASH|...) was leaky: it missed START_/STOP_SYSTEMCHECK actuator
10
+ * jobs, ABGLEICH_/ADAPTION calibrations, SET_/AUTHENTIS/SLEEP, ~1100 writes
11
+ * in all.
12
+ *
13
+ * The systematic signal is still the SGBD authors' NAMING CONTRACT, applied
14
+ * correctly rather than by leading verb, over a corpus of 27k jobs:
15
+ * 1. A STRONG READ TOKEN anywhere (LESEN/READ/STATUS/IDENT/ABFRAGE/ANZEIGE/
16
+ * ZUSTAND/ANZAHL) means read -- so ABGLEICH_LESEN_HFM ("read the calibr-
17
+ * ation") is a read despite the write-ish ABGLEICH_ prefix. Read wins.
18
+ * 2. Otherwise a WRITE TOKEN (STEUERN/SCHREIB/SETZEN/LOESCH/FLASH/START/STOP/
19
+ * RESET/CODIER/ABGLEICH/ADAPTION/AUTHENTIS/SET/...) means write.
20
+ * 3. Otherwise INFO means read. INFO is a WEAK read token, checked AFTER the
21
+ * write tokens, because it earned demotion twice over: the old \bINFO
22
+ * never matched *_INFO at all (`_` is a word character, so there is no \b
23
+ * between STEUERGERAETE_ and INFO -- every *_INFO job fell to default-
24
+ * deny and legitimate info reads were blocked in the UI), while at the
25
+ * START of a name \b DID match, so an INFO_SCHREIBEN-shaped name would
26
+ * have been called a read by rule 1. Read-wins is only safe for tokens
27
+ * that cannot prefix a write verb; INFO can, so writes are checked first.
28
+ * Measured over the 6147 unique job names in data/chassis (2026-08-17):
29
+ * exactly 3 jobs flip write->read (CBS_INFO, MODUL_INFO,
30
+ * DEBUGGING_INFORMATION -- all true reads), 0 flip read->write.
31
+ * 4. Otherwise DEFAULT-DENY: an unrecognised job is treated as a write, so a
32
+ * new or oddly-named job is guarded, never silently run.
33
+ * START/STOP match after `_` too ((?:\b|_)): \bSTOP missed STEUERN_ROE_STOP-
34
+ * style names. Default-deny already guarded those, so nothing observable
35
+ * changed in the corpus -- but with INFO checked after writes (rule 3), a
36
+ * hypothetical SYSTEMCHECK_STOP_INFO must hit the write tier, not fall
37
+ * through to the INFO tier. Relaxing a WRITE token is the safe direction.
38
+ * Kept identical to the classifier in tools/verify/sgbd_bulk_verify.py --
39
+ * two different answers to "is this a write?" is worse than either alone.
40
+ * test_write_gate.js check 3 enforces the twin token-by-token.
41
+ */
42
+
43
+ /**
44
+ * Strong read tokens: any one of these anywhere in a job name makes it a
45
+ * read, whatever else the name says.
46
+ * @type {RegExp}
47
+ */
48
+ const READ_TOKEN = new RegExp(
49
+ '(LESEN|_LES\\b|\\bLES_|READ|STATUS|IDENT|ANZEIGE|ABFRAG' +
50
+ '|ANZAHL|ZUSTAND|GET_)',
51
+ 'i'
52
+ );
53
+
54
+ /**
55
+ * CONFIG names a read ONLY when nothing else in the name says otherwise. MS45
56
+ * exposes ECU_CONFIG (83 12 F1 30 A8 01 -- a three-byte query for the
57
+ * vehicle-equipment list) and ECU_CONFIG_RESET (9B 12 F1 30 A8 04 00 ... --
58
+ * 27 bytes written back); they share service 0x30, so only the name separates
59
+ * them. This is checked BEFORE the write token but requires the write token to
60
+ * be absent, so read-wins ordering is preserved for everything else --
61
+ * CODIERUNG_LESEN stays a read because READ_TOKEN still runs first.
62
+ * @type {RegExp}
63
+ */
64
+ const CONFIG_READ_TOKEN = new RegExp('CONFIG', 'i');
65
+
66
+ /**
67
+ * Write tokens: a named write verb anywhere in the name, once no strong read
68
+ * token has claimed it.
69
+ * @type {RegExp}
70
+ */
71
+ const WRITE_TOKEN = new RegExp(
72
+ '(SCHREIB|STEUERN|_SETZEN|SETZEN|LOESCH|FLASH|PROGRAMMIER|(?:\\b|_)START' +
73
+ '|(?:\\b|_)STOP|RESET|CODIER|WRITE|\\bSET\\b|DOWNLOAD|UPLOAD|ABGLEICH' +
74
+ '|ADAPTION|SLEEP|WAKEUP|POWER_?DOWN|AUTHENTIS|INITIALISIER|EINSTELL' +
75
+ '|AKTIVIER|DEAKTIVIER|TILGUNG|ANLERN|TEACH|CLEAR)',
76
+ 'i'
77
+ );
78
+
79
+ /**
80
+ * The weak read token, consulted only after the write tokens (rule 3 above).
81
+ * @type {RegExp}
82
+ */
83
+ const INFO_READ_TOKEN = new RegExp('(?:\\b|_)INFO', 'i');
84
+
85
+ /**
86
+ * Classify a job by name: true when it must be treated as a write (rules 2
87
+ * and 4 above), false when the name says it only reads (rules 1 and 3).
88
+ * @param {string} name - The SGBD job name, any case.
89
+ * @returns {boolean} True for a write (or an unrecognised name), false for a read.
90
+ */
91
+ function isWriteJob(name) {
92
+ const n = String(name || '');
93
+ if (READ_TOKEN.test(n)) return false; // a read of anything is a read
94
+ // a *_CONFIG read, but only when no write verb rides along (_RESET etc.)
95
+ if (CONFIG_READ_TOKEN.test(n) && !WRITE_TOKEN.test(n)) return false;
96
+ if (WRITE_TOKEN.test(n)) return true; // a named write verb
97
+ if (INFO_READ_TOKEN.test(n)) return false; // *_INFO read, AFTER write check
98
+ return true; // default-deny: unknown => guarded
99
+ }
100
+
101
+ // Under node the pieces are separate modules; the browser gives them one
102
+ // shared script scope. index.js assembles the public surface from these.
103
+ if (typeof module !== 'undefined' && module.exports) {
104
+ module.exports = {
105
+ isWriteJob,
106
+ READ_TOKEN,
107
+ CONFIG_READ_TOKEN,
108
+ WRITE_TOKEN,
109
+ INFO_READ_TOKEN,
110
+ };
111
+ }