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