@asmlift/core 0.8.1 → 0.10.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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  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 +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
package/src/l3/hoist.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // see `HoistPlacement` and `BaseInitPlacement`. Everything lives in one file because each half was
11
11
  // a per-caller copy once and every copy drifted from its original.
12
12
  import type { Expr, SFn, Stmt } from './ast';
13
- import { exprChildren, mapStmtLists, stmtChildren, stmtExprs, stmtLists } from './ast';
13
+ import { exprChildren, mapStmtLists, mentionedName, stmtChildren, stmtExprs, stmtLists } from './ast';
14
14
  import { localMentions } from './mentions';
15
15
 
16
16
  /** Every identifier a MINTED name must not collide with, anywhere in `sfn` — the hoists below,
@@ -18,17 +18,18 @@ import { localMentions } from './mentions';
18
18
  *
19
19
  * Wider than "the declared locals" on purpose, and each addition is a real collision:
20
20
  * - params and locals, obviously;
21
- * - every `var`/`addr` mentioned — a GLOBAL is referenced by bare name, so a local shadowing one
22
- * silently redirects every later mention of it;
21
+ * - every name an expression mentions — a GLOBAL is referenced by bare name, so a local shadowing
22
+ * one silently redirects every later mention of it;
23
23
  * - every CALL TARGET — a local named like a callee shadows the function;
24
24
  * - every assignment target, which includes names no declaration list carries. */
