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.
Files changed (72) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +351 -0
  3. package/dist/bmweb.js +2790 -0
  4. package/package.json +53 -0
  5. package/runtime/core/bestvm/codec.js +285 -0
  6. package/runtime/core/bestvm/environment.js +116 -0
  7. package/runtime/core/bestvm/executor.js +1483 -0
  8. package/runtime/core/bestvm/index.js +52 -0
  9. package/runtime/core/bestvm/machine.js +491 -0
  10. package/runtime/core/bestvm/operands.js +356 -0
  11. package/runtime/core/bestvm/registers.js +152 -0
  12. package/runtime/core/bestvm/write-guard.js +111 -0
  13. package/runtime/core/ipofile/compile.js +364 -0
  14. package/runtime/core/ipofile/decls.js +187 -0
  15. package/runtime/core/ipofile/emit.js +708 -0
  16. package/runtime/core/ipofile/exec.js +164 -0
  17. package/runtime/core/ipofile/lex.js +243 -0
  18. package/runtime/core/ipofile/parse.js +550 -0
  19. package/runtime/core/ipofile/pool.js +404 -0
  20. package/runtime/core/ipofile/walk.js +431 -0
  21. package/runtime/core/ipovm/builtin-helpers.js +182 -0
  22. package/runtime/core/ipovm/builtins-api.js +610 -0
  23. package/runtime/core/ipovm/builtins-screen.js +493 -0
  24. package/runtime/core/ipovm/builtins-table.js +166 -0
  25. package/runtime/core/ipovm/builtins-text.js +166 -0
  26. package/runtime/core/ipovm/emissions.js +138 -0
  27. package/runtime/core/ipovm/hosts.js +191 -0
  28. package/runtime/core/ipovm/operators.js +229 -0
  29. package/runtime/core/ipovm/structures.js +250 -0
  30. package/runtime/core/ipovm/suspensions.js +241 -0
  31. package/runtime/core/ipovm/tape.js +206 -0
  32. package/runtime/core/ipovm/values.js +241 -0
  33. package/runtime/core/ipovm/vm.js +1166 -0
  34. package/runtime/core/translate.js +526 -0
  35. package/runtime/core/webshim/api-router.js +592 -0
  36. package/runtime/core/webshim/bus.js +95 -0
  37. package/runtime/core/webshim/coding.js +82 -0
  38. package/runtime/core/webshim/data-fetch.js +66 -0
  39. package/runtime/core/webshim/exchange.js +288 -0
  40. package/runtime/core/webshim/framing.js +331 -0
  41. package/runtime/core/webshim/install.js +30 -0
  42. package/runtime/core/webshim/job-runner.js +319 -0
  43. package/runtime/core/webshim/native-bus.js +108 -0
  44. package/runtime/core/webshim/timers.js +82 -0
  45. package/runtime/core/webshim/trace.js +205 -0
  46. package/runtime/core/webshim/transport-base.js +128 -0
  47. package/runtime/core/webshim/variant-resolver.js +249 -0
  48. package/runtime/core/webshim/web-serial-bus.js +734 -0
  49. package/runtime/home/bmweb-home.ips +76 -0
  50. package/runtime/home/bmweb.h +26 -0
  51. package/runtime/screens/activations.js +258 -0
  52. package/runtime/screens/garage/diff.js +331 -0
  53. package/runtime/screens/garage/share.js +276 -0
  54. package/runtime/screens/garage/store.js +547 -0
  55. package/runtime/screens/ipo-runtime/cells.js +176 -0
  56. package/runtime/screens/ipo-runtime/dialogs.js +254 -0
  57. package/runtime/screens/ipo-runtime/home.js +358 -0
  58. package/runtime/screens/ipo-runtime/open.js +393 -0
  59. package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
  60. package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
  61. package/runtime/screens/ipo-runtime/print.js +281 -0
  62. package/runtime/screens/ipo-runtime/program.js +1337 -0
  63. package/runtime/screens/ipo-runtime/protocol.js +464 -0
  64. package/runtime/screens/ipo-runtime/script-scan.js +225 -0
  65. package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
  66. package/runtime/screens/ipo-runtime/ui.js +249 -0
  67. package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
  68. package/runtime/screens/ir.js +324 -0
  69. package/runtime/screens/search/data.js +153 -0
  70. package/runtime/screens/search/match.js +285 -0
  71. package/runtime/screens/search/open.js +66 -0
  72. package/runtime/vendor/fflate.min.js +1 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @file An .IPO's bytes -> the runnable `exec` object the ipovm consumes.
