@asmlift/core 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -12,10 +12,18 @@
12
12
  // be wrong in a way that reads as right. So the decision is made here, once, by the shape of the
13
13
  // name, and every refusing kind gets its own sentence naming what was seen.
14
14
  //
15
- // Consumed by frontend/ppc.ts, which refuses before it recovers. A kind is listed only when it
16
- // behaves differently: the decomp projects' generated labels (`lbl_1_bss_2464`, `fn_1_458`) are
17
- // ordinary identifiers that the project's own headers declare and its own sources spell, so they
18
- // are `plain` and get no entry of their own.
15
+ // Consumed by frontend/ppc.ts, which refuses before it recovers, and by frontend/thumb.ts's
16
+ // literal-pool reader. A function-scope static is the one kind neither refuses by name: the
17
+ // function DEFINES it, so both read its definition out of the target (frontend/local-object.ts) and
18
+ // the lift writes it in the body under the name `localStaticName` takes apart, refusing only where
19
+ // the definition cannot be read. Thumb asks about that kind and the predefined names alone rather
20
+ // than taking `unspellableReason`'s answer for every kind, because a pool word is not a relocation:
21
+ // agbcc packs `.L` labels into pools, and this policy calls a leading dot `section-local` — an
22
+ // offset into a section, not an object — which is false about a code label in the same file.
23
+ //
24
+ // A kind is listed only when it behaves differently: the decomp projects' generated labels
25
+ // (`lbl_1_bss_2464`, `fn_1_458`) are ordinary identifiers that the project's own headers declare
26
+ // and its own sources spell, so they are `plain` and get no entry of their own.
19
27
  //
20
28
  // THE SCOPE OF THIS POLICY IS THE NAME, AND ONLY ON A DATA RELOCATION.
21
29
  // • Not the TYPE. A name this passes is still rendered at the width the asm implies, which is a
@@ -32,12 +40,31 @@
32
40
 
33
41
  /** What sort of name a relocation carries. Everything but `plain` is unspellable in C. */
34
42
  export type RelocSymbolKind =
35
- 'plain' | 'anon-pool' | 'section-local' | 'local-static' | 'cpp-vtable' | 'cpp-mangled' | 'not-an-identifier';
43
+ | 'plain'
44
+ | 'anon-pool'
45
+ | 'section-local'
46
+ | 'local-static'
47
+ | 'inline-local-static'
48
+ | 'predefined-identifier'
49
+ | 'cpp-vtable'
50
+ | 'cpp-mangled'
51
+ | 'not-an-identifier';
52
+
53
+ /** `ident$N` (mwcc) or `ident.N` (gcc, and so agbcc): a static declared inside a function, whose
54
+ * SOURCE name is `ident`. Anchored whole, one numeric suffix: see the ordering note in
55
+ * {@link classifyRelocSymbol}. */
56
+ const LOCAL_STATIC = /^([A-Za-z_][A-Za-z0-9_]*)[.$](\d+)$/;
36
57
 
