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,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The AST -> the same token stream the walker reads out of an .IPO.
|
|
3
|
+
*
|
|
4
|
+
* The emitter's contract is that its output is indistinguishable from a
|
|
5
|
+
* decoded file's: one 4-byte slot per ordinary token, an inline header's real
|
|
6
|
+
* length for ITEM / LINE, and `at` / `to` as absolute byte offsets in the same
|
|
7
|
+
* coordinate system walk() reports. That is what lets one runtime run a
|
|
8
|
+
* compiled script and a decoded one without knowing which it has.
|
|
9
|
+
*
|
|
10
|
+
* Control flow compiles to the shapes the decompiler recognises, so a
|
|
11
|
+
* decompile-then-compile round trip lands back on the same structure:
|
|
12
|
+
*
|
|
13
|
+
* cond stmt jfalse->END if
|
|
14
|
+
* cond stmt jfalse->ELSE ... jump->END ELSE if / else
|
|
15
|
+
* L: cond stmt jfalse->END ... jump->L while
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Literal type -> the pool's type letter. */
|
|
19
|
+
const IPOF_LIT_TAG = {
|
|
20
|
+
bool: 'b',
|
|
21
|
+
byte: 'y',
|
|
22
|
+
int: 'i',
|
|
23
|
+
long: 'l',
|
|
24
|
+
real: 'd',
|
|
25
|
+
string: 's',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Slot scopes, as the walker reports them. */
|
|
29
|
+
const IPOF_SC_GLOBAL = 0;
|
|
30
|
+
const IPOF_SC_LOCAL = 2;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds one proc's token list, tracking byte offsets as it goes.
|
|
34
|
+
*
|
|
35
|
+
* Tokens are appended without their `at`, then a single pass assigns offsets
|
|
36
|
+
* and resolves the recorded jump fixups -- a jump's target is usually a token
|
|
37
|
+
* that has not been emitted yet, so it cannot be resolved inline.
|
|
38
|
+
*/
|
|
39
|
+
class IpofProcEmitter {
|
|
40
|
+
/**
|
|
41
|
+
* @param {IpofCompiler} c The compiler, for its name tables.
|
|
42
|
+
* @param {Object} proc The proc AST node.
|
|
43
|
+
*/
|
|
44
|
+
constructor(c, proc) {
|
|
45
|
+
this.c = c;
|
|
46
|
+
this.proc = proc;
|
|
47
|
+
this.toks = [];
|
|
48
|
+
this.fixups = []; // {tok, label}
|
|
49
|
+
this.labels = new Map(); // label -> token index
|
|
50
|
+
this.slots = new Map(); // name -> {n, scope, ref}
|
|
51
|
+
this.base = 0; // token index the current block's jumps count from
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Append a token.
|
|
56
|
+
* @param {Object} t The token, without `at`.
|
|
57
|
+
* @returns {Object} The same token.
|
|
58
|
+
*/
|
|
59
|
+
push(t) {
|
|
60
|
+
this.toks.push(t);
|
|
61
|
+
return t;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reserve a label name that later resolves to the next token index.
|
|
66
|
+
* @returns {string} The label.
|
|
67
|
+
*/
|
|
68
|
+
label() {
|
|
69
|
+
// A monotonic counter, NOT a name derived from the current position: a
|
|
70
|
+
// while reserves its top and its exit before emitting either, so two
|
|
71
|
+
// position-derived names would collide and the loop's back edge would be
|
|
72
|
+
// bound to the exit -- turning every loop into straight-line code.
|
|
73
|
+
this._nlabel = (this._nlabel || 0) + 1;
|
|
74
|
+
return `L${this._nlabel}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Bind a label to the next token to be emitted.
|
|
79
|
+
* @param {string} l The label.
|
|
80
|
+
* @returns {void}
|
|
81
|
+
*/
|
|
82
|
+
mark(l) {
|
|
83
|
+
this.labels.set(l, this.toks.length);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Emit a jump whose target is filled in once the label is bound.
|
|
88
|
+
* @param {string} op Either 'jump' or 'jfalse'.
|
|
89
|
+
* @param {string} l The label to jump to.
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
jumpTo(op, l) {
|
|
93
|
+
const t = this.push({ op, to: 0 });
|
|
94
|
+
this.fixups.push({ tok: t, label: l, base: this.base });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A token's byte length: 4, except for the inline headers.
|
|
99
|
+
* @param {Object} t The token.
|
|
100
|
+
* @returns {number} Its length in bytes.
|
|
101
|
+
*/
|
|
102
|
+
static size(t) {
|
|
103
|
+
if (t.op === 'ITEM' || t.op === 'LINE') {
|
|
104
|
+
// op + u16 + pad + u16 nr + label \n + keys \n + pad + u16 end
|
|
105
|
+
return 6 + t.label.length + 1 + (t.keys || '').length + 1 + 4;
|
|
106
|
+
}
|
|
107
|
+
if (t.op === 'state') return t.name.length + 1 + 4 + 1;
|
|
108
|
+
return 4;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Assign every token its byte offset and resolve the jump fixups.
|
|
113
|
+
*
|
|
114
|
+
* A jump's target is a BYTE offset, so it is computed from the target
|
|
115
|
+
* token's offset -- the same number walk() reconstructs from the file's
|
|
116
|
+
* block-relative dword index.
|
|
117
|
+
*
|
|
118
|
+
* @param {number} start The byte offset the proc's first token sits at.
|
|
119
|
+
* @returns {number} The byte offset just past the proc.
|
|
120
|
+
*/
|
|
121
|
+
finish(start) {
|
|
122
|
+
let at = start;
|
|
123
|
+
const offs = [];
|
|
124
|
+
for (const t of this.toks) {
|
|
125
|
+
offs.push(at);
|
|
126
|
+
t.at = at;
|
|
127
|
+
at += IpofProcEmitter.size(t);
|
|
128
|
+
}
|
|
129
|
+
offs.push(at); // a jump past the last token
|
|
130
|
+
for (const f of this.fixups) {
|
|
131
|
+
const idx = this.labels.get(f.label);
|
|
132
|
+
f.tok.to = offs[idx === undefined ? this.toks.length : idx];
|
|
133
|
+
}
|
|
134
|
+
return at;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compiles a parsed file into an exec object.
|
|
140
|
+
*/
|
|
141
|
+
class IpofCompiler {
|
|
142
|
+
/**
|
|
143
|
+
* @param {{includes: string[], globals: Object[], procs: Object[]}} ast The
|
|
144
|
+
* parsed main file.
|
|
145
|
+
* @param {Object} [opts] Options.
|
|
146
|
+
* @param {Object<string, number>} [opts.builtins] Builtin name -> number.
|
|
147
|
+
* @param {string[]} [opts.imports] DLL import names, by index.
|
|
148
|
+
*/
|
|
149
|
+
constructor(ast, opts) {
|
|
150
|
+
const o = opts || {};
|
|
151
|
+
this.ast = ast;
|
|
152
|
+
this.builtins = o.builtins || {};
|
|
153
|
+
this.imports = o.imports || [];
|
|
154
|
+
this.protos = o.protos || {};
|
|
155
|
+
this.errors = [];
|
|
156
|
+
this.globals = new Map(); // name -> slot
|
|
157
|
+
this.procIds = new Map(); // name -> {kind, id}
|
|
158
|
+
this.procKinds = new Map(); // name -> kind
|
|
159
|
+
this.paramModes = new Map(); // name -> ['in'|'out'|'inout', ...]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Record a compile error against a source line.
|
|
164
|
+
* @param {string} message What is wrong.
|
|
165
|
+
* @param {number} line The source line.
|
|
166
|
+
* @returns {void}
|
|
167
|
+
*/
|
|
168
|
+
error(message, line) {
|
|
169
|
+
this.errors.push({ message, line, text: `line ${line}: ${message}` });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Assign global slots and proc ids, then compile every proc.
|
|
174
|
+
* @returns {{ecu: string, procs: Object, byid: Object, errors: Object[]}} The
|
|
175
|
+
* exec object plus any errors; procs is empty when errors is not.
|
|
176
|
+
*/
|
|
177
|
+
compile() {
|
|
178
|
+
// slot 0 is the void slot the format reserves; globals start at 1
|
|
179
|
+
let slot = 1;
|
|
180
|
+
for (const g of this.ast.globals) {
|
|
181
|
+
if (!this.globals.has(g.name)) this.globals.set(g.name, slot++);
|
|
182
|
+
}
|
|
183
|
+
// ids are per declaration TABLE, and the walker's procref reads them that
|
|
184
|
+
// way: a screen id and a menu id may both be 3 without colliding
|
|
185
|
+
const nextId = {
|
|
186
|
+
func: 0,
|
|
187
|
+
menu: 0,
|
|
188
|
+
screen: 0,
|
|
189
|
+
statemachine: 0,
|
|
190
|
+
state: 0,
|
|
191
|
+
};
|
|
192
|
+
// A script that already carries a library function's body -- as every
|
|
193
|
+
// decompiled file does, the includes having been compiled in -- must not
|
|
194
|
+
// get a second copy when that library is supplied as an include too. The
|
|
195
|
+
// definition from the script itself wins, and the duplicate is dropped
|
|
196
|
+
// rather than reported: it is the same function, not a conflict.
|
|
197
|
+
const kept = [];
|
|
198
|
+
for (let k = this.ast.procs.length - 1; k >= 0; k -= 1) {
|
|
199
|
+
const p = this.ast.procs[k];
|
|
200
|
+
if (this.procIds.has(p.name)) continue;
|
|
201
|
+
this.procIds.set(p.name, { kind: p.kind, id: 0 });
|
|
202
|
+
kept.unshift(p);
|
|
203
|
+
}
|
|
204
|
+
this.ast.procs = kept;
|
|
205
|
+
this.procIds.clear();
|
|
206
|
+
for (const p of kept) {
|
|
207
|
+
this.procIds.set(p.name, { kind: p.kind, id: nextId[p.kind]++ });
|
|
208
|
+
this.procKinds.set(p.name, p.kind);
|
|
209
|
+
this.paramModes.set(p.name, this.inferModes(p));
|
|
210
|
+
}
|
|
211
|
+
const procs = {};
|
|
212
|
+
const byid = {};
|
|
213
|
+
let at = 0;
|
|
214
|
+
// the global initialisers live in the startup proc, exactly as the real
|
|
215
|
+
// compiler puts them there
|
|
216
|
+
const startup = this.startupProc();
|
|
217
|
+
if (startup) this.ast.procs.unshift(startup);
|
|
218
|
+
for (const p of this.ast.procs) {
|
|
219
|
+
const e = new IpofProcEmitter(this, p);
|
|
220
|
+
try {
|
|
221
|
+
this.emitProc(e, p);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (!(err instanceof IpofSyntaxError)) throw err;
|
|
224
|
+
this.error(err.message.replace(/^line \d+: /, ''), err.line);
|
|
225
|
+
}
|
|
226
|
+
at = e.finish(at) + 32; // a gap for the next declaration header
|
|
227
|
+
procs[p.name] = e.toks;
|
|
228
|
+
const id = this.procIds.get(p.name);
|
|
229
|
+
if (id) byid[`${id.kind}:${id.id}`] = p.name;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
ecu: this.ecu || 'script',
|
|
233
|
+
procs,
|
|
234
|
+
byid,
|
|
235
|
+
errors: this.errors,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A function's real parameter modes, correcting a lost `out:`.
|
|
241
|
+
*
|
|
242
|
+
* A parameter the body assigns to, or hands to a builtin's own out slot, is
|
|
243
|
+
* written through by the callee and must therefore be passed by reference --
|
|
244
|
+
* whatever the declaration says. This matters because a decompiled source
|
|
245
|
+
* can lose the marker: ipo_source.py infers modes from use, and a parameter
|
|
246
|
+
* only ever forwarded to another function's out slot reads as `in:` to it.
|
|
247
|
+
* Compiling that literally would pass the value instead of the destination,
|
|
248
|
+
* and the callee's results would go nowhere.
|
|
249
|
+
*
|
|
250
|
+
* @param {Object} p The proc node.
|
|
251
|
+
* @returns {string[]} One mode per parameter.
|
|
252
|
+
*/
|
|
253
|
+
inferModes(p) {
|
|
254
|
+
const modes = (p.params || []).map((x) => x.mode);
|
|
255
|
+
const byName = new Map();
|
|
256
|
+
(p.params || []).forEach((x, k) => byName.set(x.name, k));
|
|
257
|
+
if (!byName.size) return modes;
|
|
258
|
+
/**
|
|
259
|
+
* Mark a parameter as written through.
|
|
260
|
+
* @param {string} name The argument's name.
|
|
261
|
+
* @returns {void}
|
|
262
|
+
*/
|
|
263
|
+
const markOut = (name) => {
|
|
264
|
+
const k = byName.get(name);
|
|
265
|
+
if (k !== undefined && modes[k] === 'in') modes[k] = 'out';
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Walk a statement list, marking every parameter the body writes to.
|
|
269
|
+
* @param {Object[]} body The statements.
|
|
270
|
+
* @returns {void}
|
|
271
|
+
*/
|
|
272
|
+
const scan = (body) => {
|
|
273
|
+
for (const s of body || []) {
|
|
274
|
+
if (!s) continue;
|
|
275
|
+
if (s.node === 'assign') markOut(s.name);
|
|
276
|
+
if (s.node === 'callstmt' || s.node === 'expr') {
|
|
277
|
+
const call = s.call || s.value;
|
|
278
|
+
const outs = (call && this.protos[call.name]) || [];
|
|
279
|
+
for (let k = 0; k < ((call && call.args) || []).length; k += 1) {
|
|
280
|
+
const a = call.args[k];
|
|
281
|
+
if (
|
|
282
|
+
a &&
|
|
283
|
+
a.node === 'name' &&
|
|
284
|
+
(outs[k] === 'out' || outs[k] === 'inout')
|
|
285
|
+
) {
|
|
286
|
+
markOut(a.name);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// a block's body is a list; a while's is one statement
|
|
291
|
+
if (s.body) scan(Array.isArray(s.body) ? s.body : [s.body]);
|
|
292
|
+
if (s.then) scan([s.then]);
|
|
293
|
+
if (s.alt) scan([s.alt]);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
scan(p.body);
|
|
297
|
+
for (const sec of p.sections || []) scan(sec.body);
|
|
298
|
+
return modes;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The synthetic proc that carries the global initialisers.
|
|
303
|
+
* @returns {Object|null} The proc node, or null when nothing is initialised.
|
|
304
|
+
*/
|
|
305
|
+
startupProc() {
|
|
306
|
+
const body = this.ast.globals
|
|
307
|
+
.filter((g) => g.init)
|
|
308
|
+
.map((g) => ({
|
|
309
|
+
node: 'assign',
|
|
310
|
+
name: g.name,
|
|
311
|
+
value: g.init,
|
|
312
|
+
line: g.line,
|
|
313
|
+
}));
|
|
314
|
+
if (!body.length) return null;
|
|
315
|
+
const name = '__inpa_startup__';
|
|
316
|
+
if (this.procIds.has(name)) return null;
|
|
317
|
+
this.procIds.set(name, { kind: 'func', id: 900 });
|
|
318
|
+
this.procKinds.set(name, 'func');
|
|
319
|
+
return {
|
|
320
|
+
node: 'proc',
|
|
321
|
+
kind: 'func',
|
|
322
|
+
name,
|
|
323
|
+
params: [],
|
|
324
|
+
locals: [],
|
|
325
|
+
body,
|
|
326
|
+
line: 0,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Compile one proc's body into its emitter.
|
|
332
|
+
* @param {IpofProcEmitter} e The emitter.
|
|
333
|
+
* @param {Object} p The proc node.
|
|
334
|
+
* @returns {void}
|
|
335
|
+
*/
|
|
336
|
+
emitProc(e, p) {
|
|
337
|
+
// params first, then locals: the slot order the format fixes
|
|
338
|
+
let n = 0;
|
|
339
|
+
for (const par of p.params) {
|
|
340
|
+
e.slots.set(par.name, {
|
|
341
|
+
n: n++,
|
|
342
|
+
scope: IPOF_SC_LOCAL,
|
|
343
|
+
ref: par.mode === 'out' || par.mode === 'inout',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
for (const l of p.locals) {
|
|
347
|
+
e.slots.set(l.name, { n: n++, scope: IPOF_SC_LOCAL, ref: false });
|
|
348
|
+
}
|
|
349
|
+
// the block header, whose dword count the runtime reads as the prologue's
|
|
350
|
+
// end; it is patched once the prologue's real length is known
|
|
351
|
+
const header = e.push({ op: 'block', dwords: 0 });
|
|
352
|
+
for (const l of p.locals) {
|
|
353
|
+
// an initialised local declares as a bare literal push, an
|
|
354
|
+
// uninitialised one as a typed decl -- the two forms the prologue has
|
|
355
|
+
if (l.init) this.emitExpr(e, l.init, p);
|
|
356
|
+
else e.push({ op: 'decl', type: l.type });
|
|
357
|
+
}
|
|
358
|
+
e.base = 0;
|
|
359
|
+
const end = e.label();
|
|
360
|
+
e.endLabel = end;
|
|
361
|
+
this.emitBlock(e, p.body, p);
|
|
362
|
+
for (const s of p.sections || []) this.emitSection(e, s, p);
|
|
363
|
+
e.mark(end);
|
|
364
|
+
e.push({ op: 'ret' });
|
|
365
|
+
e.push({ op: 'endproc' });
|
|
366
|
+
// the header's dword count is the block's own extent, which is only ever
|
|
367
|
+
// read as documentation -- the runtime walks the tape, not this number
|
|
368
|
+
header.dwords = e.toks.length - 1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Compile one ITEM / LINE / STATE section.
|
|
373
|
+
* @param {IpofProcEmitter} e The emitter.
|
|
374
|
+
* @param {Object} s The section node.
|
|
375
|
+
* @param {Object} p The enclosing proc.
|
|
376
|
+
* @returns {void}
|
|
377
|
+
*/
|
|
378
|
+
emitSection(e, s, p) {
|
|
379
|
+
if (s.node === 'STATE') {
|
|
380
|
+
e.push({
|
|
381
|
+
op: 'state',
|
|
382
|
+
name: `%${s.name}`,
|
|
383
|
+
index: this.stateIndex(s.name),
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
const t = {
|
|
387
|
+
op: s.node,
|
|
388
|
+
nr: s.nr,
|
|
389
|
+
label: s.label,
|
|
390
|
+
dwords: 0,
|
|
391
|
+
};
|
|
392
|
+
if (s.keys) t.keys = s.keys;
|
|
393
|
+
e.push(t);
|
|
394
|
+
}
|
|
395
|
+
// an inline header re-bases the jumps inside its own body
|
|
396
|
+
e.base = e.toks.length;
|
|
397
|
+
const mark = e.toks.length;
|
|
398
|
+
const end = e.label();
|
|
399
|
+
const outer = e.endLabel;
|
|
400
|
+
e.endLabel = end;
|
|
401
|
+
this.emitBlock(e, s.body, p);
|
|
402
|
+
e.mark(end);
|
|
403
|
+
e.endLabel = outer;
|
|
404
|
+
if (s.node !== 'STATE') e.toks[mark - 1].dwords = e.toks.length - mark;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The state index a `%LABEL` carries.
|
|
409
|
+
* @param {string} name The label name.
|
|
410
|
+
* @returns {number} Its index.
|
|
411
|
+
*/
|
|
412
|
+
stateIndex(name) {
|
|
413
|
+
if (!this._states) this._states = new Map();
|
|
414
|
+
if (!this._states.has(name)) this._states.set(name, this._states.size);
|
|
415
|
+
return this._states.get(name);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Compile a list of statements.
|
|
420
|
+
* @param {IpofProcEmitter} e The emitter.
|
|
421
|
+
* @param {Object[]} body The statements.
|
|
422
|
+
* @param {Object} p The enclosing proc.
|
|
423
|
+
* @returns {void}
|
|
424
|
+
*/
|
|
425
|
+
emitBlock(e, body, p) {
|
|
426
|
+
for (const s of body) this.emitStmt(e, s, p);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Compile one statement.
|
|
431
|
+
* @param {IpofProcEmitter} e The emitter.
|
|
432
|
+
* @param {Object} s The statement node.
|
|
433
|
+
* @param {Object} p The enclosing proc.
|
|
434
|
+
* @returns {void}
|
|
435
|
+
*/
|
|
436
|
+
emitStmt(e, s, p) {
|
|
437
|
+
if (s.node === 'empty') return;
|
|
438
|
+
if (s.node === 'block') {
|
|
439
|
+
this.emitBlock(e, s.body, p);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (s.node === 'label') {
|
|
443
|
+
e.push({
|
|
444
|
+
op: 'state',
|
|
445
|
+
name: `%${s.name}`,
|
|
446
|
+
index: this.stateIndex(s.name),
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (s.node === 'return') {
|
|
451
|
+
// an early exit is a jump to the body's end, the shape the decompiler
|
|
452
|
+
// reads back as `return;`
|
|
453
|
+
e.jumpTo('jump', e.endLabel);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (s.node === 'assign') {
|
|
457
|
+
this.emitExpr(e, s.value, p);
|
|
458
|
+
const slot = this.resolveSlot(e, s.name, s.line);
|
|
459
|
+
const t = { op: 'store', n: slot.n, sc: slot.scope };
|
|
460
|
+
if (slot.ref) t.ref = true;
|
|
461
|
+
e.push(t);
|
|
462
|
+
// a store is followed by the statement separator, as the compiled files
|
|
463
|
+
// show: the tape's evaluation stack is cleared between statements
|
|
464
|
+
e.push({ op: 'stmt', n: 1 });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (s.node === 'callstmt') {
|
|
468
|
+
this.emitCall(e, s.call, p);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (s.node === 'expr') {
|
|
472
|
+
this.emitExpr(e, s.value, p);
|
|
473
|
+
e.push({ op: 'stmt', n: 1 });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (s.node === 'if') {
|
|
477
|
+
this.emitExpr(e, s.cond, p);
|
|
478
|
+
e.push({ op: 'stmt', n: 1 });
|
|
479
|
+
const alt = e.label();
|
|
480
|
+
e.jumpTo('jfalse', alt);
|
|
481
|
+
this.emitStmt(e, s.then, p);
|
|
482
|
+
if (s.alt) {
|
|
483
|
+
const done = e.label();
|
|
484
|
+
e.jumpTo('jump', done);
|
|
485
|
+
e.mark(alt);
|
|
486
|
+
this.emitStmt(e, s.alt, p);
|
|
487
|
+
e.mark(done);
|
|
488
|
+
} else {
|
|
489
|
+
e.mark(alt);
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (s.node === 'while') {
|
|
494
|
+
const top = e.label();
|
|
495
|
+
const done = e.label();
|
|
496
|
+
e.mark(top);
|
|
497
|
+
this.emitExpr(e, s.cond, p);
|
|
498
|
+
e.push({ op: 'stmt', n: 1 });
|
|
499
|
+
e.jumpTo('jfalse', done);
|
|
500
|
+
this.emitStmt(e, s.body, p);
|
|
501
|
+
e.jumpTo('jump', top);
|
|
502
|
+
e.mark(done);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.error(`cannot compile a ${s.node} statement`, s.line);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Compile one call, opening its argument frame first.
|
|
510
|
+
* @param {IpofProcEmitter} e The emitter.
|
|
511
|
+
* @param {Object} c The call node.
|
|
512
|
+
* @param {Object} p The enclosing proc.
|
|
513
|
+
* @returns {void}
|
|
514
|
+
*/
|
|
515
|
+
emitCall(e, c, p) {
|
|
516
|
+
e.push({ op: 'frame' });
|
|
517
|
+
const proc = this.procIds.get(c.name);
|
|
518
|
+
// an out/inout parameter is passed by reference: the argument compiles to
|
|
519
|
+
// a procref carrying the slot, not to an ordinary push
|
|
520
|
+
const modes =
|
|
521
|
+
proc && proc.kind === 'func'
|
|
522
|
+
? this.paramModes.get(c.name) || []
|
|
523
|
+
: this.protos[c.name] || [];
|
|
524
|
+
c.args.forEach((a, k) => {
|
|
525
|
+
const byRef = modes[k] === 'out' || modes[k] === 'inout';
|
|
526
|
+
this.emitArg(e, a, p, byRef);
|
|
527
|
+
});
|
|
528
|
+
if (proc && proc.kind === 'func') {
|
|
529
|
+
e.push({ op: 'calluser', n: proc.id });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const alias = (this.imports.alias || {})[c.name];
|
|
533
|
+
const imp = this.imports.indexOf(alias || c.name);
|
|
534
|
+
if (imp >= 0) {
|
|
535
|
+
e.push({ op: 'dllcall', n: imp });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const n = this.builtins[c.name];
|
|
539
|
+
if (n === undefined) {
|
|
540
|
+
// a builtin the tables do not number cannot be compiled: there is no
|
|
541
|
+
// opcode to emit, and inventing one would send an unknown call to the car
|
|
542
|
+
this.error(
|
|
543
|
+
`unknown function "${c.name}" -- no builtin number for it`,
|
|
544
|
+
c.line
|
|
545
|
+
);
|
|
546
|
+
e.push({ op: 'stmt', n: 1 });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
e.push({ op: 'call', n, name: ipofBuiltinNameFor(n, this.builtinNames) });
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Compile one call argument.
|
|
554
|
+
*
|
|
555
|
+
* A bare name that names a proc is a procref (the form `setscreen(s_main,
|
|
556
|
+
* TRUE)` uses); a name that names an out parameter passes by reference.
|
|
557
|
+
*
|
|
558
|
+
* @param {IpofProcEmitter} e The emitter.
|
|
559
|
+
* @param {Object} a The argument expression.
|
|
560
|
+
* @param {Object} p The enclosing proc.
|
|
561
|
+
* @returns {void}
|
|
562
|
+
*/
|
|
563
|
+
emitArg(e, a, p, byRef) {
|
|
564
|
+
if (
|
|
565
|
+
a.node === 'name' &&
|
|
566
|
+
this.procIds.has(a.name) &&
|
|
567
|
+
!e.slots.has(a.name) &&
|
|
568
|
+
!this.globals.has(a.name)
|
|
569
|
+
) {
|
|
570
|
+
const r = this.procIds.get(a.name);
|
|
571
|
+
const kind = {
|
|
572
|
+
screen: 0x40,
|
|
573
|
+
menu: 0x41,
|
|
574
|
+
state: 0x42,
|
|
575
|
+
statemachine: 0x43,
|
|
576
|
+
func: 0x00,
|
|
577
|
+
}[r.kind];
|
|
578
|
+
e.push({ op: 'procref', kind, n: r.id });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (byRef && a.node === 'name') {
|
|
582
|
+
// the destination slot itself, in the scope it lives in -- the same
|
|
583
|
+
// encoding the walker reads back as a procref over a variable
|
|
584
|
+
const slot = this.resolveSlot(e, a.name, a.line);
|
|
585
|
+
e.push({ op: 'procref', kind: slot.scope, n: slot.n });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
this.emitExpr(e, a, p);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Resolve a name to its slot.
|
|
593
|
+
* @param {IpofProcEmitter} e The emitter.
|
|
594
|
+
* @param {string} name The name.
|
|
595
|
+
* @param {number} line The source line, for errors.
|
|
596
|
+
* @returns {{n: number, scope: number, ref: boolean}} The slot.
|
|
597
|
+
*/
|
|
598
|
+
resolveSlot(e, name, line) {
|
|
599
|
+
if (e.slots.has(name)) return e.slots.get(name);
|
|
600
|
+
if (this.globals.has(name)) {
|
|
601
|
+
return { n: this.globals.get(name), scope: IPOF_SC_GLOBAL, ref: false };
|
|
602
|
+
}
|
|
603
|
+
// A name shaped like the decompiler's own placeholder for a slot it could
|
|
604
|
+
// not name (`v2`, `s17`, `gb44`) is a slot the source never declared
|
|
605
|
+
// because the .IPO never named it. Give it one rather than refusing the
|
|
606
|
+
// file: the alternative is that no decompiled script round-trips.
|
|
607
|
+
if (/^(?:g?[bylirsv])\d+$/.test(name)) {
|
|
608
|
+
const slot = { n: e.slots.size, scope: IPOF_SC_LOCAL, ref: false };
|
|
609
|
+
e.slots.set(name, slot);
|
|
610
|
+
return slot;
|
|
611
|
+
}
|
|
612
|
+
this.error(`"${name}" is not declared`, line);
|
|
613
|
+
return { n: 0, scope: IPOF_SC_GLOBAL, ref: false };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Compile an expression, leaving its value on the stack.
|
|
618
|
+
* @param {IpofProcEmitter} e The emitter.
|
|
619
|
+
* @param {Object} x The expression node.
|
|
620
|
+
* @param {Object} p The enclosing proc.
|
|
621
|
+
* @returns {void}
|
|
622
|
+
*/
|
|
623
|
+
emitExpr(e, x, p) {
|
|
624
|
+
if (x.node === 'lit') {
|
|
625
|
+
// a bool is a byte in the pool, so it reads back as 1 / 0 -- never as a
|
|
626
|
+
// JS boolean, which would not compare equal to a decoded file's value
|
|
627
|
+
const v = x.type === 'bool' ? (x.value ? 1 : 0) : x.value;
|
|
628
|
+
e.push({
|
|
629
|
+
op: 'const',
|
|
630
|
+
n: this.constSlot(x),
|
|
631
|
+
t: IPOF_LIT_TAG[x.type],
|
|
632
|
+
v,
|
|
633
|
+
});
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (x.node === 'name') {
|
|
637
|
+
if (
|
|
638
|
+
!e.slots.has(x.name) &&
|
|
639
|
+
!this.globals.has(x.name) &&
|
|
640
|
+
this.procIds.has(x.name)
|
|
641
|
+
) {
|
|
642
|
+
this.emitArg(e, x, p);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const slot = this.resolveSlot(e, x.name, x.line);
|
|
646
|
+
const t = { op: 'var', n: slot.n, sc: slot.scope };
|
|
647
|
+
if (slot.ref) t.ref = true;
|
|
648
|
+
e.push(t);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (x.node === 'call') {
|
|
652
|
+
this.emitCall(e, x, p);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (x.node === 'binop') {
|
|
656
|
+
this.emitExpr(e, x.left, p);
|
|
657
|
+
this.emitExpr(e, x.right, p);
|
|
658
|
+
const code = IPOF_OP_CODE[x.op];
|
|
659
|
+
e.push({ op: 'binop', n: code, name: IPOF_BINOPS[code] || null });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (x.node === 'unop') {
|
|
663
|
+
this.emitExpr(e, x.operand, p);
|
|
664
|
+
const code = IPOF_UNOP_CODE[x.op];
|
|
665
|
+
e.push({ op: 'binop', n: code, name: IPOF_BINOPS[code] || null });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
this.error(`cannot compile a ${x.node} expression`, x.line);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* The pool index a literal would occupy.
|
|
673
|
+
*
|
|
674
|
+
* The exec form inlines every constant's value, so the index is only an
|
|
675
|
+
* identity -- but it has to be stable and unique per distinct literal, the
|
|
676
|
+
* way a real pool's is.
|
|
677
|
+
*
|
|
678
|
+
* @param {Object} x The literal node.
|
|
679
|
+
* @returns {number} Its slot.
|
|
680
|
+
*/
|
|
681
|
+
constSlot(x) {
|
|
682
|
+
if (!this._consts) this._consts = new Map();
|
|
683
|
+
const key = `${x.type}${String(x.value)}`;
|
|
684
|
+
if (!this._consts.has(key)) this._consts.set(key, this._consts.size);
|
|
685
|
+
return this._consts.get(key);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* A builtin's canonical name for a number, matching what the walker emits.
|
|
691
|
+
*
|
|
692
|
+
* @param {number} n The builtin number.
|
|
693
|
+
* @param {Object<number, string>} [names] An override table.
|
|
694
|
+
* @returns {string} The name.
|
|
695
|
+
*/
|
|
696
|
+
function ipofBuiltinNameFor(n, names) {
|
|
697
|
+
const table = names || IPOF_BUILTINS;
|
|
698
|
+
return table[n] || ipofBuiltinName(n);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
702
|
+
module.exports = {
|
|
703
|
+
IpofCompiler,
|
|
704
|
+
IpofProcEmitter,
|
|
705
|
+
ipofBuiltinNameFor,
|
|
706
|
+
IPOF_LIT_TAG,
|
|
707
|
+
};
|
|
708
|
+
}
|