3
+ *
4
+ * The browser twin of tools/export/ipo_exec.py's export() over
5
+ * tools/decompile/ipo_vm.py's VM: same decode, same proc bounds, same
6
+ * {ecu, procs, byid} shape. The runtime cannot tell a dropped file's exec
7
+ * from a shipped data/inpa-ir dump, which is the whole point -- a script the
8
+ * user supplies runs through the identical path, wire policy included.
9
+ */
10
+
11
+ /**
12
+ * Decode one .IPO into its runnable exec object.
13
+ *
14
+ * Proc bounds follow the VM, not the decompiler: `lo` is the fixed
15
+ * declaration length (the VM does not read the optional version field) and
16
+ * the last proc runs to the POOL start rather than to the code end, because
17
+ * the shipped dumps were produced that way and the tokens have to match them.
18
+ *
19
+ * @param {Uint8Array} data The .IPO file bytes.
20
+ * @param {string} stem The name to report as the exec's ecu.
21
+ * @returns {{ecu: string, procs: Object<string, Object[]>, byid: Object<string,
22
+ * string>, includes: string[], imports: Object<number, string>,
23
+ * unknown: number, bytes: number}} The exec plus the metadata the report
24
+ * panel shows.
25
+ */
26
+ function ipofDecodeExec(data, stem) {
27
+ let { start: ps, entries: pool } = ipofFindPool(data);
28
+ let coding = false;
29
+ if (ps === null) {
30
+ // The NCS coding dispatchers share this bytecode but encode their pool
31
+ // differently and index the coding host table, not the screen builtins.
32
+ // Decoding them keeps the reader complete and lets the app say what a
33
+ // dropped file actually is instead of reporting an unreadable script.
34
+ const apool = ipofAPool(data);
35
+ if (!apool)
36
+ throw new Error(`${stem}: no constant pool -- not an INPA .IPO?`);
37
+ pool = apool;
38
+ ps = ipofCodeEnd(data, null);
39
+ coding = true;
40
+ }
41
+ const codeEnd = ipofCodeEnd(data, ps);
42
+ const decls = ipofFindDecls(data, codeEnd);
43
+ if (decls.length < 3)
44
+ throw new Error(`${stem}: no procedure declarations found`);
45
+ const procs = {};
46
+ const byid = {};
47
+ let unknown = 0;
48
+ let bytes = 0;
49
+ decls.forEach((d, k) => {
50
+ // the VM's bounds, verbatim: no version-field probe, last proc ends at ps
51
+ const lo = d.off + 1 + d.name.length + 1 + 4 + 1;
52
+ const hi = k + 1 < decls.length ? decls[k + 1].off : ps;
53
+ const r = ipofWalk(data, lo, hi, pool, coding ? IPOF_CDH_NAMES : null);
54
+ procs[d.name] = r.toks;
55
+ byid[`${d.typ}:${d.id}`] = d.name;
56
+ unknown += r.unknown;
57
+ bytes += r.len;
58
+ });
59
+ let meta = ipofConstantData(pool);
60
+ if (!meta.includes.length)
61
+ meta = { includes: ipofIncludesFromBytes(data), imports: meta.imports };
62
+ return {
63
+ ecu: stem,
64
+ procs,
65
+ byid,
66
+ coding,
67
+ includes: meta.includes,
68
+ imports: meta.imports,
69
+ unknown,
70
+ bytes,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * The exec's proc inventory, grouped for the parse report.
76
+ *
77
+ * @param {Object} exec An exec object from ipofDecodeExec or the compiler.
78
+ * @returns {{menus: string[], screens: string[], funcs: string[],
79
+ * machines: string[]}} Proc names by kind, sorted.
80
+ */
81
+ function ipofInventory(exec) {
82
+ const out = {
83
+ menus: [],
84
+ screens: [],
85
+ funcs: [],
86
+ machines: [],
87
+ };
88
+ const bucket = {
89
+ menu: 'menus',
90
+ screen: 'screens',
91
+ func: 'funcs',
92
+ statemachine: 'machines',
93
+ state: 'machines',
94
+ };
95
+ for (const key of Object.keys(exec.byid || {})) {
96
+ const kind = key.split(':')[0];
97
+ const b = bucket[kind];
98
+ if (b) out[b].push(exec.byid[key]);
99
+ }
100
+ for (const k of Object.keys(out)) out[k] = out[k].slice().sort();
101
+ return out;
102
+ }
103
+
104
+ /**
105
+ * Read a file the user dropped or picked, as raw bytes.
106
+ *
107
+ * @param {File} file The browser File object.
108
+ * @returns {Promise<Uint8Array>} Its bytes.
109
+ */
110
+ function ipofReadBytes(file) {
111
+ return file.arrayBuffer().then((buf) => new Uint8Array(buf));
112
+ }
113
+
114
+ /**
115
+ * Whether a filename names a compiled script.
116
+ *
117
+ * @param {string} name The file name.
118
+ * @returns {boolean} True for .ipo, either case.
119
+ */
120
+ function ipofIsCompiled(name) {
121
+ return /\.ipo$/i.test(name || '');
122
+ }
123
+
124
+ /**
125
+ * Whether a filename names a script source the compiler accepts.
126
+ *
127
+ * @param {string} name The file name.
128
+ * @returns {boolean} True for .ips or .src.
129
+ */
130
+ function ipofIsSource(name) {
131
+ return /\.(ips|src)$/i.test(name || '');
132
+ }
133
+
134
+ /**
135
+ * Whether a filename names an include the compiler may need.
136
+ *
137
+ * @param {string} name The file name.
138
+ * @returns {boolean} True for .h and for the .src files that carry bodies.
139
+ */
140
+ function ipofIsInclude(name) {
141
+ return /\.(h|src)$/i.test(name || '');
142
+ }
143
+
144
+ /**
145
+ * A file name without its extension.
146
+ *
147
+ * @param {string} name The file name.
148
+ * @returns {string} The stem.
149
+ */
150
+ function ipofStem(name) {
151
+ return String(name || 'script').replace(/\.[^.]*$/, '');
152
+ }
153
+
154
+ if (typeof module !== 'undefined' && module.exports) {
155
+ module.exports = {
156
+ ipofDecodeExec,
157
+ ipofInventory,
158
+ ipofReadBytes,
159
+ ipofIsCompiled,
160
+ ipofIsSource,
161
+ ipofIsInclude,
162
+ ipofStem,
163
+ };
164
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @file Tokenizer for INPA's script language (.IPS / .SRC).
3
+ *
4
+ * The language is C-shaped: line and block comments, string / number / boolean
5
+ * literals, identifiers, punctuation and the operator set the bytecode's BINOPS
6
+ * table can express. `%NAME` state labels are lexed as one token because the
7
+ * compiler emits them as one, and `#include "..."` is lexed as a directive
8
+ * rather than stitched together from '#' and a string.
9
+ *
10
+ * Every token carries its line so a compile error can name it -- the whole
11
+ * point of compiling in the app is that the user sees which line the compiler
12
+ * could not take.
13
+ */
14
+
15
+ /** Keywords the parser branches on, rather than treating as identifiers. */
16
+ const IPOF_KEYWORDS = new Set([
17
+ 'MENU',
18
+ 'SCREEN',
19
+ 'STATEMACHINE',
20
+ 'STATE',
21
+ 'INIT',
22
+ 'ITEM',
23
+ 'LINE',
24
+ 'if',
25
+ 'else',
26
+ 'while',
27
+ 'return',
28
+ 'in',
29
+ 'out',
30
+ 'inout',
31
+ 'bool',
32
+ 'byte',
33
+ 'int',
34
+ 'long',
35
+ 'real',
36
+ 'string',
37
+ 'TRUE',
38
+ 'FALSE',
39
+ ]);
40
+
41
+ /**
42
+ * The multi-character operators, longest first so `<=` never lexes as `<` `=`.
43
+ * @type {string[]}
44
+ */
45
+ const IPOF_OPS3 = ['&&', '||', '^^', '<=', '>=', '==', '!='];
46
+
47
+ /** Single-character punctuation and operators. */
48
+ const IPOF_OPS1 = '+-*/<>=!&|^(){},;:%';
49
+
50
+ /**
51
+ * A lexical error, carrying the line the user has to look at.
52
+ */
53
+ class IpofSyntaxError extends Error {
54
+ /**
55
+ * @param {string} message What went wrong.
56
+ * @param {number} line The 1-based source line.
57
+ */
58
+ constructor(message, line) {
59
+ super(`line ${line}: ${message}`);
60
+ this.name = 'IpofSyntaxError';
61
+ this.line = line;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Read a quoted string literal, honouring the backslash escapes the
67
+ * decompiler emits (`\\` and `\"`).
68
+ *
69
+ * @param {string} src The source text.
70
+ * @param {number} i Offset of the opening quote.
71
+ * @param {number} line The current line, for errors.
72
+ * @returns {{value: string, next: number}} The text and the offset after the
73
+ * closing quote.
74
+ */
75
+ function ipofLexString(src, i, line) {
76
+ let s = '';
77
+ let p = i + 1;
78
+ while (p < src.length && src[p] !== '"') {
79
+ if (src[p] === '\\' && p + 1 < src.length) {
80
+ const c = src[p + 1];
81
+ s += c === 'n' ? '\n' : c === 't' ? '\t' : c;
82
+ p += 2;
83
+ continue;
84
+ }
85
+ if (src[p] === '\n') throw new IpofSyntaxError('unterminated string', line);
86
+ s += src[p];
87
+ p += 1;
88
+ }
89
+ if (p >= src.length) throw new IpofSyntaxError('unterminated string', line);
90
+ return { value: s, next: p + 1 };
91
+ }
92
+
93
+ /**
94
+ * Read a numeric literal.
95
+ *
96
+ * A dot or an exponent makes it a real; `0x` makes it an int written in hex.
97
+ * The distinction matters downstream: the pool stores reals as f64 and ints as
98
+ * a u16, so the literal's spelling picks its storage.
99
+ *
100
+ * @param {string} src The source text.
101
+ * @param {number} i Offset of the first digit.
102
+ * @returns {{value: number, real: boolean, next: number}} The value, whether it
103
+ * is a real, and the offset after it.
104
+ */
105
+ function ipofLexNumber(src, i) {
106
+ let p = i;
107
+ if (src[p] === '0' && (src[p + 1] === 'x' || src[p + 1] === 'X')) {
108
+ p += 2;
109
+ while (p < src.length && /[0-9a-fA-F]/.test(src[p])) p += 1;
110
+ return { value: parseInt(src.slice(i, p), 16), real: false, next: p };
111
+ }
112
+ let real = false;
113
+ while (p < src.length && /[0-9]/.test(src[p])) p += 1;
114
+ if (src[p] === '.' && /[0-9]/.test(src[p + 1] || '')) {
115
+ real = true;
116
+ p += 1;
117
+ while (p < src.length && /[0-9]/.test(src[p])) p += 1;
118
+ }
119
+ if (src[p] === 'e' || src[p] === 'E') {
120
+ const save = p;
121
+ p += 1;
122
+ if (src[p] === '+' || src[p] === '-') p += 1;
123
+ if (/[0-9]/.test(src[p] || '')) {
124
+ real = true;
125
+ while (p < src.length && /[0-9]/.test(src[p])) p += 1;
126
+ } else p = save;
127
+ }
128
+ const text = src.slice(i, p);
129
+ return { value: real ? parseFloat(text) : parseInt(text, 10), real, next: p };
130
+ }
131
+
132
+ /**
133
+ * Tokenize one INPA source file.
134
+ *
135
+ * @param {string} src The source text.
136
+ * @returns {Array<{kind: string, value: *, line: number, real?: boolean}>} The
137
+ * tokens; `kind` is one of include, id, kw, str, num, op, state, eof.
138
+ * @throws {IpofSyntaxError} On an unterminated string or an illegal character.
139
+ */
140
+ function ipofLex(src) {
141
+ const toks = [];
142
+ let i = 0;
143
+ let line = 1;
144
+ while (i < src.length) {
145
+ const c = src[i];
146
+ if (c === '\n') {
147
+ line += 1;
148
+ i += 1;
149
+ continue;
150
+ }
151
+ if (c === ' ' || c === '\t' || c === '\r') {
152
+ i += 1;
153
+ continue;
154
+ }
155
+ if (c === '/' && src[i + 1] === '/') {
156
+ while (i < src.length && src[i] !== '\n') i += 1;
157
+ continue;
158
+ }
159
+ if (c === '/' && src[i + 1] === '*') {
160
+ i += 2;
161
+ while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) {
162
+ if (src[i] === '\n') line += 1;
163
+ i += 1;
164
+ }
165
+ i += 2;
166
+ continue;
167
+ }
168
+ if (c === '#') {
169
+ // `#include "name"` -- a directive, not a '#' followed by a string
170
+ const m = /^#\s*include\s*"([^"]*)"/.exec(src.slice(i));
171
+ if (m) {
172
+ toks.push({ kind: 'include', value: m[1], line });
173
+ i += m[0].length;
174
+ continue;
175
+ }
176
+ // any other directive is skipped to end of line, as a preprocessor would
177
+ while (i < src.length && src[i] !== '\n') i += 1;
178
+ continue;
179
+ }
180
+ if (c === '"') {
181
+ const r = ipofLexString(src, i, line);
182
+ toks.push({ kind: 'str', value: r.value, line });
183
+ i = r.next;
184
+ continue;
185
+ }
186
+ if (/[0-9]/.test(c)) {
187
+ const r = ipofLexNumber(src, i);
188
+ toks.push({
189
+ kind: 'num',
190
+ value: r.value,
191
+ real: r.real,
192
+ line,
193
+ });
194
+ i = r.next;
195
+ continue;
196
+ }
197
+ if (c === '%' && /[A-Za-z_]/.test(src[i + 1] || '')) {
198
+ // a state label: `%NAME`, one token because it compiles to one
199
+ let p = i + 1;
200
+ while (p < src.length && /[A-Za-z0-9_]/.test(src[p])) p += 1;
201
+ toks.push({ kind: 'state', value: src.slice(i + 1, p), line });
202
+ i = p;
203
+ continue;
204
+ }
205
+ if (/[A-Za-z_]/.test(c)) {
206
+ let p = i;
207
+ while (p < src.length && /[A-Za-z0-9_]/.test(src[p])) p += 1;
208
+ const word = src.slice(i, p);
209
+ toks.push({
210
+ kind: IPOF_KEYWORDS.has(word) ? 'kw' : 'id',
211
+ value: word,
212
+ line,
213
+ });
214
+ i = p;
215
+ continue;
216
+ }
217
+ const two = src.substr(i, 2);
218
+ if (IPOF_OPS3.indexOf(two) >= 0) {
219
+ toks.push({ kind: 'op', value: two, line });
220
+ i += 2;
221
+ continue;
222
+ }
223
+ if (IPOF_OPS1.indexOf(c) >= 0) {
224
+ toks.push({ kind: 'op', value: c, line });
225
+ i += 1;
226
+ continue;
227
+ }
228
+ throw new IpofSyntaxError(
229
+ `unexpected character ${JSON.stringify(c)}`,
230
+ line
231
+ );
232
+ }
233
+ toks.push({ kind: 'eof', value: null, line });
234
+ return toks;
235
+ }
236
+
237
+ if (typeof module !== 'undefined' && module.exports) {
238
+ module.exports = {
239
+ ipofLex,
240
+ IpofSyntaxError,
241
+ IPOF_KEYWORDS,
242
+ };
243
+ }