bmweb-cli 0.1.6 → 0.1.7
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/README.md +281 -26
- package/dist/bmweb.js +1492 -243
- package/package.json +1 -1
- package/runtime/core/ipofile/encode.js +701 -0
- package/runtime/core/ipofile/exec.js +155 -0
- package/runtime/core/webshim/web-serial-bus.js +59 -10
- package/runtime/screens/ipo-runtime/program.js +8 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmweb-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "BMWeb's tools as a command line: read INPA .IPO scripts, compile .IPS sources, search the corpus job index, decode and diff shared Garage reports, and, over a K+DCAN cable, run jobs, whole-car scans and INPA screens in the terminal.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The exec form -> real .IPO container bytes, the inverse of
|
|
3
|
+
* ipofDecodeExec.
|
|
4
|
+
*
|
|
5
|
+
* exec.js reads an .IPO the way the VM does: it scans for declaration names,
|
|
6
|
+
* walks each proc's tape and inlines the constant pool into every token. That
|
|
7
|
+
* view is what the runtime runs, but it is not the file. The file is a plain
|
|
8
|
+
* block container, and this module writes that container back:
|
|
9
|
+
*
|
|
10
|
+
* HEADER <u8 verHi> <u8 verLo> <magic> 0a
|
|
11
|
+
* BLOCK* <u8 type> <name> 0a <u16 id> <u16 flags>
|
|
12
|
+
* <arg1> 0a <arg2> 0a <u8 marker> <u16 size> <payload>
|
|
13
|
+
*
|
|
14
|
+
* The payload grammar follows the block type: a code block is `size` 4-byte
|
|
15
|
+
* little-endian words (opcode | op1<<8 | op2<<16), the global block is `size`
|
|
16
|
+
* type bytes, the constant block is `size` typed literals, and a logic table is
|
|
17
|
+
* `size` 12-byte rows. Blocks are self-delimiting, so the file tiles with no
|
|
18
|
+
* gaps and no padding -- writing them back to back reproduces the input.
|
|
19
|
+
*
|
|
20
|
+
* WHERE THE EXEC AND THE CONTAINER DISAGREE
|
|
21
|
+
*
|
|
22
|
+
* The walker reports a screen's LINE and a menu's ITEM as tokens INSIDE the
|
|
23
|
+
* enclosing proc, because that is how the tape reads. In the file they are
|
|
24
|
+
* their own blocks, and the bytes the walker calls an "inline header" are that
|
|
25
|
+
* block's header: `nr` is its flags, `label` its arg1, `keys` its arg2 and
|
|
26
|
+
* `dwords` its size. Encoding therefore splits every proc at its ITEM / LINE
|
|
27
|
+
* tokens and emits one block per piece, which is what puts those bytes back
|
|
28
|
+
* where they came from.
|
|
29
|
+
*
|
|
30
|
+
* The few header fields the walker has no token for -- the container version,
|
|
31
|
+
* the magic, each block's marker and the sub-block ids -- are carried on the
|
|
32
|
+
* exec as `container` when it came from a file (ipofDecodeExec records them),
|
|
33
|
+
* and defaulted from the corpus when it came from the compiler, which cannot
|
|
34
|
+
* know them. IPOF_ENCODE_DEFAULTS documents each default and why it is safe.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Block type bytes, as the container numbers them. */
|
|
38
|
+
const IPOF_BLOCK_SCREEN = 0x01;
|
|
39
|
+
const IPOF_BLOCK_MENU = 0x02;
|
|
40
|
+
const IPOF_BLOCK_STATEMACHINE = 0x03;
|
|
41
|
+
// 0x04 is the logic table; no declaration ever names one, so nothing here
|
|
42
|
+
// writes one from tokens -- a file that has one carries it in `container`.
|
|
43
|
+
const IPOF_BLOCK_FUNCTION = 0x05;
|
|
44
|
+
const IPOF_BLOCK_GLOBALDATA = 0x11;
|
|
45
|
+
const IPOF_BLOCK_CONSTANTDATA = 0x12;
|
|
46
|
+
const IPOF_BLOCK_SCREENFUNC = 0x21;
|
|
47
|
+
const IPOF_BLOCK_LINEFUNC = 0x22;
|
|
48
|
+
const IPOF_BLOCK_MENUITEMFUNC = 0x24;
|
|
49
|
+
const IPOF_BLOCK_STATEFUNC = 0x25;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The exec's declaration kind -> the container's block type byte.
|
|
53
|
+
*
|
|
54
|
+
* decls.js labels type 3 "state" and type 4 "statemachine"; the container calls
|
|
55
|
+
* type 3 STATEMACHINE and reserves 4 for a logic table, which never carries a
|
|
56
|
+
* declaration name. Both of the decoder's spellings therefore map onto 3, so a
|
|
57
|
+
* file decoded under either label re-encodes to the byte it was read from.
|
|
58
|
+
*/
|
|
59
|
+
const IPOF_KIND_BLOCK = {
|
|
60
|
+
screen: IPOF_BLOCK_SCREEN,
|
|
61
|
+
menu: IPOF_BLOCK_MENU,
|
|
62
|
+
state: IPOF_BLOCK_STATEMACHINE,
|
|
63
|
+
statemachine: IPOF_BLOCK_STATEMACHINE,
|
|
64
|
+
func: IPOF_BLOCK_FUNCTION,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The sub-block type a section of a proc of this kind gets.
|
|
69
|
+
*
|
|
70
|
+
* A screen's first section is its SCREENFUNC and every later one a LINEFUNC; a
|
|
71
|
+
* menu's sections are MENUITEMFUNCs and a state machine's are STATEFUNCs. The
|
|
72
|
+
* whole corpus follows that rule with no exception, which is what lets a file
|
|
73
|
+
* whose sub-block types were never tokenised come back byte-exact.
|
|
74
|
+
*
|
|
75
|
+
* @param {number} parent The enclosing block's type byte.
|
|
76
|
+
* @param {number} index The section's position within the proc, from 0.
|
|
77
|
+
* @returns {number} The sub-block's type byte.
|
|
78
|
+
*/
|
|
79
|
+
function ipofSubBlockType(parent, index) {
|
|
80
|
+
if (parent === IPOF_BLOCK_MENU) return IPOF_BLOCK_MENUITEMFUNC;
|
|
81
|
+
if (parent === IPOF_BLOCK_STATEMACHINE) return IPOF_BLOCK_STATEFUNC;
|
|
82
|
+
return index === 0 ? IPOF_BLOCK_SCREENFUNC : IPOF_BLOCK_LINEFUNC;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Defaults for the container fields a compiled-from-source exec cannot know.
|
|
87
|
+
*
|
|
88
|
+
* A source file says nothing about the container it will live in, so these come
|
|
89
|
+
* from the corpus: v5.0 is the dialect every current INPA ships and the one the
|
|
90
|
+
* walker's builtin numbering matches, "TEST-Infotext" is the magic on 1788 of
|
|
91
|
+
* the 1790 shipped files, and marker 0 is what every block but four carries.
|
|
92
|
+
*/
|
|
93
|
+
const IPOF_ENCODE_DEFAULTS = {
|
|
94
|
+
verHi: 5,
|
|
95
|
+
verLo: 0,
|
|
96
|
+
magic: 'TEST-Infotext',
|
|
97
|
+
marker: 0,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Pool type letters (the walker's `t`) -> the v5.x ValueType byte. */
|
|
101
|
+
const IPOF_TAG_VT5 = { b: 0x01, y: 0x02, i: 0x03, l: 0x04, d: 0x05, s: 0x06 };
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Pool type letters -> the v1.x ValueType byte.
|
|
105
|
+
*
|
|
106
|
+
* The v1.x dialect numbers its literals differently: string is 04 where v5 uses
|
|
107
|
+
* 06, int is 02 where v5 uses 03. Encoding a v1 file with the v5 table would
|
|
108
|
+
* write a pool INPA cannot read, so the version picks the table.
|
|
109
|
+
*/
|
|
110
|
+
const IPOF_TAG_VT1 = { b: 0x01, i: 0x02, l: 0x03, s: 0x04, d: 0x05 };
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The literal-type table a container version uses.
|
|
114
|
+
*
|
|
115
|
+
* @param {number} verHi The container's major version byte.
|
|
116
|
+
* @returns {Object<string, number>} Type letter -> ValueType byte.
|
|
117
|
+
*/
|
|
118
|
+
function ipofVtTable(verHi) {
|
|
119
|
+
return verHi === 1 ? IPOF_TAG_VT1 : IPOF_TAG_VT5;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A growable byte sink with the writes the container needs.
|
|
124
|
+
*/
|
|
125
|
+
class IpofWriter {
|
|
126
|
+
/** Start empty. */
|
|
127
|
+
constructor() {
|
|
128
|
+
this.buf = new Uint8Array(1024);
|
|
129
|
+
this.len = 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Make room for `n` more bytes.
|
|
134
|
+
* @param {number} n How many bytes are about to be written.
|
|
135
|
+
* @returns {void}
|
|
136
|
+
*/
|
|
137
|
+
need(n) {
|
|
138
|
+
if (this.len + n <= this.buf.length) return;
|
|
139
|
+
let cap = this.buf.length * 2;
|
|
140
|
+
while (cap < this.len + n) cap *= 2;
|
|
141
|
+
const next = new Uint8Array(cap);
|
|
142
|
+
next.set(this.buf.subarray(0, this.len));
|
|
143
|
+
this.buf = next;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Append one byte.
|
|
148
|
+
* @param {number} v The value; only the low 8 bits are written.
|
|
149
|
+
* @returns {void}
|
|
150
|
+
*/
|
|
151
|
+
u8(v) {
|
|
152
|
+
this.need(1);
|
|
153
|
+
this.buf[this.len] = v & 0xff;
|
|
154
|
+
this.len += 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Append a little-endian unsigned 16-bit word.
|
|
159
|
+
* @param {number} v The value.
|
|
160
|
+
* @returns {void}
|
|
161
|
+
*/
|
|
162
|
+
u16(v) {
|
|
163
|
+
this.u8(v);
|
|
164
|
+
this.u8(v >> 8);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Append a little-endian 32-bit word.
|
|
169
|
+
* @param {number} v The value.
|
|
170
|
+
* @returns {void}
|
|
171
|
+
*/
|
|
172
|
+
u32(v) {
|
|
173
|
+
this.u16(v);
|
|
174
|
+
this.u16(v >>> 16);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Append a little-endian IEEE754 double, the pool's `real`.
|
|
179
|
+
* @param {number} v The value.
|
|
180
|
+
* @returns {void}
|
|
181
|
+
*/
|
|
182
|
+
f64(v) {
|
|
183
|
+
const b = new Uint8Array(8);
|
|
184
|
+
new DataView(b.buffer).setFloat64(0, v, true);
|
|
185
|
+
this.raw(b);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Append raw bytes.
|
|
190
|
+
* @param {Uint8Array|number[]} b The bytes.
|
|
191
|
+
* @returns {void}
|
|
192
|
+
*/
|
|
193
|
+
raw(b) {
|
|
194
|
+
this.need(b.length);
|
|
195
|
+
for (let i = 0; i < b.length; i += 1) this.buf[this.len + i] = b[i] & 0xff;
|
|
196
|
+
this.len += b.length;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Append text as Latin-1, the encoding the container stores names and
|
|
201
|
+
* literals in.
|
|
202
|
+
*
|
|
203
|
+
* A character above U+00FF has no Latin-1 byte. Writing a replacement would
|
|
204
|
+
* put a different string in the file than the caller asked for, so it is an
|
|
205
|
+
* error naming the text instead.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} s The text.
|
|
208
|
+
* @param {string} what What this string is, for the error message.
|
|
209
|
+
* @returns {void}
|
|
210
|
+
*/
|
|
211
|
+
latin1(s, what) {
|
|
212
|
+
const str = String(s === undefined || s === null ? '' : s);
|
|
213
|
+
this.need(str.length);
|
|
214
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
215
|
+
const c = str.charCodeAt(i);
|
|
216
|
+
if (c > 0xff) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`${what}: "${str}" holds a character the container cannot store ` +
|
|
219
|
+
`(U+${c.toString(16).toUpperCase()}); .IPO text is Latin-1`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
this.buf[this.len + i] = c;
|
|
223
|
+
}
|
|
224
|
+
this.len += str.length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Append text then the 0x0a separator that terminates every container
|
|
229
|
+
* string.
|
|
230
|
+
* @param {string} s The text.
|
|
231
|
+
* @param {string} what What this string is, for the error message.
|
|
232
|
+
* @returns {void}
|
|
233
|
+
*/
|
|
234
|
+
strz(s, what) {
|
|
235
|
+
this.latin1(s, what);
|
|
236
|
+
this.u8(0x0a);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The bytes written so far.
|
|
241
|
+
* @returns {Uint8Array} A copy sized to the content.
|
|
242
|
+
*/
|
|
243
|
+
bytes() {
|
|
244
|
+
return this.buf.slice(0, this.len);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Encode one instruction token back to its 4-byte word.
|
|
250
|
+
*
|
|
251
|
+
* This is the exact inverse of the walker's decode ladder: each branch there
|
|
252
|
+
* recognised a (b0, b1) pair and read a u16, so each case here writes that pair
|
|
253
|
+
* and that u16 back. `jump` and `jfalse` are the one place the token is not
|
|
254
|
+
* self-contained -- the walker resolved the target to an absolute byte offset,
|
|
255
|
+
* so the dword index has to be recomputed against the block the jump lives in.
|
|
256
|
+
*
|
|
257
|
+
* @param {IpofWriter} w The sink.
|
|
258
|
+
* @param {Object} t The token.
|
|
259
|
+
* @param {number} base The byte offset dword 0 of the enclosing block sits at.
|
|
260
|
+
* @param {string} where The proc name, for error messages.
|
|
261
|
+
* @returns {void}
|
|
262
|
+
*/
|
|
263
|
+
function ipofEncodeToken(w, t, base, where) {
|
|
264
|
+
/**
|
|
265
|
+
* Write one instruction word.
|
|
266
|
+
* @param {number} b0 The opcode byte.
|
|
267
|
+
* @param {number} b1 The first operand byte.
|
|
268
|
+
* @param {number} u16 The second operand word.
|
|
269
|
+
* @returns {void}
|
|
270
|
+
*/
|
|
271
|
+
const word = (b0, b1, u16) => {
|
|
272
|
+
w.u8(b0);
|
|
273
|
+
w.u8(b1);
|
|
274
|
+
w.u16(u16);
|
|
275
|
+
};
|
|
276
|
+
switch (t.op) {
|
|
277
|
+
case 'const':
|
|
278
|
+
word(0x01, 0x01, t.n);
|
|
279
|
+
return;
|
|
280
|
+
case 'var':
|
|
281
|
+
word(t.ref ? 0x03 : 0x01, t.sc, t.n);
|
|
282
|
+
return;
|
|
283
|
+
case 'store':
|
|
284
|
+
word(t.ref ? 0x07 : 0x06, t.sc, t.n);
|
|
285
|
+
return;
|
|
286
|
+
case 'decl': {
|
|
287
|
+
const code = IPOF_DECL_LOCAL_CODE[t.type];
|
|
288
|
+
if (code === undefined)
|
|
289
|
+
throw new Error(`${where}: no local type byte for "${t.type}"`);
|
|
290
|
+
word(0x08, code, 0);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
case 'procref':
|
|
294
|
+
word(0x02, t.kind, t.n);
|
|
295
|
+
return;
|
|
296
|
+
case 'binop':
|
|
297
|
+
// the operator byte is the token's `n`, which the walker read from the
|
|
298
|
+
// middle two bytes: b1 is its low half and the u16's low byte its high
|
|
299
|
+
w.u8(0x09);
|
|
300
|
+
w.u16(t.n);
|
|
301
|
+
w.u8(0x00);
|
|
302
|
+
return;
|
|
303
|
+
case 'block':
|
|
304
|
+
word(0x0a, 0x00, t.dwords);
|
|
305
|
+
return;
|
|
306
|
+
case 'jump':
|
|
307
|
+
case 'jfalse': {
|
|
308
|
+
const rel = (t.to - base) / 4;
|
|
309
|
+
if (!Number.isInteger(rel) || rel < 0 || rel > 0xffff) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`${where}: a ${t.op} to byte ${t.to} is not a dword offset from ` +
|
|
312
|
+
`block base ${base}; the exec's offsets are inconsistent`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
word(t.op === 'jump' ? 0x0a : 0x0b, 0x00, rel);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
case 'ret':
|
|
319
|
+
word(0x0e, 0x00, 0);
|
|
320
|
+
return;
|
|
321
|
+
case 'endproc':
|
|
322
|
+
word(0x0d, 0x00, 0);
|
|
323
|
+
return;
|
|
324
|
+
case 'dllcall':
|
|
325
|
+
word(0x0d, 0x01, t.n);
|
|
326
|
+
return;
|
|
327
|
+
case 'stmt':
|
|
328
|
+
word(0x05, 0x00, t.n);
|
|
329
|
+
return;
|
|
330
|
+
case 'calluser':
|
|
331
|
+
word(0x0c, 0x80, t.n);
|
|
332
|
+
return;
|
|
333
|
+
case 'call':
|
|
334
|
+
word(0x0c, 0x81, t.n);
|
|
335
|
+
return;
|
|
336
|
+
case 'frame':
|
|
337
|
+
// The walker matches b0 == 0x0f alone and never reads the operands, so
|
|
338
|
+
// the token cannot say what they were. Zero is not a guess: every one of
|
|
339
|
+
// the 582,688 frame instructions in the shipped corpus carries 0f 00
|
|
340
|
+
// 00 00, so writing that back is what the format actually holds.
|
|
341
|
+
word(0x0f, 0x00, 0);
|
|
342
|
+
return;
|
|
343
|
+
case 'unk': {
|
|
344
|
+
// bytes the walker could not name, kept verbatim as hex
|
|
345
|
+
const hex = String(t.bytes || '');
|
|
346
|
+
if (!/^[0-9a-f]{8}$/i.test(hex))
|
|
347
|
+
throw new Error(`${where}: an unk token carries no 4 bytes ("${hex}")`);
|
|
348
|
+
for (let i = 0; i < 4; i += 1) w.u8(parseInt(hex.substr(i * 2, 2), 16));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
case 'state':
|
|
352
|
+
// `%NAME \n <u32 index> 0a`, sitting where a token would
|
|
353
|
+
w.strz(t.name, `${where}: state label`);
|
|
354
|
+
w.u32(t.index);
|
|
355
|
+
w.u8(0x0a);
|
|
356
|
+
return;
|
|
357
|
+
default:
|
|
358
|
+
throw new Error(`${where}: cannot encode a "${t.op}" token`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Local declaration type name -> its opcode-08 operand byte. */
|
|
363
|
+
const IPOF_DECL_LOCAL_CODE = (() => {
|
|
364
|
+
const out = {};
|
|
365
|
+
for (const k of Object.keys(IPOF_DECL_LOCALS))
|
|
366
|
+
out[IPOF_DECL_LOCALS[k]] = Number(k);
|
|
367
|
+
return out;
|
|
368
|
+
})();
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Split one proc's tokens into the blocks the container stores them as.
|
|
372
|
+
*
|
|
373
|
+
* Everything up to the first ITEM / LINE token is the proc's own block; each
|
|
374
|
+
* ITEM / LINE token opens a new block whose header those token fields fill and
|
|
375
|
+
* whose payload is the tokens that follow it.
|
|
376
|
+
*
|
|
377
|
+
* @param {Object[]} toks The proc's tokens.
|
|
378
|
+
* @returns {Array<{head: Object|null, toks: Object[]}>} The pieces, the first
|
|
379
|
+
* of which is the proc's own body (`head` null).
|
|
380
|
+
*/
|
|
381
|
+
function ipofSplitSections(toks) {
|
|
382
|
+
const out = [{ head: null, toks: [] }];
|
|
383
|
+
for (const t of toks || []) {
|
|
384
|
+
if (t.op === 'ITEM' || t.op === 'LINE') out.push({ head: t, toks: [] });
|
|
385
|
+
else out[out.length - 1].toks.push(t);
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Write one block header and its code payload.
|
|
392
|
+
*
|
|
393
|
+
* @param {IpofWriter} w The sink.
|
|
394
|
+
* @param {Object} b The block: type, name, id, flags, arg1, arg2, marker.
|
|
395
|
+
* @param {Object[]} toks The tokens of its payload.
|
|
396
|
+
* @param {string} where The proc name, for error messages.
|
|
397
|
+
* @returns {void}
|
|
398
|
+
*/
|
|
399
|
+
function ipofWriteCodeBlock(w, b, toks, where) {
|
|
400
|
+
w.u8(b.type);
|
|
401
|
+
w.strz(b.name || '', `${where}: block name`);
|
|
402
|
+
w.u16(b.id || 0);
|
|
403
|
+
w.u16(b.flags || 0);
|
|
404
|
+
w.strz(b.arg1 || '', `${where}: block arg1`);
|
|
405
|
+
w.strz(b.arg2 || '', `${where}: block arg2`);
|
|
406
|
+
w.u8(b.marker === undefined ? IPOF_ENCODE_DEFAULTS.marker : b.marker);
|
|
407
|
+
// `size` counts dwords, and a state label is not one: it occupies the space
|
|
408
|
+
// of a token but the container sizes the payload in 4-byte units, so the
|
|
409
|
+
// count is the payload's byte length over 4 rather than the token count.
|
|
410
|
+
const body = new IpofWriter();
|
|
411
|
+
const base = 0;
|
|
412
|
+
for (const t of toks) ipofEncodeToken(body, t, base, where);
|
|
413
|
+
if (body.len % 4 !== 0) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`${where}: a block payload of ${body.len} bytes is not a whole number ` +
|
|
416
|
+
'of instruction words'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
w.u16(body.len / 4);
|
|
420
|
+
w.raw(body.bytes());
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Re-base a proc's jump targets onto its own block.
|
|
425
|
+
*
|
|
426
|
+
* The walker resolved every jump to an absolute file offset using the base it
|
|
427
|
+
* was tracking; the encoder writes each block from zero, so the targets are
|
|
428
|
+
* shifted by the block's own start. Doing it here keeps ipofEncodeToken a pure
|
|
429
|
+
* per-token inverse.
|
|
430
|
+
*
|
|
431
|
+
* @param {Object[]} toks The tokens of one block's payload.
|
|
432
|
+
* @param {number} origin The absolute offset the block's dword 0 sat at, or
|
|
433
|
+
* null when the exec carries no offsets.
|
|
434
|
+
* @returns {Object[]} Tokens whose jump targets count from the block start.
|
|
435
|
+
*/
|
|
436
|
+
function ipofRebaseJumps(toks, origin) {
|
|
437
|
+
if (origin === null || origin === undefined) return toks;
|
|
438
|
+
return toks.map((t) => {
|
|
439
|
+
if (t.op !== 'jump' && t.op !== 'jfalse') return t;
|
|
440
|
+
return Object.assign({}, t, { to: t.to - origin });
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The byte offset the first token of a token list sat at, or null.
|
|
446
|
+
*
|
|
447
|
+
* @param {Object[]} toks The tokens.
|
|
448
|
+
* @returns {number|null} The offset, or null when none carries one.
|
|
449
|
+
*/
|
|
450
|
+
function ipofFirstAt(toks) {
|
|
451
|
+
for (const t of toks) if (typeof t.at === 'number') return t.at;
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Encode a constant pool entry.
|
|
457
|
+
*
|
|
458
|
+
* @param {IpofWriter} w The sink.
|
|
459
|
+
* @param {Array} e The entry, `[typeLetter, value]`.
|
|
460
|
+
* @param {Object<string, number>} vt Type letter -> ValueType byte.
|
|
461
|
+
* @param {number} i The entry's index, for error messages.
|
|
462
|
+
* @returns {void}
|
|
463
|
+
*/
|
|
464
|
+
function ipofWritePoolEntry(w, e, vt, i) {
|
|
465
|
+
const tag = e[0];
|
|
466
|
+
const v = e[1];
|
|
467
|
+
const t = vt[tag];
|
|
468
|
+
if (t === undefined)
|
|
469
|
+
throw new Error(`constant ${i}: no ValueType for a "${tag}" literal`);
|
|
470
|
+
w.u8(t);
|
|
471
|
+
if (tag === 's') {
|
|
472
|
+
w.strz(v, `constant ${i}`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (tag === 'd') {
|
|
476
|
+
w.f64(Number(v));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (tag === 'b' || tag === 'y') {
|
|
480
|
+
w.u8(Number(v));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (tag === 'i') {
|
|
484
|
+
// An `int` slot is 16 bits. The compiler tags every whole-number literal
|
|
485
|
+
// `int` without checking the range, so a source saying 65536 would be
|
|
486
|
+
// written as 0 -- a different program, silently. Refusing it instead is
|
|
487
|
+
// the only honest option: the caller must widen the literal.
|
|
488
|
+
const n = Number(v);
|
|
489
|
+
if (!Number.isInteger(n) || n < -0x8000 || n > 0xffff) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`constant ${i}: ${n} does not fit the container's 16-bit int; ` +
|
|
492
|
+
'declare the literal as a long'
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
w.u16(n);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (tag === 'l') {
|
|
499
|
+
w.u32(Number(v));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
throw new Error(`constant ${i}: cannot encode a "${tag}" literal`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Rebuild the constant pool from the values the walker inlined into tokens.
|
|
507
|
+
*
|
|
508
|
+
* The exec has no pool array of its own: every `const` token carries its index
|
|
509
|
+
* `n`, its type letter `t` and its value `v`. Collecting them by index
|
|
510
|
+
* reconstructs the pool, and the entry count the header declares is the highest
|
|
511
|
+
* index used plus one.
|
|
512
|
+
*
|
|
513
|
+
* @param {Object} exec The exec object.
|
|
514
|
+
* @returns {Array<Array>} The pool entries by index.
|
|
515
|
+
*/
|
|
516
|
+
function ipofPoolFromTokens(exec) {
|
|
517
|
+
const byIndex = [];
|
|
518
|
+
for (const name of Object.keys(exec.procs || {})) {
|
|
519
|
+
for (const t of exec.procs[name] || []) {
|
|
520
|
+
if (t.op !== 'const') continue;
|
|
521
|
+
if (byIndex[t.n] !== undefined) continue;
|
|
522
|
+
let tag = t.t;
|
|
523
|
+
// The compiler tags every whole-number literal `int` without checking
|
|
524
|
+
// that it fits 16 bits, because the exec form holds a JS number and
|
|
525
|
+
// never had to. The pool does have to, so a literal too big for an int
|
|
526
|
+
// slot is stored in the 32-bit one; the value is what the script meant,
|
|
527
|
+
// and the wider type is the only one that can hold it.
|
|
528
|
+
if (tag === 'i') {
|
|
529
|
+
const n = Number(t.v);
|
|
530
|
+
if (Number.isInteger(n) && (n < -0x8000 || n > 0xffff)) tag = 'l';
|
|
531
|
+
}
|
|
532
|
+
byIndex[t.n] = [tag, t.v];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
for (let i = 0; i < byIndex.length; i += 1) {
|
|
536
|
+
// an index no token referenced still needs a slot, or every later index
|
|
537
|
+
// shifts; a zero int is the smallest filler that keeps the stream legal
|
|
538
|
+
if (byIndex[i] === undefined) byIndex[i] = ['i', 0];
|
|
539
|
+
}
|
|
540
|
+
return byIndex;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Encode an exec object as .IPO container bytes.
|
|
545
|
+
*
|
|
546
|
+
* When `exec.container` is present -- ipofDecodeExec records it for a file it
|
|
547
|
+
* read -- its blocks are written back exactly as they were read, which makes
|
|
548
|
+
* the round trip byte-exact. Without it the container is rebuilt from the
|
|
549
|
+
* tokens alone and the missing header fields take IPOF_ENCODE_DEFAULTS, which
|
|
550
|
+
* is the path a script compiled from source takes.
|
|
551
|
+
*
|
|
552
|
+
* @param {Object} exec The exec object from ipofDecodeExec or the compiler.
|
|
553
|
+
* @param {Object} [opts] Options.
|
|
554
|
+
* @param {number} [opts.verHi] Container major version.
|
|
555
|
+
* @param {number} [opts.verLo] Container minor version.
|
|
556
|
+
* @param {string} [opts.magic] Container magic string.
|
|
557
|
+
* @returns {Uint8Array} The .IPO bytes.
|
|
558
|
+
*/
|
|
559
|
+
function ipofEncode(exec, opts) {
|
|
560
|
+
const o = opts || {};
|
|
561
|
+
const c = exec && exec.container ? exec.container : null;
|
|
562
|
+
if (c && c.blocks) return ipofEncodeContainer(c, o);
|
|
563
|
+
return ipofEncodeFromTokens(exec, o);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Write a container the decoder recorded, block for block.
|
|
568
|
+
*
|
|
569
|
+
* @param {Object} c The recorded container.
|
|
570
|
+
* @param {Object} o Options overriding the version and magic.
|
|
571
|
+
* @returns {Uint8Array} The .IPO bytes.
|
|
572
|
+
*/
|
|
573
|
+
function ipofEncodeContainer(c, o) {
|
|
574
|
+
const w = new IpofWriter();
|
|
575
|
+
w.u8(o.verHi === undefined ? c.verHi : o.verHi);
|
|
576
|
+
w.u8(o.verLo === undefined ? c.verLo : o.verLo);
|
|
577
|
+
w.strz(o.magic === undefined ? c.magic : o.magic, 'the file magic');
|
|
578
|
+
for (const b of c.blocks) {
|
|
579
|
+
w.u8(b.type);
|
|
580
|
+
w.strz(b.name || '', 'a block name');
|
|
581
|
+
w.u16(b.id || 0);
|
|
582
|
+
w.u16(b.flags || 0);
|
|
583
|
+
w.strz(b.arg1 || '', 'a block arg1');
|
|
584
|
+
w.strz(b.arg2 || '', 'a block arg2');
|
|
585
|
+
w.u8(b.marker || 0);
|
|
586
|
+
w.u16(b.size || 0);
|
|
587
|
+
w.raw(b.payload);
|
|
588
|
+
}
|
|
589
|
+
return w.bytes();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Build a container from the exec's tokens alone.
|
|
594
|
+
*
|
|
595
|
+
* @param {Object} exec The exec object.
|
|
596
|
+
* @param {Object} o Options overriding the version and magic.
|
|
597
|
+
* @returns {Uint8Array} The .IPO bytes.
|
|
598
|
+
*/
|
|
599
|
+
function ipofEncodeFromTokens(exec, o) {
|
|
600
|
+
if (!exec || !exec.procs) throw new Error('ipofEncode: not an exec object');
|
|
601
|
+
const verHi = o.verHi === undefined ? IPOF_ENCODE_DEFAULTS.verHi : o.verHi;
|
|
602
|
+
const verLo = o.verLo === undefined ? IPOF_ENCODE_DEFAULTS.verLo : o.verLo;
|
|
603
|
+
const magic = o.magic === undefined ? IPOF_ENCODE_DEFAULTS.magic : o.magic;
|
|
604
|
+
const w = new IpofWriter();
|
|
605
|
+
w.u8(verHi);
|
|
606
|
+
w.u8(verLo);
|
|
607
|
+
w.strz(magic, 'the file magic');
|
|
608
|
+
// proc name -> its declared kind and id, from the byid table the exec carries
|
|
609
|
+
const kindOf = {};
|
|
610
|
+
const idOf = {};
|
|
611
|
+
for (const key of Object.keys(exec.byid || {})) {
|
|
612
|
+
const cut = key.indexOf(':');
|
|
613
|
+
kindOf[exec.byid[key]] = key.slice(0, cut);
|
|
614
|
+
idOf[exec.byid[key]] = Number(key.slice(cut + 1));
|
|
615
|
+
}
|
|
616
|
+
for (const name of Object.keys(exec.procs)) {
|
|
617
|
+
const kind = kindOf[name] || 'func';
|
|
618
|
+
const type = IPOF_KIND_BLOCK[kind];
|
|
619
|
+
if (type === undefined)
|
|
620
|
+
throw new Error(`${name}: no block type for a "${kind}" declaration`);
|
|
621
|
+
const parts = ipofSplitSections(exec.procs[name]);
|
|
622
|
+
parts.forEach((part, k) => {
|
|
623
|
+
const head = part.head;
|
|
624
|
+
const origin = ipofFirstAt(part.toks);
|
|
625
|
+
const toks = ipofRebaseJumps(part.toks, origin);
|
|
626
|
+
if (k === 0) {
|
|
627
|
+
ipofWriteCodeBlock(
|
|
628
|
+
w,
|
|
629
|
+
{
|
|
630
|
+
type,
|
|
631
|
+
name,
|
|
632
|
+
id: idOf[name] || 0,
|
|
633
|
+
flags: 0,
|
|
634
|
+
arg1: '',
|
|
635
|
+
arg2: '',
|
|
636
|
+
marker: IPOF_ENCODE_DEFAULTS.marker,
|
|
637
|
+
},
|
|
638
|
+
toks,
|
|
639
|
+
name
|
|
640
|
+
);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
ipofWriteCodeBlock(
|
|
644
|
+
w,
|
|
645
|
+
{
|
|
646
|
+
type: ipofSubBlockType(type, k - 1),
|
|
647
|
+
name: '',
|
|
648
|
+
id: 0,
|
|
649
|
+
flags: head.nr || 0,
|
|
650
|
+
arg1: head.label || '',
|
|
651
|
+
arg2: head.keys || '',
|
|
652
|
+
marker: IPOF_ENCODE_DEFAULTS.marker,
|
|
653
|
+
},
|
|
654
|
+
toks,
|
|
655
|
+
name
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
// the global-slot type table, then the pool -- the order every file uses
|
|
660
|
+
const globals = (exec.container && exec.container.globals) || [];
|
|
661
|
+
w.u8(IPOF_BLOCK_GLOBALDATA);
|
|
662
|
+
w.strz('Global Data', 'the global block name');
|
|
663
|
+
w.u16(0);
|
|
664
|
+
w.u16(0);
|
|
665
|
+
w.strz('', 'the global block arg1');
|
|
666
|
+
w.strz('', 'the global block arg2');
|
|
667
|
+
w.u8(IPOF_ENCODE_DEFAULTS.marker);
|
|
668
|
+
w.u16(globals.length);
|
|
669
|
+
w.raw(globals);
|
|
670
|
+
const pool = ipofPoolFromTokens(exec);
|
|
671
|
+
const vt = ipofVtTable(verHi);
|
|
672
|
+
const body = new IpofWriter();
|
|
673
|
+
pool.forEach((e, i) => ipofWritePoolEntry(body, e, vt, i));
|
|
674
|
+
w.u8(IPOF_BLOCK_CONSTANTDATA);
|
|
675
|
+
w.strz('Constant Data', 'the constant block name');
|
|
676
|
+
w.u16(0);
|
|
677
|
+
w.u16(0);
|
|
678
|
+
w.strz('', 'the constant block arg1');
|
|
679
|
+
w.strz('', 'the constant block arg2');
|
|
680
|
+
w.u8(IPOF_ENCODE_DEFAULTS.marker);
|
|
681
|
+
w.u16(pool.length);
|
|
682
|
+
w.raw(body.bytes());
|
|
683
|
+
return w.bytes();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
687
|
+
module.exports = {
|
|
688
|
+
ipofEncode,
|
|
689
|
+
IpofWriter,
|
|
690
|
+
ipofEncodeToken,
|
|
691
|
+
ipofWritePoolEntry,
|
|
692
|
+
ipofPoolFromTokens,
|
|
693
|
+
ipofSplitSections,
|
|
694
|
+
ipofSubBlockType,
|
|
695
|
+
ipofVtTable,
|
|
696
|
+
IPOF_ENCODE_DEFAULTS,
|
|
697
|
+
IPOF_TAG_VT5,
|
|
698
|
+
IPOF_TAG_VT1,
|
|
699
|
+
IPOF_KIND_BLOCK,
|
|
700
|
+
};
|
|
701
|
+
}
|