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,1483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The executor: the opcode `step` switch and the flag arithmetic it
|
|
3
|
+
* leans on. Extends Best2Vm (machine.js).
|
|
4
|
+
*
|
|
5
|
+
* THE SWITCH IS A 1:1 OPCODE TABLE, on purpose. One `case` per BEST2
|
|
6
|
+
* opcode, in the reference's grouping, each carrying the engine fact that
|
|
7
|
+
* makes it right. It is not a dispatch table and not per-opcode methods:
|
|
8
|
+
* the value of reading an opcode's semantics in one place, next to its
|
|
9
|
+
* neighbours, outweighs the length. Helpers and tables live AROUND it
|
|
10
|
+
* (this file's top, operands.js, environment.js); the case bodies keep
|
|
11
|
+
* their order and semantics.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
if (typeof require === 'function' && typeof module !== 'undefined') {
|
|
15
|
+
Object.assign(
|
|
16
|
+
globalThis,
|
|
17
|
+
require('./machine.js'),
|
|
18
|
+
require('./operands.js'),
|
|
19
|
+
require('./environment.js')
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Which flag combination each conditional jump tests (EdOperations' jump
|
|
25
|
+
* handlers). Named exactly as the disassembler emits them. jt/jnt are
|
|
26
|
+
* handled in step(): they test the TRAP REGISTER with an optional bit
|
|
27
|
+
* selector, not a boolean flag.
|
|
28
|
+
* @type {Object<string, (f: import('./machine.js').VmFlags) => boolean>}
|
|
29
|
+
*/
|
|
30
|
+
const JUMP_TESTS = {
|
|
31
|
+
jz: (f) => f.zero,
|
|
32
|
+
jnz: (f) => !f.zero,
|
|
33
|
+
jc: (f) => f.carry,
|
|
34
|
+
jnc: (f) => !f.carry,
|
|
35
|
+
jae: (f) => !f.carry,
|
|
36
|
+
jbe: (f) => f.carry || f.zero,
|
|
37
|
+
ja: (f) => !f.carry && !f.zero,
|
|
38
|
+
jb: (f) => f.carry,
|
|
39
|
+
jmi: (f) => f.sign,
|
|
40
|
+
jpl: (f) => !f.sign,
|
|
41
|
+
jv: (f) => f.overflow,
|
|
42
|
+
jnv: (f) => !f.overflow,
|
|
43
|
+
jg: (f) => !f.zero && f.sign === f.overflow,
|
|
44
|
+
jge: (f) => f.sign === f.overflow,
|
|
45
|
+
jl: (f) => f.sign !== f.overflow,
|
|
46
|
+
jle: (f) => f.zero || f.sign !== f.overflow,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Each erg* opcode has a FIXED width and signedness, independent of the
|
|
51
|
+
* operand's own type (the reference's result table). Publishing ergi
|
|
52
|
+
* unsigned reported SMG2's coolant temperature as 65531 where the engine
|
|
53
|
+
* says -5.
|
|
54
|
+
* @type {Object<string, [number, boolean]>} opcode -> [width, signed]
|
|
55
|
+
*/
|
|
56
|
+
const ERG_SPECS = {
|
|
57
|
+
ergb: [1, false],
|
|
58
|
+
ergw: [2, false],
|
|
59
|
+
ergd: [4, false],
|
|
60
|
+
ergi: [2, true],
|
|
61
|
+
ergl: [4, true],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bits of the flags WORD that `pushf` writes and `popf` reads.
|
|
66
|
+
*/
|
|
67
|
+
const FLAG_CARRY = 1;
|
|
68
|
+
const FLAG_ZERO = 2;
|
|
69
|
+
const FLAG_SIGN = 4;
|
|
70
|
+
const FLAG_OVERFLOW = 8;
|
|
71
|
+
|
|
72
|
+
/** `jt target, 32` aliases the unmapped trap bit 0. */
|
|
73
|
+
const TRAP_BIT_ALIAS_UNMAPPED = 32;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What `xtype` reports: EdInterfaceObd's name for the K+DCAN cable this
|
|
77
|
+
* app drives. SGBDs (carb) branch on it to pick their concept.
|
|
78
|
+
*/
|
|
79
|
+
const INTERFACE_TYPE = 'OBD';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What `xvers` reports: EdInterfaceObd.InterfaceVersion, 209 (0xD1). Not
|
|
83
|
+
* the engine's 7.3.0 -- the INTERFACE's version, which is what the SGBD
|
|
84
|
+
* asks.
|
|
85
|
+
*/
|
|
86
|
+
const INTERFACE_VERSION = 209;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A CommParameter blob DECLARES its own element width in byte 1
|
|
90
|
+
* (EdOperations.OpXsetpar): 0x00 = 16-bit words, 0x01 = 32-bit dwords,
|
|
91
|
+
* 0xFF = bytes; anything else is not a CommParameter.
|
|
92
|
+
* @type {Object<number, number>}
|
|
93
|
+
*/
|
|
94
|
+
const COMM_PARAM_WIDTHS = { 0x00: 2, 0x01: 4, 0xff: 1 };
|
|
95
|
+
|
|
96
|
+
/** The largest protocol concept number a CommParameter blob can name. */
|
|
97
|
+
const COMM_CONCEPT_MAX = 0x1ff;
|
|
98
|
+
|
|
99
|
+
/** BEST/2 `wait` is in SECONDS; the transport pauses in milliseconds. */
|
|
100
|
+
const MS_PER_SECOND = 1000;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reduce a value to the unsigned range of a width: `value mod 2^(8*width)`,
|
|
104
|
+
* non-negative even for a negative input.
|
|
105
|
+
* @param {number} value - The value.
|
|
106
|
+
* @param {number} width - The width in bytes.
|
|
107
|
+
* @returns {number} The wrapped value.
|
|
108
|
+
*/
|
|
109
|
+
function wrapToWidth(value, width) {
|
|
110
|
+
const lim = 2 ** (8 * width);
|
|
111
|
+
return ((value % lim) + lim) % lim;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Two's-complement negation in FULL 32 bits, `(uint)(-value)` -- what the
|
|
116
|
+
* engine feeds SetOverflow for a subtraction.
|
|
117
|
+
* @param {number} value - An unsigned value.
|
|
118
|
+
* @returns {number} Its 32-bit negation, unsigned.
|
|
119
|
+
*/
|
|
120
|
+
function negateU32(value) {
|
|
121
|
+
return (0x100000000 - value) % 0x100000000;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Read an unsigned 32-bit pattern as a signed integer.
|
|
126
|
+
* @param {number} value - The unsigned value.
|
|
127
|
+
* @returns {number} The signed value.
|
|
128
|
+
*/
|
|
129
|
+
function toInt32(value) {
|
|
130
|
+
return value >= 0x80000000 ? value - 0x100000000 : value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Zero-pad a number to two digits, for the `date`/`time` texts.
|
|
135
|
+
* @param {number} n - The number.
|
|
136
|
+
* @returns {string} Two or more digits.
|
|
137
|
+
*/
|
|
138
|
+
function twoDigits(n) {
|
|
139
|
+
return String(n).padStart(2, '0');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Object.assign(Best2Vm.prototype, {
|
|
143
|
+
/**
|
|
144
|
+
* Flags.SetOverflow: only when the operands SHARE a sign that differs
|
|
145
|
+
* from the result's. Operands are compared at the operation width.
|
|
146
|
+
* @param {number} v1 - The first operand, unsigned at `width`.
|
|
147
|
+
* @param {number} v2 - The second operand, unsigned at `width`.
|
|
148
|
+
* @param {number} result - The raw (unwrapped) result.
|
|
149
|
+
* @param {number} width - The operation width in bytes.
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
setOverflow(v1, v2, result, width) {
|
|
153
|
+
const sm = 2 ** (8 * width - 1);
|
|
154
|
+
const s1 = (v1 & sm) !== 0,
|
|
155
|
+
s2 = (v2 & sm) !== 0;
|
|
156
|
+
const sr = (wrapToWidth(result, width) & sm) !== 0;
|
|
157
|
+
this.flags.overflow = s1 === s2 && s1 !== sr;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Set Zero and Sign from a result at a width.
|
|
162
|
+
* @param {number} value - The raw (unwrapped) result.
|
|
163
|
+
* @param {number} width - The width in bytes.
|
|
164
|
+
* @returns {void}
|
|
165
|
+
*/
|
|
166
|
+
updateFlags(value, width) {
|
|
167
|
+
const masked = wrapToWidth(value, width);
|
|
168
|
+
this.flags.zero = masked === 0;
|
|
169
|
+
this.flags.sign = masked >= 2 ** (8 * width - 1);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Push a value as `n` bytes, LSB first, so the top of the stack is the
|
|
174
|
+
* most significant byte.
|
|
175
|
+
* @param {number} value - The value.
|
|
176
|
+
* @param {number} n - How many bytes.
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
pushBytes(value, n) {
|
|
180
|
+
let v = value;
|
|
181
|
+
for (let i = 0; i < n; i++) {
|
|
182
|
+
this.stack.push(v & 0xff);
|
|
183
|
+
v = Math.floor(v / 256);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Pop `n` bytes and rebuild the value: push wrote LSB first, so the MSB
|
|
189
|
+
* is on top, and popping MSB-first while shifting left reassembles
|
|
190
|
+
* exactly what was pushed. The caller checks the stack is deep enough.
|
|
191
|
+
* @param {number} n - How many bytes.
|
|
192
|
+
* @returns {number} The value.
|
|
193
|
+
*/
|
|
194
|
+
popValue(n) {
|
|
195
|
+
let v = 0;
|
|
196
|
+
for (let i = 0; i < n; i++) v = v * 256 + this.stack.pop();
|
|
197
|
+
return v;
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Execute one instruction.
|
|
202
|
+
* @param {string} name - The opcode name as sgbd_code.py emits it.
|
|
203
|
+
* @param {import('./machine.js').Operand[]} a - Its operands.
|
|
204
|
+
* @param {number} pc - Its index in `ops`, for error messages.
|
|
205
|
+
* @returns {number|symbol|undefined} The next `pc` for a taken jump,
|
|
206
|
+
* STOP at `eoj`, or undefined to fall through to `pc + 1`.
|
|
207
|
+
* @throws {VmError} An unimplemented opcode, an unresolved jump, a
|
|
208
|
+
* refused write, an oversized answer, or an `eerr`.
|
|
209
|
+
*/
|
|
210
|
+
step(name, a, pc) {
|
|
211
|
+
const A = a[0],
|
|
212
|
+
B = a[1];
|
|
213
|
+
const f = this.flags;
|
|
214
|
+
|
|
215
|
+
switch (name) {
|
|
216
|
+
case 'nop':
|
|
217
|
+
return;
|
|
218
|
+
case 'eoj':
|
|
219
|
+
return STOP;
|
|
220
|
+
|
|
221
|
+
// ---- stack: BYTES, not values. push writes the operand's width,
|
|
222
|
+
// MSB last so the top of the stack is the most significant byte;
|
|
223
|
+
// pop/atsp read a width and rebuild big-endian. atsp reads POS bytes
|
|
224
|
+
// DOWN from the top rather than peeking (OpAtsp: index = pos - len).
|
|
225
|
+
case 'push': {
|
|
226
|
+
const w = this.widthOf(A);
|
|
227
|
+
let v = this.val(A);
|
|
228
|
+
if (opIsImm(A[0])) {
|
|
229
|
+
// an immediate pushes 4 bytes (EdValueType width)
|
|
230
|
+
this.pushBytes(v, 4);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.pushBytes(v, w);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
case 'pop': {
|
|
237
|
+
// push wrote LSB first, so the MSB is on top: popping MSB-first and
|
|
238
|
+
// shifting left reassembles exactly what was pushed.
|
|
239
|
+
const w = this.widthOf(A);
|
|
240
|
+
if (this.stack.length < w) {
|
|
241
|
+
this.store(A, 0);
|
|
242
|
+
this.updateFlags(0, w);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const v = this.popValue(w);
|
|
246
|
+
this.store(A, v);
|
|
247
|
+
this.flags.overflow = false;
|
|
248
|
+
this.updateFlags(v, w);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
case 'atsp': {
|
|
252
|
+
// `atsp reg,#pos` reads WITHOUT popping, pos bytes down from the top
|
|
253
|
+
// (OpAtsp: index = pos - length into a pop-order array, then
|
|
254
|
+
// big-endian assembly). In our array the top is the LAST element,
|
|
255
|
+
// so pop-order index k is stack[len-1-k].
|
|
256
|
+
const w = this.widthOf(A);
|
|
257
|
+
const pos = this.val(B);
|
|
258
|
+
if (this.stack.length < w || pos < w) {
|
|
259
|
+
this.store(A, 0);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
let v = 0;
|
|
263
|
+
for (let k = pos - w; k < pos; k++) {
|
|
264
|
+
v = v * 256 + (this.stack[this.stack.length - 1 - k] ?? 0);
|
|
265
|
+
}
|
|
266
|
+
this.store(A, v);
|
|
267
|
+
this.updateFlags(v, w);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
case 'pushf': {
|
|
271
|
+
// the flags WORD (bit0 carry, bit1 zero, bit2 sign, bit3 overflow),
|
|
272
|
+
// 4 bytes LSB-first like any other push -- not four zeros
|
|
273
|
+
const v =
|
|
274
|
+
(f.carry ? FLAG_CARRY : 0) |
|
|
275
|
+
(f.zero ? FLAG_ZERO : 0) |
|
|
276
|
+
(f.sign ? FLAG_SIGN : 0) |
|
|
277
|
+
(f.overflow ? FLAG_OVERFLOW : 0);
|
|
278
|
+
this.pushBytes(v, 4);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
case 'popf': {
|
|
282
|
+
const v = this.stack.length >= 4 ? this.popValue(4) : 0;
|
|
283
|
+
f.carry = !!(v & FLAG_CARRY);
|
|
284
|
+
f.zero = !!(v & FLAG_ZERO);
|
|
285
|
+
f.sign = !!(v & FLAG_SIGN);
|
|
286
|
+
f.overflow = !!(v & FLAG_OVERFLOW);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'clear': {
|
|
291
|
+
if (opIsStringReg(A)) {
|
|
292
|
+
this.clearS(A[1]);
|
|
293
|
+
} else {
|
|
294
|
+
this.store(A, 0, false);
|
|
295
|
+
}
|
|
296
|
+
// OpClear sets the same flags for EVERY register type; leaving them
|
|
297
|
+
// stale on the numeric path made a jz right after `clear I0` not
|
|
298
|
+
// jump.
|
|
299
|
+
f.carry = false;
|
|
300
|
+
f.zero = true;
|
|
301
|
+
f.sign = false;
|
|
302
|
+
f.overflow = false;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'move': {
|
|
307
|
+
// width coercion: a byte destination takes the low byte, a string
|
|
308
|
+
// destination takes bytes. Reading a string source into a numeric
|
|
309
|
+
// destination folds big-endian (Operand.GetValueData).
|
|
310
|
+
const dstIsBytes =
|
|
311
|
+
opIsStringReg(A) ||
|
|
312
|
+
opIsRange(A[0]) ||
|
|
313
|
+
B[0] === OpMode.IMM_STR ||
|
|
314
|
+
opIsStringReg(B);
|
|
315
|
+
if (dstIsBytes) {
|
|
316
|
+
this.store(A, this.bytes(B), true);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const w = this.widthOf(A);
|
|
320
|
+
const v = this.val(B, w);
|
|
321
|
+
this.store(A, v);
|
|
322
|
+
f.carry = false;
|
|
323
|
+
f.overflow = false;
|
|
324
|
+
this.updateFlags(v, w);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---- integer arithmetic. Carry is the unsigned overflow out of the
|
|
329
|
+
// destination's width; Overflow is the signed one.
|
|
330
|
+
case 'adds':
|
|
331
|
+
case 'addc': {
|
|
332
|
+
const w = this.widthOf(A);
|
|
333
|
+
const x = this.val(A, w),
|
|
334
|
+
y = this.val(B, w) + (name === 'addc' && f.carry ? 1 : 0);
|
|
335
|
+
const sum = x + y;
|
|
336
|
+
const lim = 2 ** (8 * w);
|
|
337
|
+
f.carry = sum >= lim;
|
|
338
|
+
this.setOverflow(x, y, sum, w);
|
|
339
|
+
this.store(A, sum % lim);
|
|
340
|
+
this.updateFlags(sum, w);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
case 'subb':
|
|
344
|
+
case 'subc': {
|
|
345
|
+
const w = this.widthOf(A);
|
|
346
|
+
const x = this.val(A, w),
|
|
347
|
+
y = this.val(B, w) + (name === 'subc' && f.carry ? 1 : 0);
|
|
348
|
+
const diff = x - y;
|
|
349
|
+
f.carry = diff < 0;
|
|
350
|
+
this.setOverflow(x, negateU32(y), diff, w);
|
|
351
|
+
this.store(A, wrapToWidth(diff, w));
|
|
352
|
+
this.updateFlags(diff, w);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
case 'comp': {
|
|
356
|
+
// Subtract without storing. Carry is the unsigned BORROW, and
|
|
357
|
+
// Overflow follows SetOverflow(val0, (uint)(-val1), diff) -- the
|
|
358
|
+
// subtrahend is negated in FULL 32-BIT two's complement and only
|
|
359
|
+
// then masked to the width, which for w < 4 is NOT the same as
|
|
360
|
+
// negating within the width. This decides `jc`/`jl`/`jg`, i.e.
|
|
361
|
+
// every loop bound in the corpus, so it is copied literally.
|
|
362
|
+
const w = this.widthOf(A);
|
|
363
|
+
const x = this.val(A, w),
|
|
364
|
+
y = this.val(B, w);
|
|
365
|
+
const diff = x - y;
|
|
366
|
+
f.carry = diff < 0;
|
|
367
|
+
this.setOverflow(x, negateU32(y), diff, w);
|
|
368
|
+
this.updateFlags(diff, w);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
case 'mult': {
|
|
372
|
+
// SIGNED narrow multiply, and it CLOBBERS arg1 with the high half
|
|
373
|
+
// when arg1 is register-backed (OpMult). Missing the clobber left a
|
|
374
|
+
// stride register holding a stale value, so FS_LESEN's fault-record
|
|
375
|
+
// cursor advanced from the wrong base and every record read one
|
|
376
|
+
// byte early.
|
|
377
|
+
//
|
|
378
|
+
// OpMult computes the product in 32-BIT integer arithmetic -- the
|
|
379
|
+
// exact low 32 bits, wrapping -- never a 64-bit product. A plain JS
|
|
380
|
+
// double multiply rounds past 2^53 and got the low word wrong for
|
|
381
|
+
// large 32-bit operands; Math.imul is the exact wrap. The high half
|
|
382
|
+
// is "(UInt64)result >> (len << 3)" of that 32-bit result, so it is
|
|
383
|
+
// ZERO at width 4 (the engine's own comment: negative high halves
|
|
384
|
+
// are not emulated).
|
|
385
|
+
const w = this.widthOf(A);
|
|
386
|
+
const lim = 2 ** (8 * w),
|
|
387
|
+
half = lim / 2;
|
|
388
|
+
const sx = (() => {
|
|
389
|
+
const v = this.val(A, w);
|
|
390
|
+
return v >= half ? v - lim : v;
|
|
391
|
+
})();
|
|
392
|
+
const sy = (() => {
|
|
393
|
+
const v = this.val(B, w);
|
|
394
|
+
return v >= half ? v - lim : v;
|
|
395
|
+
})();
|
|
396
|
+
const result = (w === 4 ? Math.imul(sx | 0, sy | 0) : sx * sy) >>> 0;
|
|
397
|
+
this.store(A, result % lim);
|
|
398
|
+
f.overflow = false;
|
|
399
|
+
this.updateFlags(result, w);
|
|
400
|
+
if (opIsNumReg(B)) {
|
|
401
|
+
this.store(B, Math.floor(result / lim) % lim);
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
case 'div':
|
|
406
|
+
case 'divs': {
|
|
407
|
+
// OpDivs does 32-BIT SIGNED division for EVERY width -- the narrow
|
|
408
|
+
// native forms are commented out in the engine as "DIVS failure in
|
|
409
|
+
// ediabas!" -- and it writes the REMAINDER back into arg1 when arg1
|
|
410
|
+
// is register-backed. Dividing unsigned gave SMG2's hydraulic
|
|
411
|
+
// pressure 42949660 where the engine says 65524: the dividend was
|
|
412
|
+
// 0xFFFFFB38 = -1224, and -1224/100 = -12, which is 65524 as an
|
|
413
|
+
// unsigned 16-bit result.
|
|
414
|
+
const w = this.widthOf(A);
|
|
415
|
+
const x = toInt32(this.val(A, w)),
|
|
416
|
+
y = toInt32(this.val(B, w));
|
|
417
|
+
if (y === 0) {
|
|
418
|
+
// OpDivs on a zero divisor raises EDIABAS_BIP_0007 (which has no
|
|
419
|
+
// mapped trap bit -> 0), KEEPS arg0's value, and still writes a
|
|
420
|
+
// zero remainder. Yielding a silent 0 quotient turned a garbled
|
|
421
|
+
// count byte into 0-valued results published as OKAY. The
|
|
422
|
+
// trap-mask layer is not modeled (see generr), so trapping is
|
|
423
|
+
// the conservative choice over aborting the job.
|
|
424
|
+
this.trapBit = TRAP_UNMAPPED;
|
|
425
|
+
f.overflow = false;
|
|
426
|
+
this.updateFlags(0, w);
|
|
427
|
+
if (opIsNumReg(B)) {
|
|
428
|
+
this.store(B, 0);
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const q = Math.trunc(x / y),
|
|
433
|
+
rem = x % y;
|
|
434
|
+
const stored = wrapToWidth(q, w);
|
|
435
|
+
this.store(A, stored);
|
|
436
|
+
f.overflow = false;
|
|
437
|
+
this.updateFlags(stored, w);
|
|
438
|
+
if (opIsNumReg(B)) {
|
|
439
|
+
this.store(B, wrapToWidth(rem, w));
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
case 'mod': {
|
|
444
|
+
const w = this.widthOf(A);
|
|
445
|
+
const d = this.val(B);
|
|
446
|
+
const r = d === 0 ? 0 : this.val(A) % d;
|
|
447
|
+
this.store(A, r);
|
|
448
|
+
this.updateFlags(r, w);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
case 'and':
|
|
452
|
+
case 'or':
|
|
453
|
+
case 'xor': {
|
|
454
|
+
const w = this.widthOf(A);
|
|
455
|
+
const x = BigInt(this.val(A, w)),
|
|
456
|
+
y = BigInt(this.val(B, w));
|
|
457
|
+
const r = Number(
|
|
458
|
+
name === 'and' ? x & y : name === 'or' ? x | y : x ^ y
|
|
459
|
+
);
|
|
460
|
+
this.store(A, r);
|
|
461
|
+
this.updateFlags(r, w);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
case 'not': {
|
|
465
|
+
const w = this.widthOf(A);
|
|
466
|
+
const lim = 2 ** (8 * w);
|
|
467
|
+
const r = lim - 1 - this.val(A);
|
|
468
|
+
this.store(A, r);
|
|
469
|
+
this.updateFlags(r, w);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
case 'asl':
|
|
473
|
+
case 'lsl': {
|
|
474
|
+
const w = this.widthOf(A);
|
|
475
|
+
const n = this.val(B);
|
|
476
|
+
const v = this.val(A, w);
|
|
477
|
+
const r = v * 2 ** n;
|
|
478
|
+
// carry is the LAST BIT SHIFTED OUT of the width, like the engine's
|
|
479
|
+
// shift ops; untouched when the count is zero
|
|
480
|
+
if (n > 0)
|
|
481
|
+
f.carry = n <= 8 * w && Math.floor(v / 2 ** (8 * w - n)) % 2 === 1;
|
|
482
|
+
this.store(A, r % 2 ** (8 * w));
|
|
483
|
+
this.updateFlags(r, w);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
case 'lsr':
|
|
487
|
+
case 'asr': {
|
|
488
|
+
// asr is ARITHMETIC: the sign bit (at the operand's width) shifts
|
|
489
|
+
// back in. lsr shifts in zeros. Both report the last bit shifted
|
|
490
|
+
// out in carry.
|
|
491
|
+
const w = this.widthOf(A);
|
|
492
|
+
const n = this.val(B);
|
|
493
|
+
const lim = 2 ** (8 * w);
|
|
494
|
+
const u = this.val(A, w); // bit pattern
|
|
495
|
+
let v = u;
|
|
496
|
+
if (name === 'asr' && v >= lim / 2) v -= lim; // signed view
|
|
497
|
+
// the shifted-out bit is bit n-1 of the PATTERN, sign play or not
|
|
498
|
+
if (n > 0 && n <= 8 * w) {
|
|
499
|
+
f.carry = Math.floor(u / 2 ** (n - 1)) % 2 === 1;
|
|
500
|
+
}
|
|
501
|
+
const r = Math.floor(v / 2 ** n);
|
|
502
|
+
this.store(A, wrapToWidth(r, w));
|
|
503
|
+
this.updateFlags(r, w);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ---- float
|
|
508
|
+
case 'fix2flt':
|
|
509
|
+
case 'ufix2flt': {
|
|
510
|
+
// fix2flt SIGN-EXTENDS by the SOURCE operand's width (1/2/4 ->
|
|
511
|
+
// SByte/Int16/Int32); ufix2flt does not. Skipping the sign made a
|
|
512
|
+
// negative reading come out as its unsigned complement scaled --
|
|
513
|
+
// SMG2's lateral acceleration read 4294942.72 where the engine
|
|
514
|
+
// says -24.576, i.e. exactly (value - 2^32) * scale.
|
|
515
|
+
const w = this.widthOf(B) || 4;
|
|
516
|
+
let v = this.val(B, w);
|
|
517
|
+
if (name === 'fix2flt') {
|
|
518
|
+
const lim = 2 ** (8 * w);
|
|
519
|
+
if (v >= lim / 2) v -= lim;
|
|
520
|
+
}
|
|
521
|
+
this.fregs.set(A[1], v);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
case 'flt2fix': {
|
|
525
|
+
const w = this.widthOf(A);
|
|
526
|
+
const v = Math.trunc(this.getReg(B[1]));
|
|
527
|
+
this.store(A, wrapToWidth(v, w));
|
|
528
|
+
this.updateFlags(v, w);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
case 'fadd':
|
|
532
|
+
case 'fsub':
|
|
533
|
+
case 'fmul':
|
|
534
|
+
case 'fdiv': {
|
|
535
|
+
const x = this.getReg(A[1]);
|
|
536
|
+
const y = this.floatOf(B);
|
|
537
|
+
let r = x;
|
|
538
|
+
if (name === 'fadd') r = x + y;
|
|
539
|
+
else if (name === 'fsub') r = x - y;
|
|
540
|
+
else if (name === 'fmul') r = x * y;
|
|
541
|
+
else {
|
|
542
|
+
// OpFdiv divides regardless and stores what comes out; an
|
|
543
|
+
// infinite or NaN quotient raises EDIABAS_BIP_0011 (trap bit 8).
|
|
544
|
+
// Storing a silent 0 hid the division by zero entirely.
|
|
545
|
+
r = x / y;
|
|
546
|
+
if (!Number.isFinite(r)) this.trapBit = TRAP_FLOAT;
|
|
547
|
+
}
|
|
548
|
+
this.fregs.set(A[1], r);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
case 'fcomp': {
|
|
552
|
+
// comp for floats: subtract without storing. There is no width or
|
|
553
|
+
// borrow arithmetic here -- zero/sign carry the ordering and
|
|
554
|
+
// overflow stays clear, which is exactly what makes jl/jg/jle/jge
|
|
555
|
+
// (sign vs overflow) and jc/jb (carry) all read as plain < and >.
|
|
556
|
+
const x = this.floatOf(A),
|
|
557
|
+
y = this.floatOf(B);
|
|
558
|
+
f.zero = x === y;
|
|
559
|
+
f.sign = x < y;
|
|
560
|
+
f.carry = x < y;
|
|
561
|
+
f.overflow = false;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
case 'y42flt':
|
|
565
|
+
case 'y82flt': {
|
|
566
|
+
// 4/8 raw bytes -> IEEE float, LITTLE-endian: the corpus reverses
|
|
567
|
+
// wire bytes with swap first (swap S1,0,4 / y42flt F5,S1[0]), so
|
|
568
|
+
// the op itself reads host order.
|
|
569
|
+
const n = name === 'y42flt' ? 4 : 8;
|
|
570
|
+
const [m, a, b, c] = B;
|
|
571
|
+
let src;
|
|
572
|
+
if (opIsIndexed(m)) {
|
|
573
|
+
const raw = this.getSraw(a);
|
|
574
|
+
let i = m === OpMode.IDX_IMM ? b : this.getReg(b);
|
|
575
|
+
if (m === OpMode.IDX_REG_IMM) i += c || 0;
|
|
576
|
+
src = raw.slice(i, i + n);
|
|
577
|
+
} else {
|
|
578
|
+
src = Uint8Array.from(this.bytes(B)).slice(0, n);
|
|
579
|
+
}
|
|
580
|
+
const buf = new Uint8Array(n);
|
|
581
|
+
buf.set(src.slice(0, n));
|
|
582
|
+
const dv = new DataView(buf.buffer);
|
|
583
|
+
this.fregs.set(
|
|
584
|
+
A[1],
|
|
585
|
+
n === 4 ? dv.getFloat32(0, true) : dv.getFloat64(0, true)
|
|
586
|
+
);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
case 'flt2y4':
|
|
590
|
+
case 'flt2y8': {
|
|
591
|
+
const n = name === 'flt2y4' ? 4 : 8;
|
|
592
|
+
const buf = new Uint8Array(n);
|
|
593
|
+
const dv = new DataView(buf.buffer);
|
|
594
|
+
const v = this.getReg(B[1]);
|
|
595
|
+
if (n === 4) dv.setFloat32(0, v, true);
|
|
596
|
+
else dv.setFloat64(0, v, true);
|
|
597
|
+
this.store(A, buf, true);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
case 'a2flt': {
|
|
601
|
+
const txt = this.textOf(B);
|
|
602
|
+
this.fregs.set(A[1], Best2Codec.parseNum(txt));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
case 'a2fix': {
|
|
606
|
+
// StringToValue, NOT a float parse: it understands 0x hex and 0y
|
|
607
|
+
// binary. The `bits` tables hold masks as "0x01", and parseFloat
|
|
608
|
+
// stops at the 'x' and yields 0 -- so every table-driven bit test
|
|
609
|
+
// masked with 0 and reported the bit set.
|
|
610
|
+
const w = this.widthOf(A);
|
|
611
|
+
const txt = this.textOf(B);
|
|
612
|
+
const v = Best2Codec.strToValue(txt);
|
|
613
|
+
this.store(A, wrapToWidth(v, w));
|
|
614
|
+
// a2fix forces Zero and Sign false regardless of the value
|
|
615
|
+
f.zero = false;
|
|
616
|
+
f.sign = false;
|
|
617
|
+
f.overflow = false;
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
case 'flt2a': {
|
|
621
|
+
this.storeText(A, Best2Codec.fltText(this.getReg(B[1])));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
case 'fix2a':
|
|
625
|
+
case 'ufix2a': {
|
|
626
|
+
this.storeText(A, String(this.val(B)));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
case 'fix2dez':
|
|
630
|
+
case 'ufix2dez': {
|
|
631
|
+
// signed for fix2dez, unsigned for ufix2dez, width from the SOURCE
|
|
632
|
+
const w = this.widthOf(B) || 1;
|
|
633
|
+
let v = this.val(B);
|
|
634
|
+
if (name === 'fix2dez') {
|
|
635
|
+
const lim = 2 ** (8 * w);
|
|
636
|
+
if (v >= lim / 2) v -= lim;
|
|
637
|
+
}
|
|
638
|
+
this.storeText(A, String(v));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ---- byte/text conversions
|
|
643
|
+
case 'fix2hex':
|
|
644
|
+
case 'ufix2hex': {
|
|
645
|
+
// EXACT format: "0x{0:X02}" / "0x{0:X04}" / "0x{0:X08}" by the
|
|
646
|
+
// SOURCE operand's width -- prefix included, zero-padded, uppercase.
|
|
647
|
+
// This is load-bearing, not cosmetic: JobResult's SB column holds
|
|
648
|
+
// "0xA0", and the status lookup is `fix2hex S1,B0` / `tabseek "SB"`,
|
|
649
|
+
// so dropping the prefix made every job's status miss and clamp to
|
|
650
|
+
// the table's last row (ERROR_ECU_UNKNOWN_STATUSBYTE instead of OKAY).
|
|
651
|
+
const w = this.widthOf(B) || 1;
|
|
652
|
+
const digits = w >= 4 ? 8 : w === 2 ? 4 : 2;
|
|
653
|
+
const v = this.val(B);
|
|
654
|
+
const txt =
|
|
655
|
+
'0x' + (v >>> 0).toString(16).toUpperCase().padStart(digits, '0');
|
|
656
|
+
this.storeText(A, txt);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
case 'y2hex': {
|
|
660
|
+
let s = '';
|
|
661
|
+
for (const x of this.bytes(B)) {
|
|
662
|
+
s += x.toString(16).toUpperCase().padStart(2, '0');
|
|
663
|
+
}
|
|
664
|
+
this.storeText(A, s);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
case 'y2bcd': {
|
|
668
|
+
// a nibble above 9 is not a BCD digit: ValueToBcd prints '*'
|
|
669
|
+
let s = '';
|
|
670
|
+
for (const x of this.bytes(B)) {
|
|
671
|
+
for (const n of [(x >> 4) & 0xf, x & 0xf]) {
|
|
672
|
+
s += n <= 9 ? String(n) : '*';
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
this.storeText(A, s);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
case 'hex2y': {
|
|
679
|
+
const txt = Best2Codec.cstr(this.bytes(B)).replace(/[^0-9A-Fa-f]/g, '');
|
|
680
|
+
const out = new Uint8Array(Math.floor(txt.length / 2));
|
|
681
|
+
for (let i = 0; i < out.length; i++) {
|
|
682
|
+
out[i] = parseInt(txt.substr(i * 2, 2), 16);
|
|
683
|
+
}
|
|
684
|
+
this.store(A, out, true);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ---- strings (byte buffers)
|
|
689
|
+
case 'slen':
|
|
690
|
+
case 'strlen': {
|
|
691
|
+
const b = this.bytes(B);
|
|
692
|
+
const n = name === 'strlen' ? Best2Codec.cstr(b).length : b.length;
|
|
693
|
+
this.store(A, n);
|
|
694
|
+
this.updateFlags(n, this.widthOf(A));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
case 'scat':
|
|
698
|
+
case 'strcat': {
|
|
699
|
+
const x = this.bytes(A),
|
|
700
|
+
y = this.bytes(B);
|
|
701
|
+
const base =
|
|
702
|
+
name === 'strcat' ? Best2Codec.strBytes(Best2Codec.cstr(x)) : x;
|
|
703
|
+
const add =
|
|
704
|
+
name === 'strcat' ? Best2Codec.strBytes(Best2Codec.cstr(y)) : y;
|
|
705
|
+
const out = new Uint8Array(base.length + add.length);
|
|
706
|
+
out.set(base);
|
|
707
|
+
out.set(add, base.length);
|
|
708
|
+
this.store(A, out, true);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
case 'scut': {
|
|
712
|
+
// KNOWN OPEN QUESTION (do not "fix" without engine evidence): this
|
|
713
|
+
// counts on string registers carrying no trailing NUL, but
|
|
714
|
+
// storeText (the store path for pars/tabget/flt2a/...) DOES count
|
|
715
|
+
// its NUL in the logical length -- so scut directly after a
|
|
716
|
+
// storeText keeps a NUL the engine would drop. No fixture in the
|
|
717
|
+
// 3,730-result suite exercises that sequence; changing either side
|
|
718
|
+
// blind risks the 100% agreement. Revisit with a captured trace of
|
|
719
|
+
// a job doing scut-after-storeText.
|
|
720
|
+
//
|
|
721
|
+
// strcut removes the last `len` bytes -- and `len` COUNTS THE
|
|
722
|
+
// TERMINATING NUL, which the register's logical length does not
|
|
723
|
+
// include once SetStringData has stored it. So `scut S1,#1` on the
|
|
724
|
+
// 2-byte "FP" removes only the (absent) terminator and leaves "FP"
|
|
725
|
+
// intact; treating it as "drop the last byte" cut the P and broke
|
|
726
|
+
// EWS's VIN check digit 200 instructions later.
|
|
727
|
+
//
|
|
728
|
+
// Concretely: the effective byte count removed is len-1, floored at
|
|
729
|
+
// zero, because our string registers carry no trailing NUL of their
|
|
730
|
+
// own (storeText appends one only for text results).
|
|
731
|
+
const reg = A[1];
|
|
732
|
+
const buf = this.getS(reg);
|
|
733
|
+
const n = Math.max(0, this.val(B) - 1);
|
|
734
|
+
this.setS(
|
|
735
|
+
reg,
|
|
736
|
+
n >= buf.length ? new Uint8Array(0) : buf.slice(0, buf.length - n)
|
|
737
|
+
);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
case 'spaste': {
|
|
741
|
+
// INSERT (datainsert), shifting the tail right -- not an overwrite.
|
|
742
|
+
// Inserting at or past the current logical end is a silent no-op.
|
|
743
|
+
const reg = A[1];
|
|
744
|
+
const idx = A[0] === OpMode.IDX_IMM ? A[2] : this.getReg(A[2]);
|
|
745
|
+
const src = this.bytes(B);
|
|
746
|
+
const buf = this.getS(reg);
|
|
747
|
+
if (idx >= buf.length) return;
|
|
748
|
+
const out = new Uint8Array(buf.length + src.length);
|
|
749
|
+
out.set(buf.slice(0, idx), 0);
|
|
750
|
+
out.set(src, idx);
|
|
751
|
+
out.set(buf.slice(idx), idx + src.length);
|
|
752
|
+
this.setS(reg, out);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
case 'scmp': {
|
|
756
|
+
// datacmp: byte-exact, and Zero means EQUAL (the normal sense)
|
|
757
|
+
const x = this.bytes(A),
|
|
758
|
+
y = this.bytes(B);
|
|
759
|
+
f.zero = x.length === y.length && x.every((v, i) => v === y[i]);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
case 'strcmp': {
|
|
763
|
+
// INVERTED relative to scmp: OpStrcmp sets
|
|
764
|
+
// Zero = (String.Compare(a, b, Ordinal) != 0)
|
|
765
|
+
// so ZERO MEANS THE STRINGS DIFFER. Getting this backwards made
|
|
766
|
+
// every `strcmp S2,"BUSY" / jz done` status check read as "still
|
|
767
|
+
// busy" on an OKAY response, so all 28 BMS46 jobs retried the
|
|
768
|
+
// telegram until the step limit.
|
|
769
|
+
const xs = Best2Codec.cstr(this.bytes(A));
|
|
770
|
+
const ys = this.textOf(B);
|
|
771
|
+
f.zero = xs !== ys;
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
case 'serase': {
|
|
775
|
+
// delete `len` bytes at arg0's index, closing the gap (dataerase).
|
|
776
|
+
// arg0 must be an indexed operand; the index names the position.
|
|
777
|
+
const reg = A[1];
|
|
778
|
+
const idx = A[0] === OpMode.IDX_IMM ? A[2] : this.getReg(A[2]);
|
|
779
|
+
const n = this.val(B);
|
|
780
|
+
const buf = this.getS(reg);
|
|
781
|
+
// The gap is CLIPPED to the string: an erase at or past the end
|
|
782
|
+
// removes nothing, and one that runs past the end removes only what
|
|
783
|
+
// is there. Sizing the result as length-minus-count instead dropped
|
|
784
|
+
// the surviving bytes whenever the range overhung -- FA.PRG's
|
|
785
|
+
// `serase S4[1], 1` on a one-byte S4 emptied it, and the vehicle
|
|
786
|
+
// order decode stopped at its third option with
|
|
787
|
+
// ERROR_UNKNOWN_IDENTIFIER.
|
|
788
|
+
const from = Math.max(0, Math.min(buf.length, idx));
|
|
789
|
+
const to = Math.max(from, Math.min(buf.length, idx + Math.max(0, n)));
|
|
790
|
+
const out = new Uint8Array(buf.length - (to - from));
|
|
791
|
+
out.set(buf.subarray(0, from), 0);
|
|
792
|
+
out.set(buf.subarray(to), from);
|
|
793
|
+
this.setS(reg, out);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
case 'srevrs': {
|
|
797
|
+
const b = Uint8Array.from(this.bytes(A)).reverse();
|
|
798
|
+
this.store(A, b, true);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
case 'strim': {
|
|
802
|
+
this.store(
|
|
803
|
+
A,
|
|
804
|
+
Best2Codec.strBytes(Best2Codec.cstr(this.bytes(A)).trim()),
|
|
805
|
+
true
|
|
806
|
+
);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
case 'swap': {
|
|
810
|
+
// ranged modes can hold a REGISTER NAME for index or length --
|
|
811
|
+
// resolve them the way every other indexed op does, or a register-
|
|
812
|
+
// backed range coerces to 0 and nothing is reversed
|
|
813
|
+
const [m, reg, bIdx, cLen] = A;
|
|
814
|
+
const idx = this.resolveIdx(m, bIdx ?? 0);
|
|
815
|
+
const n = this.resolveLen(m, cLen ?? 0);
|
|
816
|
+
const buf = Uint8Array.from(this.getS(reg));
|
|
817
|
+
const part = buf.slice(idx, idx + n).reverse();
|
|
818
|
+
buf.set(part, idx);
|
|
819
|
+
this.setS(reg, buf);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
case 'setspc': {
|
|
823
|
+
// Sets the split parameters the NEXT stoken uses: arg0 is the
|
|
824
|
+
// separator characters (a pool string like " " or ", "), arg1 the
|
|
825
|
+
// 1-based token number.
|
|
826
|
+
this.tokenSep = Best2Codec.cstr(this.bytes(A));
|
|
827
|
+
this.tokenIdx = this.val(B);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
case 'stoken': {
|
|
831
|
+
// Token extraction: split arg1 by the setspc separators and store
|
|
832
|
+
// the setspc-selected token into arg0. Zero flag reports "no such
|
|
833
|
+
// token" -- the corpus idiom is `setspc " ",n / stoken S5,S7 / jz
|
|
834
|
+
// fail`. Runs of separators collapse: the seps here are " " and
|
|
835
|
+
// ", ", where keeping empty tokens would make every second token
|
|
836
|
+
// blank.
|
|
837
|
+
const src = Best2Codec.cstr(this.bytes(B));
|
|
838
|
+
const seps = this.tokenSep || ' ';
|
|
839
|
+
const parts = src
|
|
840
|
+
.split(
|
|
841
|
+
new RegExp(`[${seps.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&')}]+`)
|
|
842
|
+
)
|
|
843
|
+
.filter((t) => t !== '');
|
|
844
|
+
const tok = this.tokenIdx >= 1 ? parts[this.tokenIdx - 1] : undefined;
|
|
845
|
+
this.storeText(A, tok ?? '');
|
|
846
|
+
f.zero = tok === undefined;
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
case 'test': {
|
|
850
|
+
// non-destructive AND: flags only, arg0 is NOT written
|
|
851
|
+
const w = this.widthOf(A);
|
|
852
|
+
const r = Number(BigInt(this.val(A)) & BigInt(this.val(B)));
|
|
853
|
+
f.overflow = false;
|
|
854
|
+
this.updateFlags(r, w);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
case 'setc':
|
|
858
|
+
f.carry = true;
|
|
859
|
+
return;
|
|
860
|
+
case 'clrc':
|
|
861
|
+
f.carry = false;
|
|
862
|
+
return;
|
|
863
|
+
case 'clrv':
|
|
864
|
+
f.overflow = false;
|
|
865
|
+
return;
|
|
866
|
+
case 'sett':
|
|
867
|
+
this.trapBit = this.val(A) || TRAP_USER;
|
|
868
|
+
return;
|
|
869
|
+
case 'ssize': {
|
|
870
|
+
const n = this.bytes(A).length;
|
|
871
|
+
this.store(B, n);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ---- control flow
|
|
876
|
+
case 'jump':
|
|
877
|
+
if (A[1] === null) throw new VmError(`unresolved jump at ${pc}`);
|
|
878
|
+
return A[1];
|
|
879
|
+
case 'jt':
|
|
880
|
+
case 'jnt': {
|
|
881
|
+
// `jt target[, bit]`: with a bit selector, fire when the trap
|
|
882
|
+
// register equals it (bit 32 aliases the unmapped bit 0); with no
|
|
883
|
+
// selector, jt fires on ANY pending trap. jnt is the inverse --
|
|
884
|
+
// except with no selector, where EDIABAS has a documented bug
|
|
885
|
+
// (tests >= 0x40000000, so it effectively always jumps); that is
|
|
886
|
+
// emulated deliberately, since SGBDs are compiled against it.
|
|
887
|
+
let hit;
|
|
888
|
+
if (B && B[0] !== OpMode.NONE) {
|
|
889
|
+
const bit = this.val(B, 1);
|
|
890
|
+
hit =
|
|
891
|
+
bit > 0
|
|
892
|
+
? this.trapBit === bit ||
|
|
893
|
+
(this.trapBit === TRAP_UNMAPPED &&
|
|
894
|
+
bit === TRAP_BIT_ALIAS_UNMAPPED)
|
|
895
|
+
: this.trapBit >= TRAP_USER;
|
|
896
|
+
} else {
|
|
897
|
+
hit = name === 'jt' ? this.trapBit >= 0 : this.trapBit >= TRAP_USER;
|
|
898
|
+
}
|
|
899
|
+
if (name === 'jnt') hit = !hit;
|
|
900
|
+
if (!hit) return;
|
|
901
|
+
if (A[1] === null) throw new VmError(`unresolved jump at ${pc}`);
|
|
902
|
+
return A[1];
|
|
903
|
+
}
|
|
904
|
+
case 'clrt':
|
|
905
|
+
this.trapBit = TRAP_CLEAN;
|
|
906
|
+
return;
|
|
907
|
+
case 'jz':
|
|
908
|
+
case 'jnz':
|
|
909
|
+
case 'jc':
|
|
910
|
+
case 'jnc':
|
|
911
|
+
case 'jae':
|
|
912
|
+
case 'jbe':
|
|
913
|
+
case 'ja':
|
|
914
|
+
case 'jb':
|
|
915
|
+
case 'jmi':
|
|
916
|
+
case 'jpl':
|
|
917
|
+
case 'jv':
|
|
918
|
+
case 'jnv':
|
|
919
|
+
case 'jg':
|
|
920
|
+
case 'jge':
|
|
921
|
+
case 'jl':
|
|
922
|
+
case 'jle': {
|
|
923
|
+
const test = JUMP_TESTS[name];
|
|
924
|
+
if (!test) throw new VmError(`no test for ${name}`);
|
|
925
|
+
if (!test(f)) return;
|
|
926
|
+
if (A[1] === null) throw new VmError(`unresolved jump at ${pc}`);
|
|
927
|
+
return A[1];
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// ---- results
|
|
931
|
+
// Each erg* opcode has a FIXED width and signedness, independent of
|
|
932
|
+
// the operand's own type (the reference's result table, ERG_SPECS):
|
|
933
|
+
// ergb unsigned 8 ergc SIGNED 8
|
|
934
|
+
// ergw unsigned 16 ergi SIGNED 16
|
|
935
|
+
// ergd unsigned 32 ergl SIGNED 32
|
|
936
|
+
// Publishing ergi unsigned reported SMG2's coolant temperature as
|
|
937
|
+
// 65531 where the engine says -5.
|
|
938
|
+
//
|
|
939
|
+
// The result NAME is arg0.GetStringData() (textOf): usually an
|
|
940
|
+
// inline literal, but it can equally be a REGISTER -- MS420's VANOS
|
|
941
|
+
// jobs name a result from a register still holding response bytes,
|
|
942
|
+
// and the engine faithfully publishes the resulting garbage key.
|
|
943
|
+
// Assuming a pool index dropped the name to the empty string.
|
|
944
|
+
case 'ergb':
|
|
945
|
+
case 'ergw':
|
|
946
|
+
case 'ergd':
|
|
947
|
+
case 'ergi':
|
|
948
|
+
case 'ergl': {
|
|
949
|
+
const [w, signed] = ERG_SPECS[name];
|
|
950
|
+
let v = this.val(B, w) % 2 ** (8 * w);
|
|
951
|
+
if (signed && v >= 2 ** (8 * w - 1)) v -= 2 ** (8 * w);
|
|
952
|
+
this.emit(this.textOf(A), v);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
case 'ergr': {
|
|
956
|
+
// arg1 is GetFloatData(): a pool literal parses as a number rather
|
|
957
|
+
// than falling through val()'s m===8 case, which returned 0.
|
|
958
|
+
this.emit(this.textOf(A), this.floatOf(B));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
case 'ergs': {
|
|
962
|
+
const txt = this.textOf(B);
|
|
963
|
+
this.emit(this.textOf(A), txt);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
case 'ergy': {
|
|
967
|
+
this.emit(this.textOf(A), Array.from(this.bytes(B)));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
case 'ergc': {
|
|
971
|
+
// SIGNED 8-bit, published as a NUMBER (TypeC), not a character
|
|
972
|
+
let v = this.val(B, 1) & 0xff;
|
|
973
|
+
if (v >= 0x80) v -= 0x100;
|
|
974
|
+
this.emit(this.textOf(A), v);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
case 'enewset':
|
|
978
|
+
this.flush();
|
|
979
|
+
return;
|
|
980
|
+
case 'etag': {
|
|
981
|
+
// `etag target, "NAME"` is a JUMP, not a flag test: if the caller
|
|
982
|
+
// asked for a specific subset of results and NAME is not in it,
|
|
983
|
+
// skip the block that computes it. An empty request set means
|
|
984
|
+
// "everything is wanted", so the jump is never taken -- which is
|
|
985
|
+
// why treating this as a flag left every result uncomputed.
|
|
986
|
+
if (!this.wanted || this.wanted.size === 0) return;
|
|
987
|
+
const nm = B ? this.textOf(B) : '';
|
|
988
|
+
if (this.wanted.has(String(nm || '').toUpperCase())) return;
|
|
989
|
+
if (A[1] === null) throw new VmError(`unresolved etag at ${pc}`);
|
|
990
|
+
return A[1];
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ---- tables
|
|
994
|
+
case 'tabset':
|
|
995
|
+
case 'tabsetex': {
|
|
996
|
+
// Table lookup is CASE-INSENSITIVE (TableNameDict is keyed on
|
|
997
|
+
// ToUpper), and `tabsetex` names the table in arg0 with the SGBD
|
|
998
|
+
// file in arg1 -- an empty file name keeps the current stream.
|
|
999
|
+
//
|
|
1000
|
+
// The external file matters: every group SGBD in data/groups
|
|
1001
|
+
// reaches the variant assignment table with `tabsetex
|
|
1002
|
+
// "ZuordnungsTabelle", "t_grtb"` (or the Motorrad/UDS spelling),
|
|
1003
|
+
// and OpTabsetex (EdOperations.cs) opens THAT file's table stream
|
|
1004
|
+
// for a non-empty name -- a missing file is EDIABAS_SYS_0002 and a
|
|
1005
|
+
// missing table EDIABAS_BIP_0010, and neither falls back to the
|
|
1006
|
+
// current SGBD's tables. Both land in the trap register here
|
|
1007
|
+
// (SYS_0002 folded onto bit 10: this VM's trap model has no
|
|
1008
|
+
// system-error tier, and the observable effect -- the jz/jt guard
|
|
1009
|
+
// after the tabset fires -- is the same).
|
|
1010
|
+
const t = this.textOf(A);
|
|
1011
|
+
let rows;
|
|
1012
|
+
if (name === 'tabsetex') {
|
|
1013
|
+
const file = B ? this.textOf(B) : '';
|
|
1014
|
+
rows = file ? this.findExtTable(file, t) : this.findTable(t);
|
|
1015
|
+
} else {
|
|
1016
|
+
rows = this.findTable(t);
|
|
1017
|
+
}
|
|
1018
|
+
this.table = rows ? { name: t, rows, row: null } : null;
|
|
1019
|
+
// a missing table is EDIABAS_BIP_0010 (trap bit 10); a found one
|
|
1020
|
+
// must CLEAR the trap, or the SGBD's `jt err,#10` guard fires on a
|
|
1021
|
+
// perfectly good table
|
|
1022
|
+
f.zero = !rows;
|
|
1023
|
+
this.trapBit = rows ? TRAP_CLEAN : TRAP_TABLE;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
case 'tabseek':
|
|
1027
|
+
case 'tabseeku': {
|
|
1028
|
+
if (!this.table) {
|
|
1029
|
+
f.zero = true;
|
|
1030
|
+
this.trapBit = TRAP_TABLE;
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
// arg0 is always the COLUMN NAME, arg1 the value being sought.
|
|
1034
|
+
// `tabseek` compares text case-INsensitively; `tabseeku` parses
|
|
1035
|
+
// each cell as a NUMBER (StringToValue: 0x hex, 0y binary, else
|
|
1036
|
+
// decimal) and compares numerically -- which is what makes
|
|
1037
|
+
// JobResult's "0x10" style status bytes match a numeric key.
|
|
1038
|
+
//
|
|
1039
|
+
// DELIBERATELY NO WILDCARD MATCHING, group tables included. The
|
|
1040
|
+
// ZuordnungsTabelle cells look like patterns ("32 .2M. 0550",
|
|
1041
|
+
// "00 ---- 0100"), but the engine's SeekTable (EdiabasNet.cs) is a
|
|
1042
|
+
// plain ToUpper dictionary -- first matching row, exact text -- and
|
|
1043
|
+
// the wildcard walk lives IN the group bytecode: d_0032's
|
|
1044
|
+
// IDENTIFIKATION writes the '.' characters into its seek key
|
|
1045
|
+
// itself (`move L0,#$2e / move S5[L1],B0` at 0x9d6/0xabb) when the
|
|
1046
|
+
// ident chars are in [0-9A-Z], so the key it seeks is byte-equal
|
|
1047
|
+
// to the cell it must hit. A wildcard-aware seek here would match
|
|
1048
|
+
// rows the engine never selects.
|
|
1049
|
+
const col = this.textOf(A);
|
|
1050
|
+
const keyOp = B || A;
|
|
1051
|
+
const numeric = name === 'tabseeku';
|
|
1052
|
+
const keyNum = numeric ? this.val(keyOp) : null;
|
|
1053
|
+
const keyTxt = numeric ? null : this.textOf(keyOp).toLowerCase();
|
|
1054
|
+
const hit = this.table.rows.find((r) => {
|
|
1055
|
+
const c = sgbdTableCell(r, col);
|
|
1056
|
+
if (c === undefined) return false;
|
|
1057
|
+
if (numeric) return Best2Codec.strToValue(String(c)) === keyNum;
|
|
1058
|
+
return String(c).toLowerCase() === keyTxt;
|
|
1059
|
+
});
|
|
1060
|
+
// A VALUE MISS IS NOT AN ERROR: SeekTable returns the LAST data row
|
|
1061
|
+
// and reports not-found, because SGBD tables conventionally put a
|
|
1062
|
+
// catch-all in the last row. Zero==true means "not found" (inverted
|
|
1063
|
+
// from the usual sense), which is the `tabseek / jz` idiom.
|
|
1064
|
+
this.table.row =
|
|
1065
|
+
hit || this.table.rows[this.table.rows.length - 1] || null;
|
|
1066
|
+
f.zero = !hit;
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
case 'tabget': {
|
|
1070
|
+
// Column name from a pool literal OR a register (lit() never
|
|
1071
|
+
// returns nullish, so `lit(B[1]) ?? ...` silently missed every
|
|
1072
|
+
// register-held name). Case-insensitive like tabseek.
|
|
1073
|
+
const col = this.textOf(B);
|
|
1074
|
+
const cell =
|
|
1075
|
+
this.table && this.table.row
|
|
1076
|
+
? sgbdTableCell(this.table.row, col)
|
|
1077
|
+
: undefined;
|
|
1078
|
+
this.storeText(
|
|
1079
|
+
A,
|
|
1080
|
+
cell === undefined || cell === null ? '' : String(cell)
|
|
1081
|
+
);
|
|
1082
|
+
f.zero = cell === undefined || cell === null;
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
case 'tabrows': {
|
|
1086
|
+
// Rows + 1: the header row counts (GetTableRows + 1), while every
|
|
1087
|
+
// row INDEX is 0-based over data rows -- so this is not a loop bound
|
|
1088
|
+
this.store(A, this.table ? this.table.rows.length + 1 : 0);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
case 'tabcols': {
|
|
1092
|
+
this.store(
|
|
1093
|
+
A,
|
|
1094
|
+
this.table && this.table.rows[0]
|
|
1095
|
+
? Object.keys(this.table.rows[0]).length
|
|
1096
|
+
: 0
|
|
1097
|
+
);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
case 'tabline': {
|
|
1101
|
+
// 0-based over DATA rows; an out-of-range index clamps to the last
|
|
1102
|
+
// row and reports Zero=true (GetTableLine), it does not fail
|
|
1103
|
+
const i = this.val(B);
|
|
1104
|
+
if (!this.table) {
|
|
1105
|
+
f.zero = true;
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const inRange = i >= 0 && i < this.table.rows.length;
|
|
1109
|
+
this.table.row = inRange
|
|
1110
|
+
? this.table.rows[i]
|
|
1111
|
+
: this.table.rows[this.table.rows.length - 1] || null;
|
|
1112
|
+
f.zero = !inRange;
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ---- job arguments. ZERO IS THE PRESENCE FLAG, not "value == 0":
|
|
1117
|
+
// par* set Zero=true and only clear it when the requested parameter
|
|
1118
|
+
// exists and is non-empty. Indices are 1-BASED and the decrement is
|
|
1119
|
+
// unsigned, so index 0 underflows and always misses. Without these
|
|
1120
|
+
// flags a job that guards on `pars / jz` ran its with-arguments path
|
|
1121
|
+
// on no arguments and exited before emitting anything.
|
|
1122
|
+
case 'pars': {
|
|
1123
|
+
const parts = this.args();
|
|
1124
|
+
const i = this.val(B) - 1;
|
|
1125
|
+
f.zero = true;
|
|
1126
|
+
let txt = '';
|
|
1127
|
+
if (i >= 0 && i < parts.length && parts[i] !== '') {
|
|
1128
|
+
txt = parts[i];
|
|
1129
|
+
f.zero = false;
|
|
1130
|
+
}
|
|
1131
|
+
this.storeText(A, txt);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
case 'parb':
|
|
1135
|
+
case 'parw':
|
|
1136
|
+
case 'parl':
|
|
1137
|
+
case 'pard':
|
|
1138
|
+
case 'pari': {
|
|
1139
|
+
const parts = this.args();
|
|
1140
|
+
const i = this.val(B) - 1;
|
|
1141
|
+
f.zero = true;
|
|
1142
|
+
f.carry = false;
|
|
1143
|
+
f.sign = false;
|
|
1144
|
+
f.overflow = false;
|
|
1145
|
+
let v = 0;
|
|
1146
|
+
if (i >= 0 && i < parts.length && parts[i] !== '') {
|
|
1147
|
+
v = Best2Codec.strToValue(parts[i]);
|
|
1148
|
+
f.zero = false;
|
|
1149
|
+
}
|
|
1150
|
+
this.store(A, v);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
case 'parr': {
|
|
1154
|
+
const parts = this.args();
|
|
1155
|
+
const i = this.val(B) - 1;
|
|
1156
|
+
f.zero = true;
|
|
1157
|
+
f.carry = false;
|
|
1158
|
+
f.sign = false;
|
|
1159
|
+
f.overflow = false;
|
|
1160
|
+
let v = 0;
|
|
1161
|
+
if (i >= 0 && i < parts.length && parts[i] !== '') {
|
|
1162
|
+
v = Best2Codec.parseNum(parts[i]);
|
|
1163
|
+
f.zero = false;
|
|
1164
|
+
}
|
|
1165
|
+
this.fregs.set(A[1], v);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
case 'parn': {
|
|
1169
|
+
const n = this.args().length;
|
|
1170
|
+
this.store(A, n);
|
|
1171
|
+
f.overflow = false;
|
|
1172
|
+
this.updateFlags(this.val(A), this.widthOf(A));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
case 'pary': {
|
|
1176
|
+
// the WHOLE binary argument blob -- no index, no splitting
|
|
1177
|
+
f.zero = this.argBytes.length === 0;
|
|
1178
|
+
this.store(A, this.argBytes, true);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// ---- telegrams
|
|
1183
|
+
case 'xsend':
|
|
1184
|
+
case 'xsendf':
|
|
1185
|
+
case 'xsendr':
|
|
1186
|
+
case 'xsendex':
|
|
1187
|
+
case 'xrequf':
|
|
1188
|
+
case 'xraw': {
|
|
1189
|
+
// arg0 names the register that RECEIVES the answer; the request is
|
|
1190
|
+
// the second operand where present, else arg0's current contents.
|
|
1191
|
+
const req = B ? this.bytes(B) : this.bytes(A);
|
|
1192
|
+
// THE WRITE GUARD. This is the only point where bytes leave the VM,
|
|
1193
|
+
// so it is the only place the check has to exist. A job whose name
|
|
1194
|
+
// says it CHANGES the ECU refuses to transmit unless the caller
|
|
1195
|
+
// opted in -- the failure happens before the bytes reach the cable
|
|
1196
|
+
// rather than after. Simulation and fixtures are unaffected: they
|
|
1197
|
+
// pass allowWrites, because writing into a .sim file harms nothing.
|
|
1198
|
+
if (this.writeJob && !this.allowWrites) {
|
|
1199
|
+
throw new VmError(
|
|
1200
|
+
`refusing to transmit for write job ${this.jobName}: ` +
|
|
1201
|
+
'construct the VM with {allowWrites: true} to permit it'
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
// The wire parameters ride along: the transport needs the concept
|
|
1205
|
+
// to frame, checksum and pace this exchange. Callers that replay
|
|
1206
|
+
// captured answers just ignore the second argument. A `wait` since
|
|
1207
|
+
// the last exchange becomes waitMs, honored before the write.
|
|
1208
|
+
let comm = this.comm;
|
|
1209
|
+
if (this.pendingWaitMs) {
|
|
1210
|
+
comm = { ...(comm || {}), waitMs: this.pendingWaitMs };
|
|
1211
|
+
this.pendingWaitMs = 0;
|
|
1212
|
+
}
|
|
1213
|
+
const ans = this.send(Array.from(req), comm) || [];
|
|
1214
|
+
if (ans.length > this.arraySize) {
|
|
1215
|
+
// An answer that exceeds the register capacity must not be
|
|
1216
|
+
// half-stored: setS would refuse the write and the job would
|
|
1217
|
+
// decode its own request bytes -- still sitting in the register --
|
|
1218
|
+
// as the ECU's response. Fail loudly instead.
|
|
1219
|
+
throw new VmError(
|
|
1220
|
+
`answer of ${ans.length} bytes exceeds the ` +
|
|
1221
|
+
`job's array size (${this.arraySize})`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
this.answer = Uint8Array.from(ans);
|
|
1225
|
+
this.store(A, this.answer, true);
|
|
1226
|
+
f.zero = this.answer.length === 0;
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// ---- environment / no-ops for decode purposes. These affect timing,
|
|
1231
|
+
// tracing or interface configuration, none of which changes a decoded
|
|
1232
|
+
// value, so they are accepted and ignored rather than aborting a job.
|
|
1233
|
+
//
|
|
1234
|
+
// Process-wide shared data. `shmset key, value` (arg0 is the KEY,
|
|
1235
|
+
// arg1 the value); `shmget dest, key` (arg0 is the DEST). Keys are
|
|
1236
|
+
// uppercased, values persist across jobs in a session -- which is how
|
|
1237
|
+
// MS450 hands its AIF block from one job to the next, using a key
|
|
1238
|
+
// that is literally a single NUL byte.
|
|
1239
|
+
case 'shmset': {
|
|
1240
|
+
this.shared.set(this.shmKey(A), Uint8Array.from(this.bytes(B)));
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
case 'shmget': {
|
|
1244
|
+
// CARRY IS THE MISS INDICATOR (true = key absent); the destination
|
|
1245
|
+
// gets an empty array.
|
|
1246
|
+
const hit = this.shared.get(this.shmKey(B));
|
|
1247
|
+
f.carry = !hit;
|
|
1248
|
+
this.store(A, hit || new Uint8Array(0), true);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
case 'xsetpar': {
|
|
1252
|
+
// CommParameter: the SGBD tells the interface handler how to drive
|
|
1253
|
+
// the wire. The blob DECLARES its own element width in byte 1
|
|
1254
|
+
// (EdOperations.OpXsetpar): 0x00 = 16-bit words, 0x01 = 32-bit
|
|
1255
|
+
// dwords, 0xFF = bytes; anything else is not a CommParameter. That
|
|
1256
|
+
// replaces the old "a dword parse gives an absurd concept" guess.
|
|
1257
|
+
//
|
|
1258
|
+
// The VM does not touch the port; it surfaces {concept, baud,
|
|
1259
|
+
// timeout, ...} to send(), which owns framing and the wire.
|
|
1260
|
+
const raw = Array.from(this.bytes(A));
|
|
1261
|
+
const width = raw.length >= 2 ? COMM_PARAM_WIDTHS[raw[1]] || 0 : 0;
|
|
1262
|
+
let words = [];
|
|
1263
|
+
if (width && raw.length % width === 0) {
|
|
1264
|
+
for (let i = 0; i + width - 1 < raw.length; i += width) {
|
|
1265
|
+
words.push(Best2Codec.leValue(raw, i, width));
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (words.length >= 2 && words[0] > 0 && words[0] <= COMM_CONCEPT_MAX) {
|
|
1269
|
+
this.comm = Best2Codec.decodeCommParams(words);
|
|
1270
|
+
if (this.answerLen) this.comm.answerLen = this.answerLen;
|
|
1271
|
+
}
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
case 'a2y': {
|
|
1275
|
+
// ASCII hex -> bytes: "AB0102" (separators tolerated) into a byte
|
|
1276
|
+
// array. Carry reports a character that is not hex -- conversion
|
|
1277
|
+
// stops there, keeping the bytes parsed so far.
|
|
1278
|
+
const txt = Best2Codec.cstr(this.bytes(B)).trim();
|
|
1279
|
+
const clean = txt.replace(/^0x/i, '');
|
|
1280
|
+
const out = [];
|
|
1281
|
+
let bad = false;
|
|
1282
|
+
for (let i = 0; i < clean.length;) {
|
|
1283
|
+
if (/[\s,;:]/.test(clean[i])) {
|
|
1284
|
+
i += 1;
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
const pair = clean.slice(i, i + 2);
|
|
1288
|
+
if (!/^[0-9a-fA-F]{2}$/.test(pair)) {
|
|
1289
|
+
bad = true;
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
out.push(parseInt(pair, 16));
|
|
1293
|
+
i += 2;
|
|
1294
|
+
}
|
|
1295
|
+
this.store(A, Uint8Array.from(out), true);
|
|
1296
|
+
f.carry = bad;
|
|
1297
|
+
f.zero = out.length === 0;
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
case 'eerr': {
|
|
1301
|
+
// The corpus idiom is `sett n / eerr` inside flash and
|
|
1302
|
+
// authentication jobs. The engine's exact behavior is not
|
|
1303
|
+
// recoverable from this repo, so raise the trapped error loudly --
|
|
1304
|
+
// a clean failure beats a silently-continued flash guard. If a
|
|
1305
|
+
// replay ever shows eerr on a passing path, the differential test
|
|
1306
|
+
// will flag this and it gets refined.
|
|
1307
|
+
throw new VmError(`SGBD raised error via eerr (trap ${this.trapBit})`);
|
|
1308
|
+
}
|
|
1309
|
+
case 'xtype': {
|
|
1310
|
+
// Interface type name. EdInterfaceObd reports "OBD" for the K+DCAN
|
|
1311
|
+
// cable this app drives; SGBDs (carb) branch on it to pick their
|
|
1312
|
+
// concept.
|
|
1313
|
+
this.store(A, Best2Codec.strBytes(INTERFACE_TYPE), true);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
case 'xvers': {
|
|
1317
|
+
// EdInterfaceObd.InterfaceVersion: 209 (0xD1). Not the engine's
|
|
1318
|
+
// 7.3.0 -- the INTERFACE's version, which is what the SGBD asks.
|
|
1319
|
+
this.store(A, INTERFACE_VERSION);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
case 'settmr': {
|
|
1323
|
+
// set_trap_mask: bits of errors the SGBD chooses to handle itself
|
|
1324
|
+
// (EdOperations.OpSettmr / EdiabasNet.SetError: an error whose bit
|
|
1325
|
+
// is masked does not abort the job; the SGBD reads it back with
|
|
1326
|
+
// `trap`/`gettmr`). This is NOT a timer.
|
|
1327
|
+
this.trapMask = this.val(A) >>> 0;
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
case 'gettmr': {
|
|
1331
|
+
this.store(A, this.trapMask >>> 0);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
case 'ticks': {
|
|
1335
|
+
// milliseconds of wall clock (EdOperations.OpTicks: Ticks / 10000)
|
|
1336
|
+
this.store(A, (this.now ? this.now.getTime() : Date.now()) >>> 0);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
case 'date': {
|
|
1340
|
+
// dd.MM.yyyy text, the German convention AIF date fields expect.
|
|
1341
|
+
// this.now (when the caller set one) keeps the bytes identical
|
|
1342
|
+
// across webshim's replay passes -- see the constructor.
|
|
1343
|
+
const d = this.now || new Date();
|
|
1344
|
+
this.storeText(
|
|
1345
|
+
A,
|
|
1346
|
+
`${twoDigits(d.getDate())}.${twoDigits(d.getMonth() + 1)}.${d.getFullYear()}`
|
|
1347
|
+
);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
case 'time': {
|
|
1351
|
+
const d = this.now || new Date();
|
|
1352
|
+
this.storeText(
|
|
1353
|
+
A,
|
|
1354
|
+
`${twoDigits(d.getHours())}:${twoDigits(d.getMinutes())}:${twoDigits(d.getSeconds())}`
|
|
1355
|
+
);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
case 'wait': {
|
|
1359
|
+
// The VM is synchronous, so it cannot sleep -- but the TRANSPORT
|
|
1360
|
+
// can. Accumulate the requested pause and hand it to the next
|
|
1361
|
+
// exchange via comm, where runExchange honors it before writing.
|
|
1362
|
+
// Pacing-sensitive sequences (reset-then-reident, adaptation
|
|
1363
|
+
// clears) stop firing back-to-back on a real wire.
|
|
1364
|
+
//
|
|
1365
|
+
// BEST/2 `wait` is in SECONDS: EdOperations.OpWait sleeps
|
|
1366
|
+
// arg * 1000 ms. Treating it as milliseconds retried the MS45
|
|
1367
|
+
// authentisierung_start key ~1 s too early -- the DME was still
|
|
1368
|
+
// busy, answered nothing, and the retransmit collided with the
|
|
1369
|
+
// late answer into an IFH-0003 cascade. `waitex` is the ms variant.
|
|
1370
|
+
this.pendingWaitMs =
|
|
1371
|
+
(this.pendingWaitMs || 0) + this.val(A) * MS_PER_SECOND;
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
case 'waitex': {
|
|
1375
|
+
// BEST/2 `waitex`: the same pause in MILLISECONDS (OpWaitex).
|
|
1376
|
+
this.pendingWaitMs = (this.pendingWaitMs || 0) + this.val(A);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
case 'generr': {
|
|
1380
|
+
// Deliberately raise an EDIABAS error. Modeled like sett: the
|
|
1381
|
+
// error lands in the trap register where jt/jnt can test it, which
|
|
1382
|
+
// is the trappable half of the engine's behavior. (The engine also
|
|
1383
|
+
// terminates when the error is unmasked; the trap-mask layer is
|
|
1384
|
+
// not decoded, so trapping is the conservative choice -- a job
|
|
1385
|
+
// that meant to die still takes its error path via jt.)
|
|
1386
|
+
this.trapBit = this.val(A) || TRAP_USER;
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
case 'fopen': {
|
|
1390
|
+
// SGBDs open runtime scratch files (ALC_60: c:/ediabas/ecu/
|
|
1391
|
+
// cod_lm.dat -- a coding session's leftovers, absent even on a
|
|
1392
|
+
// fresh real install). No virtual filesystem is provided, so every
|
|
1393
|
+
// open MISSES with honest flags rather than leaving stale
|
|
1394
|
+
// registers: handle 0, zero = failure -- the state a real engine
|
|
1395
|
+
// reports on a clean machine, and what the jobs' own jz guards
|
|
1396
|
+
// expect to handle.
|
|
1397
|
+
this.store(A, 0);
|
|
1398
|
+
f.zero = true;
|
|
1399
|
+
f.carry = true;
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
case 'freadln':
|
|
1403
|
+
case 'fread': {
|
|
1404
|
+
// nothing is ever open (see fopen): empty read, zero = at end
|
|
1405
|
+
this.store(A, new Uint8Array(0), true);
|
|
1406
|
+
f.zero = true;
|
|
1407
|
+
f.carry = true;
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
case 'xawlen': {
|
|
1411
|
+
// set_answer_length (EdOperations.OpXawlen): int16 pairs. For DS2 the
|
|
1412
|
+
// interface derives the answer's total length from it
|
|
1413
|
+
// (EdInterfaceObd.TelLengthDs2): [n>0] fixed n bytes; [-o, k] = the
|
|
1414
|
+
// byte at offset o plus k. zke5/lws5 say [-1, 0]; ms450ds0 [-3, 5].
|
|
1415
|
+
// Carried on comm so the transport reads the SGBD's rule, not a
|
|
1416
|
+
// guessed "length is byte 1".
|
|
1417
|
+
const raw = Array.from(this.bytes(A));
|
|
1418
|
+
const al = [];
|
|
1419
|
+
for (let i = 0; i + 1 < raw.length; i += 2) {
|
|
1420
|
+
const v = raw[i] | (raw[i + 1] << 8);
|
|
1421
|
+
al.push(v & 0x8000 ? v - 0x10000 : v);
|
|
1422
|
+
}
|
|
1423
|
+
this.answerLen = al;
|
|
1424
|
+
this.comm = Object.assign({}, this.comm || {}, { answerLen: al });
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
case 'xconnect':
|
|
1428
|
+
case 'xhangup':
|
|
1429
|
+
case 'xstopf':
|
|
1430
|
+
case 'xreps':
|
|
1431
|
+
case 'xkeyb':
|
|
1432
|
+
case 'xkeybytes':
|
|
1433
|
+
case 'xprog':
|
|
1434
|
+
case 'xreset':
|
|
1435
|
+
case 'setflt':
|
|
1436
|
+
case 'clrflt':
|
|
1437
|
+
case 'cfgig':
|
|
1438
|
+
case 'cfgsg':
|
|
1439
|
+
case 'cfgss':
|
|
1440
|
+
case 'trap':
|
|
1441
|
+
case 'plink':
|
|
1442
|
+
case 'pjob':
|
|
1443
|
+
case 'pexec':
|
|
1444
|
+
case 'fclose':
|
|
1445
|
+
case 'fseekln':
|
|
1446
|
+
case 'fwrite':
|
|
1447
|
+
case 'ergsysi':
|
|
1448
|
+
case 'iupdate':
|
|
1449
|
+
case 'realf':
|
|
1450
|
+
// ergsysi stays a no-op ON PURPOSE: it publishes into result set 0,
|
|
1451
|
+
// which this VM deliberately never synthesizes (the engine injects
|
|
1452
|
+
// set 0; see SYSTEM_RESULTS in test_bestvm.js).
|
|
1453
|
+
return;
|
|
1454
|
+
|
|
1455
|
+
default:
|
|
1456
|
+
// Silence is how a VM produces wrong answers. An unknown opcode
|
|
1457
|
+
// throws, the differential test catches it, and the gap gets
|
|
1458
|
+
// implemented instead of guessed.
|
|
1459
|
+
throw new VmError(`unimplemented opcode ${name} at ${pc}`);
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1465
|
+
module.exports = {
|
|
1466
|
+
JUMP_TESTS,
|
|
1467
|
+
ERG_SPECS,
|
|
1468
|
+
FLAG_CARRY,
|
|
1469
|
+
FLAG_ZERO,
|
|
1470
|
+
FLAG_SIGN,
|
|
1471
|
+
FLAG_OVERFLOW,
|
|
1472
|
+
TRAP_BIT_ALIAS_UNMAPPED,
|
|
1473
|
+
INTERFACE_TYPE,
|
|
1474
|
+
INTERFACE_VERSION,
|
|
1475
|
+
COMM_PARAM_WIDTHS,
|
|
1476
|
+
COMM_CONCEPT_MAX,
|
|
1477
|
+
MS_PER_SECOND,
|
|
1478
|
+
wrapToWidth,
|
|
1479
|
+
negateU32,
|
|
1480
|
+
toInt32,
|
|
1481
|
+
twoDigits,
|
|
1482
|
+
};
|
|
1483
|
+
}
|