37
- /** Classify a relocation's symbol by its spelling. Order matters: the shapes that ARE valid C
38
- * identifiers (`__vt__…`, a mangled class-scoped name) or contain characters a C identifier may
39
- * not (`@`, `.`, `$`) are each recognised before the general identifier test, so the catch-all
40
- * below can be exactly "a name of no kind this policy knows, and not an identifier either". */
58
+ /** The identifiers a compiler defines inside every function, whose objects it names like a static. */
59
+ const PREDEFINED = new Set(['__func__', '__FUNCTION__', '__PRETTY_FUNCTION__']);
60
+
61
+ /** Classify a relocation's symbol by its spelling. Order matters twice over. The shapes that ARE
62
+ * valid C identifiers (`__vt__…`, a mangled class-scoped name) or contain characters a C
63
+ * identifier may not (`@`, `.`, `$`) are each recognised before the general identifier test, so
64
+ * the catch-all below can be exactly "a name of no kind this policy knows, and not an identifier
65
+ * either". And among themselves, the tests that look at a PREFIX come before the one that looks
66
+ * at a SUFFIX: a compiler is free to hang its static counter off a name of any other kind, and
67
+ * the kind is what picks the refusal sentence. */
41
68
  export function classifyRelocSymbol(sym: string): RelocSymbolKind {
42
69
  if (sym.startsWith('@')) {
43
70
  return 'anon-pool'; // `@193` — mwcc's anonymous string/constant pool entries
@@ -45,12 +72,17 @@ export function classifyRelocSymbol(sym: string): RelocSymbolKind {
45
72
  if (sym.startsWith('.')) {
46
73
  return 'section-local'; // `...bss.0`, `.rodata` — an offset into a section
47
74
  }
48
- if (sym.includes('$')) {
49
- return 'local-static'; // `sprHideTbl$797` — a function-scope static plus mwcc's TU-wide counter
50
- }
51
75
  if (sym.startsWith('__vt__')) {
52
76
  return 'cpp-vtable'; // `__vt__6System` — a compiler-emitted virtual table
53
77
  }
78
+ // mwcc names a static of an inlined HEADER function `<name>$localstatic<N>$<function>`
79
+ // (`_half$localstatic3$sqrtf`, `…$sqrtf__Ff` in C++; weak, in every Animal Crossing target that
80
+ // includes the libm header). The object belongs to the inline function's definition, not to the
81
+ // function referencing it, so it is its own kind, and asked before the class-scope marker: the
82
+ // trailing function name is the one part of the symbol that may carry a mangling.
83
+ if (/^[A-Za-z_]\w*\$localstatic\d+\$/.test(sym)) {
84
+ return 'inline-local-static';
85
+ }
54
86
  // mwcc mangles a class-scoped name as `<name>__<length><Class>` (`statbuff__9CmdStream`) or,
55
87
  // for a nested scope, `<name>__Q<depth><…>` (`__ct__Q26Action5ChildFv`). The marker is the `__`
56
88
  // followed by that LENGTH or `Q<depth>`, never a double underscore on its own: a rule that fired
@@ -66,6 +98,28 @@ export function classifyRelocSymbol(sym: string): RelocSymbolKind {
66
98
  if (/__(?:\d|Q\d)/.test(sym)) {
67
99
  return 'cpp-mangled';
68
100
  }
101
+ // A function-scope static plus the counter its compiler assigned: `sprHideTbl$797` (mwcc),
102
+ // `tide.3` / `zeroes.13` (gcc, and so agbcc). Both counters are TRANSLATION-UNIT-wide rather
103
+ // than per-function, which is why neither is reconstructible from the source: three functions in
104
+ // one TU, compiled by this project's agbcc, give `pa.3`, `pb.7`, `pc.11` and `pc2.12` — the
105
+ // number counts declarations across the whole unit and skips.
106
+ //
107
+ // LAST of the named kinds, and anchored to a WHOLE identifier plus ONE numeric suffix, because
108
+ // `[.$]\d+$` alone is a suffix test and every other kind here can wear that suffix. Unanchored
109
+ // and placed first it claimed `__vt__6System.1` and `statbuff__9CmdStream.0` — each then refused
110
+ // with a sentence about a counter, for a name whose real problem is the class definition it
111
+ // comes from — and it claimed gcc's IPA clones (`foo.isra.0`, `foo.part.0`, `foo.cold.1`), which
112
+ // are not statics at all and fall to `not-an-identifier` where they belong. A `$` name of any
113
+ // other shape (`$L1`) is not a static either.
114
+ //
115
+ // The one static no source declares: the name the compiler predefines in every function body.
116
+ // mwcc emits `__FUNCTION__`, `__func__` and `__PRETTY_FUNCTION__` as `__FUNCTION__$2`,
117
+ // `__func__$1`, `__PRETTY_FUNCTION__$3` (compiled), and a definition under that name is a
118
+ // redefinition the compiler rejects.
119
+ const m = sym.match(LOCAL_STATIC);
120
+ if (m !== null) {
121
+ return PREDEFINED.has(m[1]) ? 'predefined-identifier' : 'local-static';
122
+ }
69
123
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(sym) ? 'plain' : 'not-an-identifier';
70
124
  }
71
125
 
@@ -89,7 +143,17 @@ export function unspellableReason(sym: string): string | null {
89
143
  case 'local-static':
90
144
  return (
91
145
  `names a function-scope static ('${sym}') — the suffix is a translation-unit-wide counter ` +
92
- `the compiler assigned, which no source can spell`
146
+ `the compiler assigned, so no declaration elsewhere names the object; only its definition can`
147
+ );
148
+ case 'inline-local-static':
149
+ return (
150
+ `names a function-scope static of an inlined function ('${sym}') — the object belongs to ` +
151
+ `that function's definition, which a source reaches by calling it, never by naming the object`
152
+ );
153
+ case 'predefined-identifier':
154
+ return (
155
+ `names the function's predefined name ('${sym}') — the compiler declares it in every ` +
156
+ `function body, so a definition would redeclare it`
93
157
  );
94
158
  case 'cpp-vtable':
95
159
  return (
@@ -107,3 +171,20 @@ export function unspellableReason(sym: string): string | null {
107
171
  return `names '${sym}', which is not a C identifier`;
108
172
  }
109
173
  }
174
+
175
+ /** A function-scope static's linker name taken apart: the name the source wrote, and the counter
176
+ * the compiler appended (`tide.3` → `tide`, 3; `sprHideTbl$797` → `sprHideTbl`, 797) — or null
177
+ * for a name of any other kind, a static of an inlined function among them.
178
+ *
179
+ * The counter is file-wide and counts DECLARATIONS, so within one function it is the order the
180
+ * source declared its statics in. agbcc names and emits each static in one call, made while it
181
+ * parses the declaration (toplev.c:2515-2529 `rest_of_decl_compilation`: varasm.c:690-698
182
+ * `make_decl_rtl` takes `var_labelno++`, then `assemble_variable` writes the data); mwcc numbers
183
+ * `z$4 a$5` in declaration order whatever order the code first reads them in (compiled). */
184
+ export function localStaticName(sym: string): { name: string; counter: number } | null {
185
+ if (classifyRelocSymbol(sym) !== 'local-static') {
186
+ return null;
187
+ }
188
+ const m = sym.match(LOCAL_STATIC)!;
189
+ return { name: m[1], counter: Number(m[2]) };
190
+ }
@@ -210,13 +210,80 @@ function splitOperands(s: string): string[] {
210
210
  return out;
211
211
  }
212
212
 
213
+ /** A COPROCESSOR-1 REGISTER, in every spelling either MIPS dialect writes. THE ONE COPY: this
214
+ * reader decides which tokens keep the `$` sigil and `frontend/mips.ts` passes the same object as
215
+ * `OpaquePolicy.fpReg`, so the two questions — "does this survive normalisation with its sigil?"
216
+ * and "is this the FPU's file?" — are answered by one predicate. Two copies are sequenced rather
217
+ * than merely duplicated: widening the frontend's alone changes nothing, because this one has
218
+ * already stripped the token, and widening this one alone hands the frontend a sigil its own
219
+ * predicate rejects. Each half is silent on its own and the result is an opaque on a register in
220
+ * a file nothing models.
221
+ *
222
+ * Why the sigil: objdump writes a GPR bare (`v0`) and an FPU register with the sigil (`$f12`), so
223
+ * an FPU register is the one operand shape this reader must NOT strip, or the two dialects hand
224
+ * the frontend two different spellings of the same register. It is REQUIRED rather than optional
225
+ * because the bare form is indistinguishable from an objdump branch target, which is also bare
226
+ * lower-case hex (`f4`, `fa0`).
227
+ *
228
+ * The spellings, and all three are live in the trees: objdump's numbers (`$f12`), this dialect's
229
+ * o32 ABI names (`$ft2`, `$fv0`, `$fa0`, `$fs0`), and the trailing `f` those names take for the
230
+ * ODD HALF of a double-precision pair (`$ft0f`, `$fa1f`). `$fp` is the frame pointer and is
231
+ * stripped like any GPR: the digit is what decides, and it is what excludes it.
232
+ *
233
+ * Across every `$`-token in the three MIPS `asm/` trees this matches 52 tokens and every one is an
234
+ * FPU register:
235
+ * `grep -rhoE '\$[A-Za-z0-9_]+' apps/benchmark/checkouts/{af,marioparty3,snowboardkids2-decomp}/asm
236
+ * | sort -u | grep -icE '^\$f[vats]?[0-9]+f?$'` */
237
+ export const MIPS_FP_REG = /^\$f[vats]?\d+f?$/i;
238
+
239
+ /** The o32 ABI names of the EVEN FPU registers, by register number — the only ones a
240
+ * single-precision arithmetic instruction names under the FR=0 register model these compilers
241
+ * target. Read off the instruction words, not a manual: over 9,981 `add.s`/`sub.s`/`mul.s`/`div.s`
242
+ * lines in the `af` and `marioparty3` trees, decoding each word's `fd`/`fs`/`ft` fields gives every
243
+ * name one number and no name two. */
244
+ const O32_FP_NAMES: ReadonlyMap<string, number> = new Map([
245
+ ['fv0', 0],
246
+ ['fv1', 2],
247
+ ['ft0', 4],
248
+ ['ft1', 6],
249
+ ['ft2', 8],
250
+ ['ft3', 10],
251
+ ['fa0', 12],
252
+ ['fa1', 14],
253
+ ['ft4', 16],
254
+ ['ft5', 18],
255
+ ['fs0', 20],
256
+ ['fs1', 22],
257
+ ['fs2', 24],
258
+ ['fs3', 26],
259
+ ['fs4', 28],
260
+ ['fs5', 30],
261
+ ]);
262
+
263
+ /** An FPU register token, in either dialect's spelling, as the ONE key both dialects share — the
264
+ * objdump number (`$fa0` and `$f12` are both `$f12`) — or null for anything this does not name: a
265
+ * GPR, or an ODD half (`$f13`, `$fa0f`), which only a double-precision value or a word move uses.
266
+ * A key is what the SSA builder names a register by, so two spellings of one register that did
267
+ * not meet here would be two variables. */
268
+ export function mipsEvenFpKey(tok: string): string | null {
269
+ if (!MIPS_FP_REG.test(tok)) {
270
+ return null;
271
+ }
272
+ const bare = tok.slice(1).toLowerCase();
273
+ const num = /^f\d+$/.test(bare) ? Number(bare.slice(1)) : O32_FP_NAMES.get(bare);
274
+ return num !== undefined && num < 32 && num % 2 === 0 ? `$f${num}` : null;
275
+ }
276
+
213
277
  // Rewrite one Splat operand into the canonical objdump spelling the frontend consumes: strip the
214
- // `$` register sigil, fold a memory operand's displacement expression, evaluate a bare constant
215
- // expression, split a `%hi`/`%lo` reference into an immediate plus its record, decline a PIC one.
278
+ // `$` register sigil (except on an FPU register, where objdump keeps it), fold a memory operand's
279
+ // displacement expression, evaluate a bare constant expression, split a `%hi`/`%lo` reference into
280
+ // an immediate plus its record, decline a PIC one.
216
281
  function normalizeOperand(name: string, op: string): { op: string; reloc?: DisasmReloc } {
217
282
  // `%hi(SYM)` / `%lo(SYM + N)` / `%lo(SYM)(base)` — a global's address. Becomes the relocation
218
283
  // record an object file would carry plus the immediate the instruction really encodes, so the
219
284
  // frontend folds this dialect through the same path as objdump; NOT declined like the PIC relocs.
285
+ // The addend is a constant expression like any other here, so its RADIX is {@link evalConst}'s
286
+ // question rather than this pattern's — {@link refuseOctal} says why the placement is the rule.
220
287
  const hilo = op.match(
221
288
  /^%(hi|lo)\(\s*([A-Za-z_.$][\w.$]*)\s*(?:([+-])\s*(0x[0-9a-fA-F]+|\d+))?\s*\)(?:\((\$?[A-Za-z]\w*)\))?$/,
222
289
  );
@@ -235,8 +302,8 @@ function normalizeOperand(name: string, op: string): { op: string; reloc?: Disas
235
302
  if (HILO_OP.test(op)) {
236
303
  throw new FrontendUnsupportedError(
237
304
  `cannot lift '${name}': relocation operand '${op}' — this reader resolves a '%hi'/'%lo' half ` +
238
- `only against a symbol ('SYM' or 'SYM ± <integer>'), and will not treat one it cannot resolve ` +
239
- `as arithmetic`,
305
+ `only against a symbol ('SYM' or 'SYM ± <integer>'), and will not treat one it cannot ` +
306
+ `resolve as arithmetic`,
240
307
  );
241
308
  }
242
309
  if (RELOC_OP.test(op)) {
@@ -256,9 +323,42 @@ function normalizeOperand(name: string, op: string): { op: string; reloc?: Disas
256
323
  if (op.startsWith('(')) {
257
324
  return plain(String(evalConst(name, op)));
258
325
  }
259
- return plain(op.replace(/^\$/, ''));
326
+ // The one path that hands an operand on UNREAD, to a reader somewhere else: `addiu`/`addi`'s
327
+ // re-sign above is one, the MIPS frontend's `parseImm` another, and neither is told the radix, so
328
+ // a leading-zero immediate stops here — `addiu $v0, $a0, 020` lifted as `a0 + 20` against the
329
+ // assembler's 16.
330
+ if (OCTAL_MAGNITUDE.test(op)) {
331
+ refuseOctal(name, op, op);
332
+ }
333
+ return plain(MIPS_FP_REG.test(op) ? op : op.replace(/^\$/, ''));
260
334
  }
261
335
 
336
+ /** A magnitude with a LEADING ZERO is octal to the assembler and decimal to every `parseInt(…, 10)`
337
+ * in this file. Both readings are measured rather than assumed: under `mips-linux-gnu-as` 2.45,
338
+ * `.word 020` assembles to 0x10 and `.word 010` to 8 read back out of `.data`, and
339
+ * `lw $v0, 020($a0)` encodes the displacement 16 — against the 20 and the 10 a decimal reader
340
+ * answers. The difference reached the emitted C as a different ELEMENT: `%lo(gTab + 020)` lifted
341
+ * as `((s32 *)&gTab)[5]` where the assembler's addend gives `[4]`, and `%lo(gTab + 010)` as the
342
+ * fractional index `2.5`. A wrong address compiles and scores, so this is not something to guess.
343
+ *
344
+ * This reader models hex and decimal, so the third radix REFUSES rather than being resolved:
345
+ * nothing produces the shape — 0 operand occurrences carrying a leading-zero integer over the
346
+ * 4,200,524 operands of the 20,260 `.s` files that carry a `glabel` or a `.set noreorder` in the
347
+ * nine benchmark checkouts (counted with python; a recursive `grep` here skips `build/`) — so a
348
+ * reading for it would be a capability nothing could referee.
349
+ *
350
+ * THE PLACEMENT IS THE POINT. The rule belongs to the readers that turn a digit string into a
351
+ * value, not to the patterns that feed them: a guard on one caller's regex leaves the next caller
352
+ * reading base 10, and the three readers that do — `evalConst`'s literal, {@link signExtend16} and
353
+ * the shared `parseImm` — are not all in this file. */
354
+ const OCTAL_MAGNITUDE = /^[-+]?0\d/;
355
+ const refuseOctal = (name: string, tok: string, operand: string): never => {
356
+ throw new FrontendUnsupportedError(
357
+ `cannot lift '${name}': operand '${operand}' has a leading-zero magnitude ('${tok}'), which is ` +
358
+ `octal to the assembler and a radix this reader does not model`,
359
+ );
360
+ };
361
+
262
362
  // Evaluate a constant integer expression (the assembler's hi/lo split: hex/dec literals with
263
363
  // `+ - * << >> & | ^ ~` and parentheses). Precedence-climbing; C-like precedence. A shift `>>` is
264
364
  // LOGICAL — Splat's operands are unsigned 32-bit constants. Anything unparsable declines LOUD
@@ -292,6 +392,9 @@ function evalConst(name: string, expr: string): number {
292
392
  return ~unary();
293
393
  }
294
394
  if (/^(0x[0-9a-fA-F]+|\d+)$/.test(t)) {
395
+ if (OCTAL_MAGNITUDE.test(t)) {
396
+ refuseOctal(name, t, expr);
397
+ }
295
398
  return t.toLowerCase().startsWith('0x') ? parseInt(t, 16) : parseInt(t, 10);
296
399
  }
297
400
  return fail();