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,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Parser for INPA's script language: tokens -> an AST.
|
|
3
|
+
*
|
|
4
|
+
* The grammar is the one tools/decompile/ipo_source.py emits, which is the one
|
|
5
|
+
* BMW's own shipped .SRC files are written in -- globals with initialisers,
|
|
6
|
+
* `#include`, functions with in:/out:/inout: parameters, MENU with INIT and
|
|
7
|
+
* ITEM(nr,"label") blocks, SCREEN with LINE("label","keys") blocks,
|
|
8
|
+
* STATEMACHINE with %labels, if/else/while, and C-precedence expressions.
|
|
9
|
+
*
|
|
10
|
+
* The AST is deliberately thin: the emitter walks it once and never revisits a
|
|
11
|
+
* node, so nodes carry only what the emitter needs plus the line number an
|
|
12
|
+
* error has to report.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Binary operator precedence, the decompiler's table read backwards. */
|
|
16
|
+
const IPOF_PREC = {
|
|
17
|
+
'||': 1,
|
|
18
|
+
'^^': 1,
|
|
19
|
+
'&&': 2,
|
|
20
|
+
'&': 3,
|
|
21
|
+
'|': 3,
|
|
22
|
+
'^': 3,
|
|
23
|
+
'==': 4,
|
|
24
|
+
'!=': 4,
|
|
25
|
+
'<': 5,
|
|
26
|
+
'>': 5,
|
|
27
|
+
'<=': 5,
|
|
28
|
+
'>=': 5,
|
|
29
|
+
'+': 6,
|
|
30
|
+
'-': 6,
|
|
31
|
+
'*': 7,
|
|
32
|
+
'/': 7,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Source operator text -> the binop opcode the walker names. */
|
|
36
|
+
const IPOF_OP_CODE = {
|
|
37
|
+
'+': 0x60,
|
|
38
|
+
'-': 0x61,
|
|
39
|
+
'*': 0x62,
|
|
40
|
+
'/': 0x63,
|
|
41
|
+
'<': 0x64,
|
|
42
|
+
'>': 0x65,
|
|
43
|
+
'<=': 0x66,
|
|
44
|
+
'>=': 0x67,
|
|
45
|
+
'==': 0x68,
|
|
46
|
+
'!=': 0x69,
|
|
47
|
+
'&&': 0x6a,
|
|
48
|
+
'||': 0x6b,
|
|
49
|
+
'^^': 0x6c,
|
|
50
|
+
'&': 0x6f,
|
|
51
|
+
'|': 0x70,
|
|
52
|
+
'^': 0x71,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Unary operator text -> opcode. */
|
|
56
|
+
const IPOF_UNOP_CODE = { '-': 0x6d, '!': 0x6e };
|
|
57
|
+
|
|
58
|
+
/** The declared value types. */
|
|
59
|
+
const IPOF_TYPES = new Set(['bool', 'byte', 'int', 'long', 'real', 'string']);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A recursive-descent parser over one file's tokens.
|
|
63
|
+
*/
|
|
64
|
+
class IpofParser {
|
|
65
|
+
/**
|
|
66
|
+
* @param {Array<Object>} toks Tokens from ipofLex.
|
|
67
|
+
*/
|
|
68
|
+
constructor(toks) {
|
|
69
|
+
this.toks = toks;
|
|
70
|
+
this.i = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The current token.
|
|
75
|
+
* @returns {Object} The token.
|
|
76
|
+
*/
|
|
77
|
+
peek() {
|
|
78
|
+
return this.toks[this.i];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Consume and return the current token.
|
|
83
|
+
* @returns {Object} The token.
|
|
84
|
+
*/
|
|
85
|
+
next() {
|
|
86
|
+
const t = this.toks[this.i];
|
|
87
|
+
this.i += 1;
|
|
88
|
+
return t;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether the current token matches a kind and (optionally) a value.
|
|
93
|
+
* @param {string} kind The token kind.
|
|
94
|
+
* @param {*} [value] The required value.
|
|
95
|
+
* @returns {boolean} True on a match.
|
|
96
|
+
*/
|
|
97
|
+
at(kind, value) {
|
|
98
|
+
const t = this.peek();
|
|
99
|
+
return t.kind === kind && (value === undefined || t.value === value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Consume the current token if it matches, else return null.
|
|
104
|
+
* @param {string} kind The token kind.
|
|
105
|
+
* @param {*} [value] The required value.
|
|
106
|
+
* @returns {Object|null} The token, or null.
|
|
107
|
+
*/
|
|
108
|
+
accept(kind, value) {
|
|
109
|
+
return this.at(kind, value) ? this.next() : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Consume the current token, requiring a match.
|
|
114
|
+
* @param {string} kind The token kind.
|
|
115
|
+
* @param {*} [value] The required value.
|
|
116
|
+
* @returns {Object} The token.
|
|
117
|
+
* @throws {IpofSyntaxError} When it does not match.
|
|
118
|
+
*/
|
|
119
|
+
expect(kind, value) {
|
|
120
|
+
const t = this.peek();
|
|
121
|
+
if (!this.at(kind, value)) {
|
|
122
|
+
const want = value === undefined ? kind : JSON.stringify(value);
|
|
123
|
+
const got =
|
|
124
|
+
t.kind === 'eof' ? 'end of file' : JSON.stringify(String(t.value));
|
|
125
|
+
throw new IpofSyntaxError(`expected ${want}, found ${got}`, t.line);
|
|
126
|
+
}
|
|
127
|
+
return this.next();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse a whole file.
|
|
132
|
+
* @returns {{includes: string[], globals: Object[], procs: Object[]}} The AST.
|
|
133
|
+
*/
|
|
134
|
+
parseFile() {
|
|
135
|
+
const includes = [];
|
|
136
|
+
const globals = [];
|
|
137
|
+
const procs = [];
|
|
138
|
+
while (!this.at('eof')) {
|
|
139
|
+
if (this.at('include')) {
|
|
140
|
+
includes.push(this.next().value);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (this.at('kw') && IPOF_TYPES.has(this.peek().value)) {
|
|
144
|
+
globals.push(...this.parseVarDecl());
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
procs.push(this.parseProc());
|
|
148
|
+
}
|
|
149
|
+
return { includes, globals, procs };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse one `type name [= literal] [, name ...];` declaration.
|
|
154
|
+
* @returns {Object[]} One node per declared name.
|
|
155
|
+
*/
|
|
156
|
+
parseVarDecl() {
|
|
157
|
+
const typeTok = this.next();
|
|
158
|
+
const type = typeTok.value;
|
|
159
|
+
const out = [];
|
|
160
|
+
do {
|
|
161
|
+
const nameTok = this.expect('id');
|
|
162
|
+
let init = null;
|
|
163
|
+
if (this.accept('op', '=')) init = this.parseExpr();
|
|
164
|
+
out.push({
|
|
165
|
+
node: 'var',
|
|
166
|
+
type,
|
|
167
|
+
name: nameTok.value,
|
|
168
|
+
init,
|
|
169
|
+
line: nameTok.line,
|
|
170
|
+
});
|
|
171
|
+
} while (this.accept('op', ','));
|
|
172
|
+
this.expect('op', ';');
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse one procedure: a function, MENU, SCREEN or STATEMACHINE.
|
|
178
|
+
* @returns {Object} The proc node.
|
|
179
|
+
*/
|
|
180
|
+
parseProc() {
|
|
181
|
+
const t = this.peek();
|
|
182
|
+
if (
|
|
183
|
+
this.at('kw', 'MENU') ||
|
|
184
|
+
this.at('kw', 'SCREEN') ||
|
|
185
|
+
this.at('kw', 'STATEMACHINE')
|
|
186
|
+
) {
|
|
187
|
+
const kw = this.next().value;
|
|
188
|
+
const kind = {
|
|
189
|
+
MENU: 'menu',
|
|
190
|
+
SCREEN: 'screen',
|
|
191
|
+
STATEMACHINE: 'statemachine',
|
|
192
|
+
}[kw];
|
|
193
|
+
const name = this.expect('id').value;
|
|
194
|
+
this.expect('op', '(');
|
|
195
|
+
this.expect('op', ')');
|
|
196
|
+
this.expect('op', '{');
|
|
197
|
+
return this.parseProcBody(kind, name, t.line);
|
|
198
|
+
}
|
|
199
|
+
// a function: `name(params) { ... }`
|
|
200
|
+
const name = this.expect('id').value;
|
|
201
|
+
this.expect('op', '(');
|
|
202
|
+
const params = this.parseParams();
|
|
203
|
+
this.expect('op', ')');
|
|
204
|
+
this.expect('op', '{');
|
|
205
|
+
const locals = this.parseLocals();
|
|
206
|
+
const body = this.parseStmts();
|
|
207
|
+
this.expect('op', '}');
|
|
208
|
+
return {
|
|
209
|
+
node: 'proc',
|
|
210
|
+
kind: 'func',
|
|
211
|
+
name,
|
|
212
|
+
params,
|
|
213
|
+
locals,
|
|
214
|
+
body,
|
|
215
|
+
line: t.line,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parse a parameter list: `in: type name, out: type name, ...`.
|
|
221
|
+
* @returns {Array<{mode: string, type: string, name: string}>} The params.
|
|
222
|
+
*/
|
|
223
|
+
parseParams() {
|
|
224
|
+
const out = [];
|
|
225
|
+
if (this.at('op', ')')) return out;
|
|
226
|
+
do {
|
|
227
|
+
const modeTok = this.peek();
|
|
228
|
+
let mode = 'in';
|
|
229
|
+
if (
|
|
230
|
+
this.at('kw', 'in') ||
|
|
231
|
+
this.at('kw', 'out') ||
|
|
232
|
+
this.at('kw', 'inout')
|
|
233
|
+
) {
|
|
234
|
+
mode = this.next().value;
|
|
235
|
+
this.expect('op', ':');
|
|
236
|
+
}
|
|
237
|
+
const type = this.peek().value;
|
|
238
|
+
if (!IPOF_TYPES.has(type)) {
|
|
239
|
+
throw new IpofSyntaxError(
|
|
240
|
+
`parameter needs a type, found "${type}"`,
|
|
241
|
+
modeTok.line
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
this.next();
|
|
245
|
+
out.push({ mode, type, name: this.expect('id').value });
|
|
246
|
+
} while (this.accept('op', ','));
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Parse the local declarations at a body's top.
|
|
252
|
+
* @returns {Object[]} The local nodes.
|
|
253
|
+
*/
|
|
254
|
+
parseLocals() {
|
|
255
|
+
const out = [];
|
|
256
|
+
while (this.at('kw') && IPOF_TYPES.has(this.peek().value))
|
|
257
|
+
out.push(...this.parseVarDecl());
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Parse a MENU / SCREEN / STATEMACHINE body, whose sections are its blocks.
|
|
263
|
+
* @param {string} kind The proc kind.
|
|
264
|
+
* @param {string} name The proc name.
|
|
265
|
+
* @param {number} line The declaration's line.
|
|
266
|
+
* @returns {Object} The proc node.
|
|
267
|
+
*/
|
|
268
|
+
parseProcBody(kind, name, line) {
|
|
269
|
+
const locals = this.parseLocals();
|
|
270
|
+
const pre = [];
|
|
271
|
+
const sections = [];
|
|
272
|
+
while (!this.at('op', '}')) {
|
|
273
|
+
if (this.at('kw', 'INIT')) {
|
|
274
|
+
this.next();
|
|
275
|
+
this.expect('op', '{');
|
|
276
|
+
// INIT is the menu's prologue: it runs before any item, so its
|
|
277
|
+
// statements belong with the ones written bare above it
|
|
278
|
+
pre.push(...this.parseStmts());
|
|
279
|
+
this.expect('op', '}');
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (this.at('kw', 'ITEM') || this.at('kw', 'LINE')) {
|
|
283
|
+
const head = this.next().value;
|
|
284
|
+
this.expect('op', '(');
|
|
285
|
+
let nr = 0;
|
|
286
|
+
let label;
|
|
287
|
+
let keys = '';
|
|
288
|
+
if (head === 'ITEM') {
|
|
289
|
+
nr = this.expect('num').value;
|
|
290
|
+
this.expect('op', ',');
|
|
291
|
+
label = this.expect('str').value;
|
|
292
|
+
} else {
|
|
293
|
+
label = this.expect('str').value;
|
|
294
|
+
if (this.accept('op', ',')) keys = this.expect('str').value;
|
|
295
|
+
}
|
|
296
|
+
this.expect('op', ')');
|
|
297
|
+
this.expect('op', '{');
|
|
298
|
+
const body = this.parseStmts();
|
|
299
|
+
this.expect('op', '}');
|
|
300
|
+
sections.push({
|
|
301
|
+
node: head,
|
|
302
|
+
nr,
|
|
303
|
+
label,
|
|
304
|
+
keys,
|
|
305
|
+
body,
|
|
306
|
+
line,
|
|
307
|
+
});
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (this.at('state')) {
|
|
311
|
+
const st = this.next();
|
|
312
|
+
const body = [];
|
|
313
|
+
// a state runs to the next label or the proc's end
|
|
314
|
+
while (!this.at('op', '}') && !this.at('state'))
|
|
315
|
+
body.push(this.parseStmt());
|
|
316
|
+
sections.push({
|
|
317
|
+
node: 'STATE',
|
|
318
|
+
name: st.value,
|
|
319
|
+
body,
|
|
320
|
+
line: st.line,
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
pre.push(this.parseStmt());
|
|
325
|
+
}
|
|
326
|
+
this.expect('op', '}');
|
|
327
|
+
return {
|
|
328
|
+
node: 'proc',
|
|
329
|
+
kind,
|
|
330
|
+
name,
|
|
331
|
+
params: [],
|
|
332
|
+
locals,
|
|
333
|
+
body: pre,
|
|
334
|
+
sections,
|
|
335
|
+
line,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Parse statements up to a closing brace.
|
|
341
|
+
* @returns {Object[]} The statement nodes.
|
|
342
|
+
*/
|
|
343
|
+
parseStmts() {
|
|
344
|
+
const out = [];
|
|
345
|
+
while (!this.at('op', '}') && !this.at('eof')) out.push(this.parseStmt());
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Parse one statement.
|
|
351
|
+
* @returns {Object} The statement node.
|
|
352
|
+
*/
|
|
353
|
+
parseStmt() {
|
|
354
|
+
const t = this.peek();
|
|
355
|
+
if (this.at('op', ';')) {
|
|
356
|
+
this.next();
|
|
357
|
+
return { node: 'empty', line: t.line };
|
|
358
|
+
}
|
|
359
|
+
if (this.at('op', '{')) {
|
|
360
|
+
this.next();
|
|
361
|
+
const body = this.parseStmts();
|
|
362
|
+
this.expect('op', '}');
|
|
363
|
+
return { node: 'block', body, line: t.line };
|
|
364
|
+
}
|
|
365
|
+
if (this.at('kw', 'if')) {
|
|
366
|
+
this.next();
|
|
367
|
+
this.expect('op', '(');
|
|
368
|
+
const cond = this.parseExpr();
|
|
369
|
+
this.expect('op', ')');
|
|
370
|
+
const then = this.parseStmt();
|
|
371
|
+
let alt = null;
|
|
372
|
+
if (this.accept('kw', 'else')) alt = this.parseStmt();
|
|
373
|
+
return {
|
|
374
|
+
node: 'if',
|
|
375
|
+
cond,
|
|
376
|
+
then,
|
|
377
|
+
alt,
|
|
378
|
+
line: t.line,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (this.at('kw', 'while')) {
|
|
382
|
+
this.next();
|
|
383
|
+
this.expect('op', '(');
|
|
384
|
+
const cond = this.parseExpr();
|
|
385
|
+
this.expect('op', ')');
|
|
386
|
+
const body = this.parseStmt();
|
|
387
|
+
return {
|
|
388
|
+
node: 'while',
|
|
389
|
+
cond,
|
|
390
|
+
body,
|
|
391
|
+
line: t.line,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (this.at('kw', 'return')) {
|
|
395
|
+
this.next();
|
|
396
|
+
this.expect('op', ';');
|
|
397
|
+
return { node: 'return', line: t.line };
|
|
398
|
+
}
|
|
399
|
+
if (this.at('state')) {
|
|
400
|
+
const st = this.next();
|
|
401
|
+
return { node: 'label', name: st.value, line: st.line };
|
|
402
|
+
}
|
|
403
|
+
// an assignment or a call: both start with an expression
|
|
404
|
+
const e = this.parseExpr();
|
|
405
|
+
if (this.accept('op', '=')) {
|
|
406
|
+
const value = this.parseExpr();
|
|
407
|
+
this.expect('op', ';');
|
|
408
|
+
if (e.node !== 'name') {
|
|
409
|
+
throw new IpofSyntaxError(
|
|
410
|
+
'assignment needs a variable on the left',
|
|
411
|
+
t.line
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
node: 'assign',
|
|
416
|
+
name: e.name,
|
|
417
|
+
value,
|
|
418
|
+
line: t.line,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
this.expect('op', ';');
|
|
422
|
+
if (e.node !== 'call') return { node: 'expr', value: e, line: t.line };
|
|
423
|
+
return { node: 'callstmt', call: e, line: t.line };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Parse an expression with C precedence.
|
|
428
|
+
* @param {number} [minPrec] The lowest precedence to bind at.
|
|
429
|
+
* @returns {Object} The expression node.
|
|
430
|
+
*/
|
|
431
|
+
parseExpr(minPrec) {
|
|
432
|
+
const min = minPrec || 0;
|
|
433
|
+
let left = this.parseUnary();
|
|
434
|
+
for (;;) {
|
|
435
|
+
const t = this.peek();
|
|
436
|
+
if (t.kind !== 'op') break;
|
|
437
|
+
const prec = IPOF_PREC[t.value];
|
|
438
|
+
if (prec === undefined || prec < min) break;
|
|
439
|
+
this.next();
|
|
440
|
+
const right = this.parseExpr(prec + 1);
|
|
441
|
+
left = {
|
|
442
|
+
node: 'binop',
|
|
443
|
+
op: t.value,
|
|
444
|
+
left,
|
|
445
|
+
right,
|
|
446
|
+
line: t.line,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return left;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Parse a unary expression.
|
|
454
|
+
* @returns {Object} The expression node.
|
|
455
|
+
*/
|
|
456
|
+
parseUnary() {
|
|
457
|
+
const t = this.peek();
|
|
458
|
+
if (this.at('op', '-') || this.at('op', '!')) {
|
|
459
|
+
this.next();
|
|
460
|
+
return {
|
|
461
|
+
node: 'unop',
|
|
462
|
+
op: t.value,
|
|
463
|
+
operand: this.parseUnary(),
|
|
464
|
+
line: t.line,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return this.parseAtom();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Parse an atom: a literal, a name, a call, or a parenthesised expression.
|
|
472
|
+
* @returns {Object} The expression node.
|
|
473
|
+
*/
|
|
474
|
+
parseAtom() {
|
|
475
|
+
const t = this.next();
|
|
476
|
+
if (t.kind === 'num') {
|
|
477
|
+
return {
|
|
478
|
+
node: 'lit',
|
|
479
|
+
type: t.real ? 'real' : 'int',
|
|
480
|
+
value: t.value,
|
|
481
|
+
line: t.line,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (t.kind === 'str') {
|
|
485
|
+
return {
|
|
486
|
+
node: 'lit',
|
|
487
|
+
type: 'string',
|
|
488
|
+
value: t.value,
|
|
489
|
+
line: t.line,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (t.kind === 'kw' && (t.value === 'TRUE' || t.value === 'FALSE')) {
|
|
493
|
+
return {
|
|
494
|
+
node: 'lit',
|
|
495
|
+
type: 'bool',
|
|
496
|
+
value: t.value === 'TRUE',
|
|
497
|
+
line: t.line,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (t.kind === 'op' && t.value === '(') {
|
|
501
|
+
const e = this.parseExpr();
|
|
502
|
+
this.expect('op', ')');
|
|
503
|
+
return e;
|
|
504
|
+
}
|
|
505
|
+
if (t.kind === 'id') {
|
|
506
|
+
if (this.at('op', '(')) {
|
|
507
|
+
this.next();
|
|
508
|
+
const args = [];
|
|
509
|
+
if (!this.at('op', ')')) {
|
|
510
|
+
do {
|
|
511
|
+
args.push(this.parseExpr());
|
|
512
|
+
} while (this.accept('op', ','));
|
|
513
|
+
}
|
|
514
|
+
this.expect('op', ')');
|
|
515
|
+
return {
|
|
516
|
+
node: 'call',
|
|
517
|
+
name: t.value,
|
|
518
|
+
args,
|
|
519
|
+
line: t.line,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return { node: 'name', name: t.value, line: t.line };
|
|
523
|
+
}
|
|
524
|
+
const got =
|
|
525
|
+
t.kind === 'eof' ? 'end of file' : JSON.stringify(String(t.value));
|
|
526
|
+
throw new IpofSyntaxError(`expected a value, found ${got}`, t.line);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Parse one INPA source file.
|
|
532
|
+
*
|
|
533
|
+
* @param {string} src The source text.
|
|
534
|
+
* @returns {{includes: string[], globals: Object[], procs: Object[]}} The AST.
|
|
535
|
+
* @throws {IpofSyntaxError} On a syntax error, naming the line.
|
|
536
|
+
*/
|
|
537
|
+
function ipofParse(src) {
|
|
538
|
+
return new IpofParser(ipofLex(src)).parseFile();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
542
|
+
module.exports = {
|
|
543
|
+
ipofParse,
|
|
544
|
+
IpofParser,
|
|
545
|
+
IPOF_PREC,
|
|
546
|
+
IPOF_OP_CODE,
|
|
547
|
+
IPOF_UNOP_CODE,
|
|
548
|
+
IPOF_TYPES,
|
|
549
|
+
};
|
|
550
|
+
}
|