25
25
  export function takenNames(sfn: SFn): Set<string> {
26
26
  const taken = new Set<string>([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
27
27
  const visit = (e: Expr): void => {
28
- if (e.k === 'var' || e.k === 'addr') {
29
- taken.add(e.name);
28
+ const n = mentionedName(e);
29
+ if (n !== undefined) {
30
+ taken.add(n);
30
31
  }
31
- if (e.k === 'call') {
32
+ if (e.k === 'call' && typeof e.fn === 'string') {
32
33
  taken.add(e.fn);
33
34
  }
34
35
  for (const c of exprChildren(e)) {
@@ -115,7 +116,7 @@ function mentionsHere(s: Stmt, name: string): boolean {
115
116
  }
116
117
  let found = false;
117
118
  const visit = (e: Expr): void => {
118
- if ((e.k === 'var' || e.k === 'addr') && e.name === name) {
119
+ if (mentionedName(e) === name) {
119
120
  found = true;
120
121
  }
121
122
  for (const c of exprChildren(e)) {
@@ -38,15 +38,33 @@
38
38
  // re-enters everything, a case can fall through), so there the variable must appear nowhere
39
39
  // outside the rewritten `if` at all. Declines (null) when nothing changes.
40
40
  import type { Expr, SFn, Stmt } from './ast';
41
- import { NEGATE_REL, exprChildren, exprEquals, exprHasEffect, stmtChildren, stmtExprs, walkExprs } from './ast';
42
- import { arithConversionSignedness, declaredTypes, provablyNonNegative } from './typing';
41
+ import {
42
+ NEGATE_REL,
43
+ exprChildren,
44
+ exprEquals,
45
+ exprHasEffect,
46
+ keepsHelperRel,
47
+ mentionedName,
48
+ stmtChildren,
49
+ stmtExprs,
50
+ walkExprs,
51
+ } from './ast';
52
+ import {
53
+ arithConversionSignedness,
54
+ declaredTypes,
55
+ exprIntWidth,
56
+ provablyNonNegative,
57
+ rendersSignedOverUnsigned,
58
+ } from './typing';
43
59
 
44
60
  const readsVar = (e: Expr, name: string): boolean =>
45
- ((e.k === 'var' || e.k === 'addr') && e.name === name) || exprChildren(e).some((c) => readsVar(c, name));
61
+ mentionedName(e) === name || exprChildren(e).some((c) => readsVar(c, name));
46
62
 
47
- // READS only — a pure write in a tail is benign (it overwrites the minted value on every path,
48
- // and any read after it is that write's business); touchesOutside below is TOTAL because strong
49
- // mode must know the name is absent, presence of any kind included.
63
+ // MENTIONS in an expression — a read, an `&v` a callee may read through, a `v++` that reads before
64
+ // it writes. A whole-statement write is what is benign (an `assign` overwrites the minted value on
65
+ // every path, and any read after it is that write's business), which is why the target of one is
66
+ // not consulted here; touchesOutside below is TOTAL because strong mode must know the name is
67
+ // absent, presence of any kind included.
50
68
  const stmtTouches = (s: Stmt, name: string): boolean =>
51
69
  stmtExprs(s).some((e) => readsVar(e, name)) || stmtChildren(s).some((x) => stmtTouches(x, name));
52
70
 
@@ -67,10 +85,11 @@ const stripWideIntCast = (e: Expr): Expr =>
67
85
  * would silently discard it. */
68
86
  const varRooted = (e: Expr): boolean => (e.k === 'var' ? true : e.k === 'cast' ? varRooted(e.e) : false);
69
87
 
70
- /** A guard re-spell's X: call/marker-free, every named leaf a non-volatile param/local of THIS
71
- * function (a global or `&gSym` could be project-declared volatile), and every deref `varRooted`. */
88
+ /** A guard re-spell's X: call/marker/post-increment-free, every named leaf a non-volatile param/local
89
+ * of THIS function (a global or `&gSym` could be project-declared volatile), and every deref
90
+ * `varRooted`. */
72
91
  const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: ReadonlySet<string>): boolean => {
73
- if (e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
92
+ if (e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || e.k === 'addr') {
74
93
  return false;
75
94
  }
76
95
  if (e.k === 'var' && (!ownNames.has(e.name) || volatileLocals.has(e.name))) {
@@ -83,8 +102,9 @@ const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: R
83
102
  };
84
103
 
85
104
  /** A compare operand and the init's value denote the same 32-bit value under different SPELLINGS
86
- * when a width-32 cast is all that separates them: `/uns-cmp` wraps one side in `(u32)` to make
87
- * the branch unsigned, and on a zero-trip guard that side is the very const the init assigns.
105
+ * when a width-32 cast is all that separates them: the unsigned-compare spelling wraps one side in
106
+ * `(u32)` to make the branch unsigned, and on a zero-trip guard that side is the very const the
107
+ * init assigns.
88
108
  * The swap is still exact — `v = X` stores X's 32 bits and `v` is 32-bit-declared
89
109
  * (meaningPreserved refuses otherwise), so `v` and `(u32)X` carry the same bit pattern and only
90
110
  * the compare's rendered signedness can differ, which meaningPreserved checks separately. A
@@ -110,6 +130,12 @@ const meaningPreserved = (
110
130
  if (vt?.kind !== 'int' || vt.width !== 32) {
111
131
  return false;
112
132
  }
133
+ // …AND SO MUST X, which the premise above states and the check did not: `v = X` represents X
134
+ // exactly only while X fits in v, so a 64-bit X assigned to a 32-bit v truncates and every
135
+ // signedness argument below it is about a value the program does not have.
136
+ if (exprIntWidth(side === 'l' ? l : r, env) !== 32) {
137
+ return false;
138
+ }
113
139
  if (provablyNonNegative(l, env) && provablyNonNegative(r, env)) {
114
140
  return true;
115
141
  }
@@ -119,6 +145,25 @@ const meaningPreserved = (
119
145
  return before !== undefined && before === after;
120
146
  };
121
147
 
148
+ /** `cond` with one side read through `v`, AFTER `meaningPreserved` said yes. That answer is a
149
+ * value proof the finished tree cannot repeat: `v` holds X, but `v`'s declaration does not say
150
+ * what range X had, so rank.ts's judgment of the tree (typing.ts `signedOverUnsignedCompare`)
151
+ * would call an unsigned compare whose ORIGINAL operands were both non-negative signed, and
152
+ * withhold a spelling that compares the same values. So a compare that was value-faithful before
153
+ * the swap is marked proved: its `unsignedCmp` evidence is dropped. One that was not keeps it. */
154
+ const swapSide = (
155
+ cond: Extract<Expr, { k: 'bin' }>,
156
+ side: 'l' | 'r',
157
+ v: string,
158
+ env: ReturnType<typeof declaredTypes>,
159
+ ): Expr => {
160
+ const out: Extract<Expr, { k: 'bin' }> = { ...cond, [side]: { k: 'var', name: v } };
161
+ if (cond.unsignedCmp === true && !rendersSignedOverUnsigned(cond.l, cond.r, env)) {
162
+ delete out.unsignedCmp;
163
+ }
164
+ return out;
165
+ };
166
+
122
167
  /** TOTAL (reads and pure writes) — see the note on stmtTouches. Runs against the pre-rewrite
123
168
  * tree (`skip` is an original-tree statement, found by identity); the rewrites never change a
124
169
  * name's presence in a subtree, so the verdict carries over to the rewritten one. */
@@ -205,7 +250,7 @@ export function initFirstGuards(sfn: SFn): SFn | null {
205
250
  then = then.slice(1);
206
251
  els = els.slice(1);
207
252
  }
208
- if (then.length === 0 && els.length > 0) {
253
+ if (then.length === 0 && els.length > 0 && !keepsHelperRel(cond)) {
209
254
  const neg = cond.k === 'bin' ? NEGATE_REL[cond.op] : undefined;
210
255
  if (neg !== undefined && cond.k === 'bin') {
211
256
  cond = { ...cond, op: neg };
@@ -216,16 +261,20 @@ export function initFirstGuards(sfn: SFn): SFn | null {
216
261
  // a COMMON-hoisted variable holds its const on both paths, so the condition's matching
217
262
  // const operand reads through it unconditionally — no tail gate needed.
218
263
  // A BARE const only, where the guard re-spelling below uses the cast-tolerant `sameValue`:
219
- // this site does not run `meaningPreserved`, so it may not swap in a name whose declared
220
- // type could re-render the compare's signedness — which is exactly what peeling a `(u32)`
221
- // would put on the table. A row that needs `/uns-cmp`'s spelling hoisted here brings the
222
- // gate with it.
264
+ // peeling a `(u32)` here would put a re-rendered signedness on the table with no cast left to
265
+ // say which. The name's DECLARED type can re-render it too — `if (a1 > 0)` read through a
266
+ // `u32 v` that holds the 0 is `a1 != 0` — so the swap asks `meaningPreserved` as the guard
267
+ // re-spelling does.
223
268
  for (const hv of hoisted) {
224
269
  if (cond.k === 'bin' && NEGATE_REL[cond.op]) {
225
270
  if (cond.l.k === 'const' && cond.l.value === hv.value) {
226
- cond = { ...cond, l: { k: 'var', name: hv.name } };
271
+ if (meaningPreserved(cond.l, cond.r, 'l', hv.name, env)) {
272
+ cond = swapSide(cond, 'l', hv.name, env);
273
+ }
227
274
  } else if (cond.r.k === 'const' && cond.r.value === hv.value) {
228
- cond = { ...cond, r: { k: 'var', name: hv.name } };
275
+ if (meaningPreserved(cond.l, cond.r, 'r', hv.name, env)) {
276
+ cond = swapSide(cond, 'r', hv.name, env);
277
+ }
229
278
  }
230
279
  }
231
280
  }
@@ -265,7 +314,7 @@ export function initFirstGuards(sfn: SFn): SFn | null {
265
314
  ) {
266
315
  out.push(init);
267
316
  moved.add(init);
268
- cond = { ...cond, [side]: { k: 'var', name: init.name } };
317
+ cond = swapSide(cond, side, init.name, env);
269
318
  then = then.slice(1);
270
319
  changed = true;
271
320
  }
@@ -0,0 +1,98 @@
1
+ // L3 respell variation: a pointer cell spelled as the integer a project may have declared it.
2
+ //
3
+ // A global the body stores a pointer into is spelled as a pointer (l3/ptrcell.ts): the store is
4
+ // `g = (void *)((u8 *)g + K)` or `g = (void *)X`, the integer uses convert it, `(u32)g`, and the
5
+ // candidate's own world declares it `void *`. A project that declares the cell an INTEGER
6
+ // (`u32 g; … g += 4;`) wrote none of those conversions, and two compilers tell: CodeWarrior rejects
7
+ // a pointer stored into the integer, and IDO allocates registers differently around a `(u32)g` it
8
+ // takes as a conversion. Under a pointer declaration `g = g + K` scales K by the pointee instead,
9
+ // so which of the two is right is a fact about the project's declaration, which nothing in the asm
10
+ // states.
11
+ //
12
+ // So the spelling is a PROOF-CARRYING candidate (`needsProof`, Candidate.matchOnly): published
13
+ // only at a byte-exact score, where the bytes are the evidence that the declaration it compiled
14
+ // against gives K the asm's stride. Its own self-declared world declares the cell an integer,
15
+ // because no pointer store is left in it.
16
+ //
17
+ // SCOPE (decline over approximate): a cell no declaration in the tree types and the symbol map
18
+ // does not declare other than an integer, and of its uses only the ones the pointer spelling made:
19
+ // the `(void *)` stores into it and a `(u32)` conversion of its bare value. A map's pointer is the
20
+ // project's own declaration, and its own world declares that, under which a byte-exact score is
21
+ // no evidence of the stride the project's pointee gives K. A global the body only reads converted
22
+ // is not this question.
23
+ import type { IrType } from '../ir/types';
24
+ import { T } from '../ir/types';
25
+ import { type Expr, type SFn, type Stmt, mapExprChildren, mapStmtExprs } from './ast';
26
+ import { pointerCellStore, pointerCellsOf } from './ptrcell';
27
+ import { declaredTypes } from './typing';
28
+
29
+ const isPtrTo = (t: IrType, to: 'void' | 'u8'): boolean =>
30
+ t.kind === 'ptr' && (to === 'void' ? t.to.kind === 'void' : t.to.kind === 'int' && t.to.width === 8);
31
+ const isWord = (t: IrType): boolean => t.kind === 'int' && t.width === 32;
32
+
33
+ /** `g`, `(u8 *)g`, `(u32)g` or `(u32)(u8 *)g` for the bare name `name`. */
34
+ const isCell = (x: Expr, name: string): boolean =>
35
+ (x.k === 'var' && x.name === name) || (x.k === 'cast' && (isWord(x.to) || isPtrTo(x.to, 'u8')) && isCell(x.e, name));
36
+
37
+ /** The integer a `(void *)` store into the cell `name` stores: its own arithmetic for a self-store,
38
+ * a bare value as it stands, anything else converted. */
39
+ function integerValue(value: Expr, name: string): Expr {
40
+ const stored = value.k === 'cast' ? value.e : value;
41
+ // `(u8 *)((u32)g + r)`: the integer sum the arithmetic table restores to a byte pointer
42
+ // (structure/pointer-spelling.ts ARITH_ROWS, `restore: 'bytes'`)
43
+ const sum = stored.k === 'cast' && isPtrTo(stored.to, 'u8') ? stored.e : stored;
44
+ if (sum.k === 'bin' && (sum.op === '+' || sum.op === '-') && isCell(sum.l, name)) {
45
+ return { ...sum, l: { k: 'var', name } };
46
+ }
47
+ return stored.k === 'var' ? stored : { k: 'cast', to: T.u(32), e: stored };
48
+ }
49
+
50
+ /** The `/int-cell` candidate: every pointer cell no declaration types, spelled as an integer.
51
+ * `mapDeclared` says the symbol map declares a name something other than an integer. */
52
+ export function integerCells(
53
+ sfn: SFn,
54
+ mapDeclared: (name: string) => boolean,
55
+ ): { sfn: SFn; needsProof: boolean } | null {
56
+ const vt = declaredTypes(sfn);
57
+ const cells = new Set([...pointerCellsOf(sfn)].filter((n) => vt(n) === undefined && !mapDeclared(n)));
58
+ if (cells.size === 0) {
59
+ return null;
60
+ }
61
+ const expr = (e0: Expr): Expr => {
62
+ const e = mapExprChildren(e0, expr);
63
+ if (e.k === 'cast' && isWord(e.to)) {
64
+ const inner = e.e.k === 'cast' && isPtrTo(e.e.to, 'u8') ? e.e.e : e.e;
65
+ if (inner.k === 'var' && cells.has(inner.name)) {
66
+ return inner;
67
+ }
68
+ }
69
+ return e;
70
+ };
71
+ const stmt = (s: Stmt): Stmt => {
72
+ switch (s.k) {
73
+ case 'assign': {
74
+ const value = expr(s.value);
75
+ return cells.has(s.name) && pointerCellStore(s) !== undefined
76
+ ? { ...s, value: integerValue(value, s.name) }
77
+ : { ...s, value };
78
+ }
79
+ case 'if':
80
+ return { ...s, cond: expr(s.cond), then: s.then.map(stmt), else: s.else.map(stmt) };
81
+ case 'while':
82
+ case 'dowhile':
83
+ return { ...s, cond: expr(s.cond), body: s.body.map(stmt) };
84
+ case 'for':
85
+ return { ...s, init: stmt(s.init), cond: expr(s.cond), inc: stmt(s.inc), body: s.body.map(stmt) };
86
+ case 'switch':
87
+ return {
88
+ ...s,
89
+ scrutinee: expr(s.scrutinee),
90
+ cases: s.cases.map((c) => ({ ...c, body: c.body.map(stmt) })),
91
+ default: s.default?.map(stmt),
92
+ };
93
+ default:
94
+ return mapStmtExprs(s, expr);
95
+ }
96
+ };
97
+ return { sfn: { ...sfn, body: sfn.body.map(stmt) }, needsProof: true };
98
+ }
@@ -90,7 +90,7 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
90
90
  }
91
91
  for (const e of stmtExprs(s)) {
92
92
  walkExpr(e, (x, isIndexBase) => {
93
- if (x.k === 'var' || x.k === 'addr') {
93
+ if (x.k === 'var' || x.k === 'addr' || x.k === 'postincr') {
94
94
  const m = seen(x.name, at);
95
95
  if (m) {
96
96
  if (x.k === 'addr') {
@@ -100,6 +100,11 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
100
100
  } else {
101
101
  m.otherUses++;
102
102
  }
103
+ // `v++` reads AND writes, so it counts on both sides. `topAssignAt`/`constValue` stay
104
+ // untouched: they describe a top-level `assign` STATEMENT, which this is not.
105
+ if (x.k === 'postincr') {
106
+ m.assigns++;
107
+ }
103
108
  }
104
109
  }
105
110
  });
@@ -126,8 +131,9 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
126
131
  * rewriting and go stale.
127
132
  *
128
133
  * TOTAL over the vocabulary by construction: `assign` is the only `Stmt` carrying a bare name and
129
- * `var`/`addr` the only `Expr`s, and both walks are derived from `stmtChildren`/`stmtExprs` — so a
130
- * `for`'s init and inc, a `switch`'s scrutinee, its cases and its default are all covered. */
134
+ * `var`/`addr`/`postincr` the only `Expr`s, and both walks are derived from
135
+ * `stmtChildren`/`stmtExprs` — so a `for`'s init and inc, a `switch`'s scrutinee, its cases and its
136
+ * default are all covered. */
131
137
  function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first: boolean): Set<string> {
132
138
  // TWO FLAT SWEEPS, not one expression walk per nesting level. `walkExprs` already descends
133
139
  // `stmtChildren` (ast.ts), so calling it per statement from inside a recursion that ALSO
@@ -149,7 +155,7 @@ function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first:
149
155
  stack.push(...stmtChildren(s));
150
156
  }
151
157
  for (const e of walkExprs(body)) {
152
- if ((e.k === 'var' || e.k === 'addr') && names.has(e.name)) {
158
+ if ((e.k === 'var' || e.k === 'addr' || e.k === 'postincr') && names.has(e.name)) {
153
159
  found.add(e.name);
154
160
  if (first) {
155
161
  return found;
@@ -0,0 +1,259 @@
1
+ // L3 stacked variations `/narrow-decl`, `/narrow-load` and `/narrow-read`: a named narrow value
2
+ // declared at its width. `/narrow-decl` takes the narrowing cast at the local's one write,
3
+ // `/narrow-load` a write that is a narrow memory read, which has no cast to drop, and `/narrow-read`
4
+ // the narrowing at every one of its reads:
5
+ //
6
+ // s32 v; v = (u8)(x - 1); … v … becomes u8 v; v = x - 1; … v … (/narrow-decl)
7
+ // s32 v; v = p[3]; … v … becomes u8 v; v = p[3]; … v … (/narrow-load)
8
+ // s32 v; v = f(); … (u8)v … (u8)v … becomes u8 v; v = f(); … v … v … (/narrow-read)
9
+ //
10
+ // The two spellings compute the same C value at every read. The declaration truncates where the
11
+ // cast did, or stores a value already of its type, and a `u8`, `s8`, `u16` or `s16` read is
12
+ // promoted to `int`, which is what an `s32` read already is. They are not the same program to gcc
13
+ // 2.9's front end. `get_narrower` (gcc/tree.c:4516) sees through the promotion of a narrow VARIABLE
14
+ // and not through an `int` that a cast was assigned to, so for an unsigned narrow one
15
+ // `shorten_compare` (gcc/c-common.c:1158) makes a compare against a constant unsigned and folds
16
+ // `>= 0`, `/` and `%` are shortened to their unsigned helpers (gcc/c-typeck.c:2036-2041, :2079-2090),
17
+ // and `>>` becomes a logical shift (gcc/c-typeck.c:2119). Without such an operator the object can
18
+ // still differ: `convert_to_integer` pushes a narrow store's truncation into an AND's operands
19
+ // (gcc/convert.c:278-284), and a `u8` local is a pseudo PROMOTE_MODE already zero-extended
20
+ // (gcc/thumb.h:344, gcc/stmt.c:3318-3323), whose low byte is free to read (gcc/expr.c:830-836),
21
+ // where an `s32` one takes a fresh byte pseudo and a register copy. Compiled with agbcc
22
+ // `-O2 -mthumb-interwork -fhex-asm -fprologue-bugfix`,
23
+ //
24
+ // v = g.c - 1; h.d = (v & 1) + rnd() % (5 - v) + 1;
25
+ //
26
+ // copies `v` before the AND under `s32 v` (`mov r1, #1; add r4, r5, #0; and r4, r4, r1`) and
27
+ // operates on it in place under `u8 v` (`mov r5, #1; and r5, r5, r4`), which is what
28
+ // `kleod:sub_0803E8CC` holds; over `v | 2`, `v ^ 2`, `v + 3` and `v * 3` the two compile alike.
29
+ // IDO, KMC gcc and gcc 2.7.2 each emit different code for that pair as well, and every mwcc
30
+ // build does for a compare of the narrowed value. Which one the
31
+ // source declared is not in the asm, so the differ referees, and the candidate it is derived onto
32
+ // stays in the fan. A load written to a `u8` local is loaded where it is written, and one written to
33
+ // an `s32` local where it is first read (two byte reads held across two calls, compiled in the
34
+ // matching suite: `pokeemerald:LoadMonInfo`).
35
+ //
36
+ // At the reads, `(u8)(s32)e`, `(u8)(u32)e` and `(u8)e` keep the same low byte of any integer `e`,
37
+ // so dropping each read's cast keeps its value, whichever write stored it. The pair compiles
38
+ // differently by where the extension lands: a `u8` local holding a call's result is zero-extended
39
+ // as it is stored, right after the `bl`, where `(u8)v` extends at the read, after whatever ran in
40
+ // between (`pokeemerald:RtcGetDayCount`, three results passed on as `u8`s).
41
+ //
42
+ // Three STACKED variations (rank-variations.ts), derived onto every other candidate's tree: the
43
+ // width of a declaration is orthogonal to what every respell variation outside that table changes,
44
+ // and the row that needs it needs it on top of `/offmember`. Three, not one, because a function
45
+ // holding a local narrowed at its cast write, a local written by a narrow load and a local narrowed
46
+ // at its reads may need any combination of the kinds narrowed. Each rewrites every local it admits.
47
+ // They do not commute: a cast that is one local's write and another's read is taken by whichever
48
+ // runs first, and so is a load-written local whose reads are all narrowed. So every subset of the
49
+ // three is its own candidate, and in one holding a write-side member and `/narrow-read` the
50
+ // write side runs first and takes the local.
51
+ //
52
+ // KNOWN GAP, and it is the price of deciding at L3 rather than where the value is named: which
53
+ // spelling a call's result had shows only in where its extension lands, right after its `bl` or
54
+ // just before its read, and structure/analysis.ts settles that per local when it names the call and
55
+ // inlines the extension into the read. So two read-side locals declared differently (`s32 y` cast
56
+ // at its read beside `u8 m`) have no candidate, as each entry narrows both or neither; here that
57
+ // would take 2^k candidates over k such locals. Nor has `s32 v = (u8)f()` once another such value
58
+ // is held across its call, which then compiles like neither spelling; held alone it compiles like
59
+ // `u8 v`, which /narrow-read reaches.
60
+ //
61
+ // REFUSED, each because the two spellings would stop computing the same value or would not build:
62
+ // • a local that is not a 32-bit integer;
63
+ // • a local whose address is taken, or that is volatile, a frame object, uninitialized, or homed
64
+ // in a stack slot. Each of those is an object in memory, whose width is its access width;
65
+ // • a write of a constant that fits neither the signed nor the unsigned type of the narrow width,
66
+ // which gcc warns of when it converts one implicitly (gcc/c-common.c:849-861, :870-895) and a
67
+ // `-Werror` build refuses;
68
+ // • /narrow-decl, /narrow-load: a `u32` local. Its reads stay bare, and a `u32` read is unsigned,
69
+ // so narrowing it would turn its compares, divisions and shifts into signed ones;
70
+ // • /narrow-decl, /narrow-load: a local written more than once, or by `v++`, where a later write
71
+ // could store a value the narrow declaration would truncate and a bare read would not;
72
+ // • /narrow-decl: a local whose one write is a `for` loop's init, or that is not an integer
73
+ // narrowed to a narrower integer. A call's operand is not known to be an integer (its callee
74
+ // may return a pointer), so `(u8)f()` is refused too;
75
+ // • /narrow-load: a local whose one write is a `for` loop's init, or is no `p[i]` or member read
76
+ // of a narrower integer type. A narrow variable (`v = a1` of a `u8 a1`) is refused: no row has
77
+ // needed it;
78
+ // • /narrow-read: a read that is not a narrowing cast (a bare `v`, an index base, a `v++`), reads
79
+ // cast to two widths or two signednesses, a write in a `for` loop's init or step, and a write
80
+ // whose value is a pointer, a float or of no known type. A call is admitted: its value already
81
+ // converts implicitly to the 32-bit local, and narrowing changes only the width of that
82
+ // conversion, never whether C performs one. A `u32` local and one written more than once are
83
+ // admitted, because every read already narrows each value stored to the one width.
84
+ import type { IrType } from '../ir/types';
85
+ import {
86
+ type Expr,
87
+ type SFn,
88
+ type Stmt,
89
+ mapExprChildren,
90
+ mapStmtExprs,
91
+ mapStmtLists,
92
+ stmtLists,
93
+ walkExprs,
94
+ } from './ast';
95
+ import { type Mentions, localMentions, readsOf } from './mentions';
96
+ import { declaredTypes, exprCType } from './typing';
97
+
98
+ type Narrowing = Extract<Expr, { k: 'cast' }> & { to: Extract<IrType, { kind: 'int' }> };
99
+
100
+ /** An integer narrowed to a narrower integer. A pointer or a float operand keeps its cast: C converts
101
+ * neither to an integer by assignment alone. */
102
+ const isNarrowing = (e: Expr, env: ReturnType<typeof declaredTypes>): e is Narrowing =>
103
+ e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && exprCType(e.e, env)?.kind === 'int';
104
+
105
+ /** The narrower integer type a memory read (`p[i]`, `*p`, a member) already has, which a local it is
106
+ * assigned to may be declared at with nothing to drop. */
107
+ const narrowLoad = (e: Expr, env: ReturnType<typeof declaredTypes>): Narrowing['to'] | undefined => {
108
+ if (e.k !== 'index' && e.k !== 'field') {
109
+ return undefined;
110
+ }
111
+ const t = exprCType(e, env);
112
+ return t?.kind === 'int' && t.width < 32 ? (t as Narrowing['to']) : undefined;
113
+ };
114
+
115
+ /** `(T)v`, the read of `name` narrowed to the narrower integer T. */
116
+ const isReadNarrowing = (e: Expr, name: string): e is Narrowing =>
117
+ e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && e.e.k === 'var' && e.e.name === name;
118
+
119
+ /** The one type every read of `name` is narrowed to, or undefined when a read is not narrowed
120
+ * or two reads are narrowed differently. `reads` counts every read however spelled, so a read
121
+ * that is not a narrowing leaves the count short. */
122
+ function readNarrowing(body: Stmt[], name: string, reads: number): Narrowing['to'] | undefined {
123
+ let to: Narrowing['to'] | undefined;
124
+ let narrowed = 0;
125
+ for (const e of walkExprs(body)) {
126
+ if (!isReadNarrowing(e, name)) {
127
+ continue;
128
+ }
129
+ if (to !== undefined && (to.width !== e.to.width || to.signed !== e.to.signed)) {
130
+ return undefined;
131
+ }
132
+ to = e.to;
133
+ narrowed++;
134
+ }
135
+ return narrowed === reads ? to : undefined;
136
+ }
137
+
138
+ /** The value of every assignment statement to `name`, found the way the write-side rewrite finds
139
+ * them: through statement lists, so a `for` init or step is not one. */
140
+ function writtenValues(body: Stmt[], name: string): Expr[] {
141
+ const values: Expr[] = [];
142
+ const walk = (list: Stmt[]): void => {
143
+ for (const s of list) {
144
+ if (s.k === 'assign' && s.name === name) {
145
+ values.push(s.value);
146
+ }
147
+ stmtLists(s).forEach(walk);
148
+ }
149
+ };
150
+ walk(body);
151
+ return values;
152
+ }
153
+
154
+ /** `body` with every `(T)name` read spelled `name`. */
155
+ const dropReadCasts = (body: Stmt[], name: string): Stmt[] => {
156
+ const expr = (e: Expr): Expr => (isReadNarrowing(e, name) ? e.e : mapExprChildren(e, expr));
157
+ return body.map((s) => mapStmtExprs(s, expr));
158
+ };
159
+
160
+ /** The local's admission common to both sides: a 32-bit integer in no memory. */
161
+ const admissible = (l: SFn['locals'][number], m: Mentions | undefined): m is Mentions =>
162
+ m !== undefined &&
163
+ m.addrTaken === 0 &&
164
+ l.type.kind === 'int' &&
165
+ l.type.width === 32 &&
166
+ !l.volatile &&
167
+ !l.frame &&
168
+ !l.uninit &&
169
+ !l.slots;
170
+
171
+ /** Whether storing `value` into an `int` of `to`'s width converts without gcc's constant warning: a
172
+ * constant must fit the signed or the unsigned type of that width. */
173
+ const storesQuietly = (value: Expr, to: Narrowing['to']): boolean =>
174
+ value.k !== 'const' || (value.value >= -(2 ** (to.width - 1)) && value.value < 2 ** to.width);
175
+
176
+ /** `sfn` with each local in `narrowed` declared at its width, or null when it is empty. */
177
+ const declared = (sfn: SFn, body: Stmt[], narrowed: Map<string, IrType>): SFn | null =>
178
+ narrowed.size === 0
179
+ ? null
180
+ : { ...sfn, body, locals: sfn.locals.map((l) => ({ ...l, type: narrowed.get(l.name) ?? l.type })) };
181
+
182
+ /** What a write-side variation makes of a local's one write: the narrow type to declare the local
183
+ * at and the value to store, or undefined when it does not take that write. */
184
+ type WriteNarrowing = (
185
+ value: Expr,
186
+ env: ReturnType<typeof declaredTypes>,
187
+ ) => { to: Narrowing['to']; value: Expr } | undefined;
188
+
189
+ /** A narrowing cast, dropped. */
190
+ const castWrite: WriteNarrowing = (value, env) =>
191
+ isNarrowing(value, env) && storesQuietly(value.e, value.to) ? { to: value.to, value: value.e } : undefined;
192
+
193
+ /** A narrow memory read, kept as written. */
194
+ const loadWrite: WriteNarrowing = (value, env) => {
195
+ const to = narrowLoad(value, env);
196
+ return to === undefined ? undefined : { to, value };
197
+ };
198
+
199
+ /** The tree with every admitted local whose one write `take` takes narrowed at that write, or null
200
+ * when none is. */
201
+ function narrowAtWrite(sfn: SFn, take: WriteNarrowing): SFn | null {
202
+ const mentions = localMentions(sfn);
203
+ const env = declaredTypes(sfn);
204
+ let body = sfn.body;
205
+ const narrowed = new Map<string, IrType>();
206
+ for (const l of sfn.locals) {
207
+ const m = mentions.get(l.name);
208
+ if (!admissible(l, m) || m.assigns !== 1 || l.type.kind !== 'int' || !l.type.signed) {
209
+ continue;
210
+ }
211
+ let to: IrType | undefined;
212
+ const rewrite = (list: Stmt[]): Stmt[] =>
213
+ list.map((s) => {
214
+ const taken = s.k === 'assign' && s.name === l.name ? take(s.value, env) : undefined;
215
+ if (taken !== undefined) {
216
+ to = taken.to;
217
+ return { ...s, value: taken.value };
218
+ }
219
+ return mapStmtLists(s, rewrite);
220
+ });
221
+ const next = rewrite(body);
222
+ if (to !== undefined) {
223
+ body = next;
224
+ narrowed.set(l.name, to);
225
+ }
226
+ }
227
+ return declared(sfn, body, narrowed);
228
+ }
229
+
230
+ /** `/narrow-decl`: the tree with every admitted local narrowed at its cast write, or null when none is. */
231
+ export const narrowDeclarations = (sfn: SFn): SFn | null => narrowAtWrite(sfn, castWrite);
232
+
233
+ /** `/narrow-load`: the tree with every admitted local written by a narrow memory read declared at that
234
+ * read's type, or null when none is. */
235
+ export const narrowLoadDeclarations = (sfn: SFn): SFn | null => narrowAtWrite(sfn, loadWrite);
236
+
237
+ /** `/narrow-read`: the tree with every admitted local narrowed at its reads, or null when none is. */
238
+ export function narrowReadDeclarations(sfn: SFn): SFn | null {
239
+ const mentions = localMentions(sfn);
240
+ const env = declaredTypes(sfn);
241
+ let body = sfn.body;
242
+ const narrowed = new Map<string, IrType>();
243
+ for (const l of sfn.locals) {
244
+ const m = mentions.get(l.name);
245
+ if (!admissible(l, m)) {
246
+ continue;
247
+ }
248
+ const written = writtenValues(body, l.name);
249
+ if (written.length !== m.assigns || written.some((w) => w.k !== 'call' && exprCType(w, env)?.kind !== 'int')) {
250
+ continue;
251
+ }
252
+ const read = readNarrowing(body, l.name, readsOf(m));
253
+ if (read !== undefined && written.every((w) => storesQuietly(w, read))) {
254
+ body = dropReadCasts(body, l.name);
255
+ narrowed.set(l.name, read);
256
+ }
257
+ }
258
+ return declared(sfn, body, narrowed);
259
+ }
@@ -11,11 +11,13 @@
11
11
  //
12
12
  // SCOPE (decline over approximate): cluster MEMBERSHIP comes from CONST deref bases only (a
13
13
  // struct-pointer cast base and everything inside a dot-form field subtree keep their spelling —
14
- // their stride is the struct's, not a byte's); a cluster needs at least two DISTINCT addresses
14
+ // their stride is the struct's, not a byte's — and a `volatile`-qualified base keeps its spelling
15
+ // and stays out of every cluster, since a `u8 *` base local would read and write the device
16
+ // plainly); a cluster needs at least two DISTINCT addresses
15
17
  // within the target's declared derivation reach of its lowest (TargetDescription nearBaseSpan —
16
18
  // beyond it the derive costs more than the pool word it saves); every access the walk visits
17
- // rewrites, so a cluster splits only across the field-subtree and struct-pointer-cast
18
- // boundaries. A member basecse
19
+ // rewrites, so a cluster splits only across the field-subtree, struct-pointer-cast and
20
+ // `volatile` boundaries. A member basecse
19
21
  // already hoisted arrives as a `var` base and is invisible here — the reused-base and
20
22
  // neighbor-base spellings stay separate candidates. Once a cluster HAS formed, a bare const
21
23
  // VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
@@ -24,7 +26,7 @@
24
26
  // only coincidentally lands in the window, which is the stated cost of the variation (the `s32` cast
25
27
  // assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
26
28
  // (null) when no cluster forms.
27
- import { baseConst } from './address';
29
+ import { baseConst, qualifiedBase } from './address';
28
30
  import type { Expr, SFn } from './ast';
29
31
  import { mapExprChildren, mapStmtExprs } from './ast';
30
32
  import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
@@ -46,7 +48,7 @@ export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
46
48
  return e; // rewrite refuses these subtrees, so collecting under them would seed a cluster
47
49
  }
48
50
  const m = mapExprChildren(e, collect);
49
- if (m.k === 'index') {
51
+ if (m.k === 'index' && !qualifiedBase(m.base)) {
50
52
  const c = baseConst(m.base);
51
53
  if (c !== null) {
52
54
  addrs.add(c);
@@ -92,6 +94,9 @@ export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
92
94
  if (e.k === 'field') {
93
95
  return e;
94
96
  }
97
+ if (e.k === 'index' && qualifiedBase(e.base)) {
98
+ return { ...e, idx: rewrite(e.idx) }; // the whole cast chain stays, so the access stays volatile
99
+ }
95
100
  if (e.k === 'index') {
96
101
  const c = baseConst(e.base);
97
102
  const lo = c !== null ? baseOf.get(c) : undefined;