@asmlift/core 0.9.0 → 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 (101) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
@@ -1,22 +1,25 @@
1
- // L3 stacked variations `/narrow-decl` and `/narrow-read`: a named narrow value declared at its
2
- // width. `/narrow-decl` takes the narrowing at the local's one write, `/narrow-read` at every one of
3
- // its reads:
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:
4
5
  //
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)
6
8
  // s32 v; v = f(); … (u8)v … (u8)v … becomes u8 v; v = f(); … v … v … (/narrow-read)
7
9
  //
8
10
  // The two spellings compute the same C value at every read. The declaration truncates where the
9
- // cast did, and a `u8`, `s8`, `u16` or `s16` read is promoted to `int`, which is what an `s32` read
10
- // already is. They are not the same program to gcc 2.9's front end. `get_narrower`
11
- // (gcc/tree.c:4516) sees through the promotion of a narrow VARIABLE and not through an `int` that
12
- // a cast was assigned to, so for an unsigned narrow one `shorten_compare` (gcc/c-common.c:1158)
13
- // makes a compare against a constant unsigned and folds `>= 0`, `/` and `%` are shortened to their
14
- // unsigned helpers (gcc/c-typeck.c:2036-2041, :2079-2090), and `>>` becomes a logical shift
15
- // (gcc/c-typeck.c:2119). Without such an operator the object can still differ: `convert_to_integer`
16
- // pushes a narrow store's truncation into an AND's operands (gcc/convert.c:278-284), and a `u8`
17
- // local is a pseudo PROMOTE_MODE already zero-extended (gcc/thumb.h:344, gcc/stmt.c:3318-3323),
18
- // whose low byte is free to read (gcc/expr.c:830-836), where an `s32` one takes a fresh byte pseudo
19
- // and a register copy. Compiled with agbcc `-O2 -mthumb-interwork -fhex-asm -fprologue-bugfix`,
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`,
20
23
  //
21
24
  // v = g.c - 1; h.d = (v & 1) + rnd() % (5 - v) + 1;
22
25
  //
@@ -26,7 +29,9 @@
26
29
  // IDO, KMC gcc and gcc 2.7.2 each emit different code for that pair as well, and every mwcc
27
30
  // build does for a compare of the narrowed value. Which one the
28
31
  // source declared is not in the asm, so the differ referees, and the candidate it is derived onto
29
- // stays in the fan.
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`).
30
35
  //
31
36
  // At the reads, `(u8)(s32)e`, `(u8)(u32)e` and `(u8)e` keep the same low byte of any integer `e`,
32
37
  // so dropping each read's cast keeps its value, whichever write stored it. The pair compiles
@@ -34,13 +39,15 @@
34
39
  // as it is stored, right after the `bl`, where `(u8)v` extends at the read, after whatever ran in
35
40
  // between (`pokeemerald:RtcGetDayCount`, three results passed on as `u8`s).
36
41
  //
37
- // Two STACKED variations (rank-variations.ts), derived onto every other candidate's tree: the width
38
- // of a declaration is orthogonal to what every respell variation outside that table changes, and
39
- // the row that needs it needs it on top of `/offmember`. Two, not one, because a function holding a
40
- // local narrowed at its write and a local narrowed at its reads may need either one narrowed alone.
41
- // Each rewrites every local it admits. The two do not commute: a cast that is one local's write and
42
- // another's read is taken by whichever runs first. So each alone is its own candidate, and in the
43
- // all-together candidate `/narrow-decl` runs first and takes that cast as the write.
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.
44
51
  //
45
52
  // KNOWN GAP, and it is the price of deciding at L3 rather than where the value is named: which
46
53
  // spelling a call's result had shows only in where its extension lands, right after its `bl` or
@@ -58,13 +65,16 @@
58
65
  // • a write of a constant that fits neither the signed nor the unsigned type of the narrow width,
59
66
  // which gcc warns of when it converts one implicitly (gcc/c-common.c:849-861, :870-895) and a
60
67
  // `-Werror` build refuses;
61
- // • /narrow-decl: a `u32` local. Its reads stay bare, and a `u32` read is unsigned, so narrowing
62
- // it would turn its compares, divisions and shifts into signed ones;
63
- // • /narrow-decl: a local written more than once, or by `v++`, where a later write could store a
64
- // value the narrow declaration would truncate and a bare read would not;
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;
65
72
  // • /narrow-decl: a local whose one write is a `for` loop's init, or that is not an integer
66
73
  // narrowed to a narrower integer. A call's operand is not known to be an integer (its callee
67
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;
68
78
  // • /narrow-read: a read that is not a narrowing cast (a bare `v`, an index base, a `v++`), reads
69
79
  // cast to two widths or two signednesses, a write in a `for` loop's init or step, and a write
70
80
  // whose value is a pointer, a float or of no known type. A call is admitted: its value already
@@ -92,6 +102,16 @@ type Narrowing = Extract<Expr, { k: 'cast' }> & { to: Extract<IrType, { kind: 'i
92
102
  const isNarrowing = (e: Expr, env: ReturnType<typeof declaredTypes>): e is Narrowing =>
93
103
  e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && exprCType(e.e, env)?.kind === 'int';
94
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
+
95
115
  /** `(T)v`, the read of `name` narrowed to the narrower integer T. */
96
116
  const isReadNarrowing = (e: Expr, name: string): e is Narrowing =>
97
117
  e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && e.e.k === 'var' && e.e.name === name;
@@ -159,8 +179,26 @@ const declared = (sfn: SFn, body: Stmt[], narrowed: Map<string, IrType>): SFn |
159
179
  ? null
160
180
  : { ...sfn, body, locals: sfn.locals.map((l) => ({ ...l, type: narrowed.get(l.name) ?? l.type })) };
161
181
 
162
- /** `/narrow-decl`: the tree with every admitted local narrowed at its write, or null when none is. */
163
- export function narrowDeclarations(sfn: SFn): SFn | null {
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 {
164
202
  const mentions = localMentions(sfn);
165
203
  const env = declaredTypes(sfn);
166
204
  let body = sfn.body;
@@ -173,14 +211,10 @@ export function narrowDeclarations(sfn: SFn): SFn | null {
173
211
  let to: IrType | undefined;
174
212
  const rewrite = (list: Stmt[]): Stmt[] =>
175
213
  list.map((s) => {
176
- if (
177
- s.k === 'assign' &&
178
- s.name === l.name &&
179
- isNarrowing(s.value, env) &&
180
- storesQuietly(s.value.e, s.value.to)
181
- ) {
182
- to = s.value.to;
183
- return { ...s, value: s.value.e };
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 };
184
218
  }
185
219
  return mapStmtLists(s, rewrite);
186
220
  });
@@ -193,6 +227,13 @@ export function narrowDeclarations(sfn: SFn): SFn | null {
193
227
  return declared(sfn, body, narrowed);
194
228
  }
195
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
+
196
237
  /** `/narrow-read`: the tree with every admitted local narrowed at its reads, or null when none is. */
197
238
  export function narrowReadDeclarations(sfn: SFn): SFn | null {
198
239
  const mentions = localMentions(sfn);
@@ -19,7 +19,7 @@
19
19
  // where the forms genuinely differ (the body runs at least once vs at least zero times), which
20
20
  // is why it never wraps. Declines (null) when no empty do-while exists.
21
21
  import type { Expr, SFn, Stmt } from './ast';
22
- import { exprChildren, exprEquals, mapExprChildren, mapStmtLists, stmtChildren, stmtExprs } from './ast';
22
+ import { exprChildren, exprEquals, mapExprChildren, mapStmtLists, stmtChildren, stmtExprs, unionEvidence } from './ast';
23
23
 
24
24
  export function pollGuards(sfn: SFn): SFn | null {
25
25
  let changed = false;
@@ -140,7 +140,13 @@ export function pollReads(sfn: SFn): SFn | null {
140
140
  condFoldable(w.cond, volatileLocals) &&
141
141
  ownsEveryOccurrence(sfn.body, a.name)
142
142
  ) {
143
- out.push({ k: 'while', cond: subst(w.cond, a.name, a.value), body: [] });
143
+ // The loop's re-read and the first read print alike; the folded read stands for both
144
+ // (ast.ts `unionEvidence`).
145
+ out.push({
146
+ k: 'while',
147
+ cond: subst(w.cond, a.name, unionEvidence(a.value, (w.body[0] as typeof a).value)),
148
+ body: [],
149
+ });
144
150
  dropped.add(a.name);
145
151
  i++;
146
152
  continue;
package/src/l3/ptrcell.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  // L3: a POINTER CELL — a global the body stores a pointer into, and the one place that spelling is
2
2
  // recognised.
3
3
  //
4
- // structure() spells a pointer value stored into a word global as `g = (void *)X` (structure.ts
5
- // `intoPtrCell`), the store every pointer declaration of `g` takes. Three readers act on it, and
6
- // all three ask `pointerCells` rather than re-reading the cast:
4
+ // structure() spells a pointer value stored into a word global as `g = (void *)X`
5
+ // (structure/pointer-spelling.ts `intoPtrCell`), the store every pointer declaration of `g` takes.
6
+ // That store is built by `pointerCellValue` and read by `pointerCellStore`, both below; the one other
7
+ // writer of it, `legalizePointerCells`, converts an integer stored into a cell through the same
8
+ // function. Three readers act on it, and all three ask `pointerCells` rather than re-reading the cast:
7
9
  //
8
10
  // • l3/symbol-refs.ts declares a name-only cell `void *` in the candidate's own world, the type
9
11
  // that store needs, and so each global the cell meets bare that nothing reads as an integer
@@ -15,11 +17,24 @@
15
17
  // l3/symbol-refs.ts's header gives: a rewrite that drops the store must drop the fact with it.
16
18
  import { type IrType, T } from '../ir/types';
17
19
  import { declaredWidth } from '../proto';
18
- import { type Expr, type SFn, type Stmt, exprChildren, mapExprChildren, stmtChildren, walkExprs } from './ast';
20
+ import {
21
+ type Expr,
22
+ type SFn,
23
+ type Stmt,
24
+ assignsIn,
25
+ exprChildren,
26
+ mapExprChildren,
27
+ taintOverAssigns,
28
+ walkExprs,
29
+ } from './ast';
19
30
  import { declaredTypes, exprCType } from './typing';
20
31
 
21
- /** The bare name an `assign` stores a `(void *)` value into: a pointer cell, when no local binds
22
- * it. */
32
+ /** A value stored into a pointer cell: `(void *)X`, which every pointer declaration of the cell
33
+ * takes. */
34
+ export const pointerCellValue = (e: Expr): Expr => ({ k: 'cast', to: T.ptr(T.void()), e });
35
+
36
+ /** The bare name an `assign` stores a `(void *)` value into (`pointerCellValue`): a pointer cell,
37
+ * when no local binds it. */
23
38
  export function pointerCellStore(s: Stmt): string | undefined {
24
39
  return s.k === 'assign' && s.value.k === 'cast' && s.value.to.kind === 'ptr' && s.value.to.to.kind === 'void'
25
40
  ? s.name
@@ -28,18 +43,6 @@ export function pointerCellStore(s: Stmt): string | undefined {
28
43
 
29
44
  const COMPARE = new Set(['<', '<=', '>', '>=', '==', '!=']);
30
45
 
31
- const assigns = (body: Stmt[]): Extract<Stmt, { k: 'assign' }>[] => {
32
- const out: Extract<Stmt, { k: 'assign' }>[] = [];
33
- const visit = (s: Stmt): void => {
34
- if (s.k === 'assign') {
35
- out.push(s);
36
- }
37
- stmtChildren(s).forEach(visit);
38
- };
39
- body.forEach(visit);
40
- return out;
41
- };
42
-
43
46
  /** Whether `e` reads a variable `pick` takes. */
44
47
  const readsVar = (e: Expr, pick: (name: string) => boolean): boolean =>
45
48
  (e.k === 'var' && pick(e.name)) || exprChildren(e).some((c) => readsVar(c, pick));
@@ -59,18 +62,12 @@ export function cellOffset(e: Expr, cells: ReadonlySet<string>): boolean {
59
62
  /** The globals `body` stores a pointer into: a `(void *)` store, or the bare value of another one,
60
63
  * or that value moved by integers (`cellOffset`). */
61
64
  export function pointerCells(body: Stmt[], isGlobal: (name: string) => boolean): Set<string> {
62
- const stores = assigns(body).filter((s) => isGlobal(s.name));
63
- const out = new Set(stores.filter((s) => pointerCellStore(s) !== undefined).map((s) => s.name));
64
- for (let grew = true; grew;) {
65
- grew = false;
66
- for (const s of stores) {
67
- if (!out.has(s.name) && ((s.value.k === 'var' && out.has(s.value.name)) || cellOffset(s.value, out))) {
68
- out.add(s.name);
69
- grew = true;
70
- }
71
- }
72
- }
73
- return out;
65
+ const stores = assignsIn(body).filter((s) => isGlobal(s.name));
66
+ return taintOverAssigns(
67
+ stores,
68
+ stores.filter((s) => pointerCellStore(s) !== undefined).map((s) => s.name),
69
+ (s, cells) => (s.value.k === 'var' && cells.has(s.value.name)) || cellOffset(s.value, cells),
70
+ );
74
71
  }
75
72
 
76
73
  /** The bare globals a pointer cell meets bare: compared with one or with its `cellOffset`, or
@@ -83,7 +80,7 @@ function meetsBare(body: Stmt[], isGlobal: (name: string) => boolean, cells: Rea
83
80
  }
84
81
  };
85
82
  const isCell = (e: Expr): boolean => (e.k === 'var' && cells.has(e.name)) || cellOffset(e, cells);
86
- for (const s of assigns(body)) {
83
+ for (const s of assignsIn(body)) {
87
84
  if (cells.has(s.name)) {
88
85
  meets(s.value);
89
86
  }
@@ -303,8 +300,8 @@ export function pointerPartners(sfn: SFn): Set<string> {
303
300
  * goes `(u32)(u8 *)g` beside the cell's `(u32)g`: under a float declaration that is no C, as the
304
301
  * bare comparison was not. KNOWN GAP: under an array or a function declaration that is the
305
302
  * global's address, compared with no diagnostic, and IDO takes the integer read that made it no
306
- * partner (`v1 = g`) with none either; structure.ts `globalWord` says why no spelling reads the
307
- * word there.
303
+ * partner (`v1 = g`) with none either; structure/pointer-spelling.ts `globalWord` says why no
304
+ * spelling reads the word there.
308
305
  *
309
306
  * Run by the C-family backend's `emit` (backend/cfamily.ts), after every respell variation, so a
310
307
  * respell reads the uses as the structurer spelled them and a store it removes takes the
@@ -350,7 +347,7 @@ export function legalizePointerCells(sfn: SFn): SFn {
350
347
  intArg: (e) =>
351
348
  e.k === 'bin' && cellSum(e) && exprCType(e, vt)?.kind === 'ptr' ? { k: 'cast', to: T.u(32), e } : asInt(e),
352
349
  comparedGlobal: (e) => ({ k: 'cast', to: T.u(32), e: { k: 'cast', to: T.ptr(T.u(8)), e } }),
353
- intoCell: (e) => ({ k: 'cast', to: T.ptr(T.void()), e: asBytes(e) }),
350
+ intoCell: (e) => pointerCellValue(asBytes(e)),
354
351
  pointerOffset: asBytes,
355
352
  storedGlobal: () => {},
356
353
  });
@@ -29,10 +29,24 @@
29
29
  // `*(volatile s32 *)0x040000D4/D8/DC` device writes — `volatile` restricts what may be done with
30
30
  // THOSE accesses and does not merge their type into the pointer's alias set.
31
31
  //
32
- // SEMANTICS ARE PRESERVED BY CONSTRUCTION on a target whose pointers are 32 bits: the declaration
33
- // changes, and every read is wrapped in a cast back to the field's own recovered integer type, so
34
- // each use computes the same value it did. Nothing else moves. THE 32-BIT ASSUMPTION IS ASSERTED,
35
- // NOT CHECKED — there is no pointer-width field on TargetDescription to check it against, and the
32
+ // SEMANTICS ARE PRESERVED BY CONSTRUCTION on a target whose pointers are 32 bits — WHILE NO STORE
33
+ // OF ANOTHER ALIAS SET REACHES THE FIELD'S BYTES: the declaration changes, and every read is
34
+ // wrapped in a cast back to the field's own recovered integer type, so each use computes the same
35
+ // value it did. The cast keeps the VALUE, not the read's alias set: the load is still of a pointer
36
+ // type, which is the very freedom this variation exists to give agbcc, and which it uses to read
37
+ // stale past an integer store to the same bytes. Two such stores are knowable from the tree, and
38
+ // each has a gate. Over a local array the body IS that store: the frame audit licenses a wider
39
+ // runtime index into those bytes as one integer type (frontend/frame-objects.ts `indexedWidth`),
40
+ // so `over-local-storage` refuses a field selected out of one. And once a local's address has LEFT
41
+ // the function (a call argument, a stored or returned value) while the function writes local
42
+ // storage, a pointer it obtains from anywhere but its params or a fixed address may be that same
43
+ // address handed back (`k(buf); p = get(); x = p->a; buf[i] = v;`), so `frame-escaped` refuses a
44
+ // field read through one. Past those, a store the asm shows is tied to the field's bytes only by
45
+ // the caller's or a global's aliasing, which the tree cannot see — which spelling the original had
46
+ // is then what the differ referees, and it is the only referee there.
47
+ //
48
+ // THE 32-BIT ASSUMPTION IS ASSERTED, NOT CHECKED — there is no pointer-width field on
49
+ // TargetDescription to check it against, and the
36
50
  // assumption is already tower-wide (`l3/typing.ts`'s `ptrElemBytes` returns 4 for any pointee). On
37
51
  // a 64-bit target this variation would change a struct's LAYOUT rather than only its spelling, so the
38
52
  // width field is what that target's first row must add, and this note is where to start.
@@ -49,9 +63,11 @@
49
63
  //
50
64
  // GATE (PTR_FIELD_GATES, read once per field): the field's recovered type must be a 32-bit
51
65
  // integer; it must never be a store LVALUE (the write side would need a cast on the value, which
52
- // is a second question); and it must never stand as the BASE of another access (a field the tree
53
- // already dereferences is one the recovery typed from its own use, not from a width). Nothing
54
- // qualifying ⇒ decline (null).
66
+ // is a second question); it must never stand as the BASE of another access (a field the tree
67
+ // already dereferences is one the recovery typed from its own use, not from a width); and it must
68
+ // never be selected out of a local array's bytes (`over-local-storage`, the caveat above); and
69
+ // while a written local's address has escaped, it must be read through a param or a fixed address
70
+ // (`frame-escaped`, the same caveat). Nothing qualifying ⇒ decline (null).
55
71
  //
56
72
  // SCOPE, because both of this table's blind spots look exactly like a gate refusing. First, THERE
57
73
  // IS NO PAD RULE, deliberately: `plan` builds a ctx only for fields the tree ACCESSES, so "a field
@@ -67,12 +83,16 @@ import {
67
83
  type SFn,
68
84
  type Stmt,
69
85
  type StructType,
86
+ assignsIn,
70
87
  mapExprChildren,
71
88
  mapStmtExprs,
89
+ mentionedName,
72
90
  stmtChildren,
91
+ taintOverAssigns,
73
92
  walkExprs,
74
93
  } from './ast';
75
94
  import { type Gate, firstRejection } from './gates';
95
+ import { localAddresses, localStorageEscapesWritten, nameStorage } from './storage';
76
96
  import { declaredTypes, exprCType } from './typing';
77
97
 
78
98
  /** One recovered field as the gates read it — built only for fields the tree accesses. */
@@ -82,6 +102,12 @@ interface FieldCtx {
82
102
  written: boolean;
83
103
  /** the field's value stands as the base of an `index` or another `field` */
84
104
  dereferenced: boolean;
105
+ /** some access to the field selects it out of a local array's bytes */
106
+ overLocalStorage: boolean;
107
+ /** some access reads the field through a pointer that is neither a param nor a fixed address */
108
+ obtainedPointer: boolean;
109
+ /** a local's address leaves the function, and the function writes local storage */
110
+ frameEscaped: boolean;
85
111
  }
86
112
 
87
113
  export const PTR_FIELD_GATES: readonly Gate<FieldCtx>[] = [
@@ -106,6 +132,26 @@ export const PTR_FIELD_GATES: readonly Gate<FieldCtx>[] = [
106
132
  guardedBy: 'ptrfield.test.ts: a field standing as an access base declines',
107
133
  rejects: (c) => c.dereferenced,
108
134
  },
135
+ {
136
+ id: 'over-local-storage',
137
+ why:
138
+ "a local array's bytes are typed by the frame audit, which licenses a wider runtime index only " +
139
+ 'while every access of one width is one integer type; a pointer field over them is an alias ' +
140
+ 'set of its own, and agbcc at -O2 reads it stale past an integer store to the same bytes',
141
+ sound: true,
142
+ guardedBy: 'ptrfield.test.ts: a field selected out of a local array declines',
143
+ rejects: (c) => c.overLocalStorage,
144
+ },
145
+ {
146
+ id: 'frame-escaped',
147
+ why:
148
+ "once a written local's address has left the function, a pointer it obtains later may be that " +
149
+ 'address handed back, and a pointer field read through it is then read stale past the integer ' +
150
+ 'store to the same bytes',
151
+ sound: true,
152
+ guardedBy: 'ptrfield.test.ts: a field read through a re-obtained pointer after a frame address escaped declines',
153
+ rejects: (c) => c.frameEscaped && c.obtainedPointer,
154
+ },
109
155
  ];
110
156
 
111
157
  /** the struct a `field` node selects from, or null */
@@ -122,6 +168,10 @@ const keyOf = (structName: string, field: string): string => `${structName}.${fi
122
168
  * reads cast back to. */
123
169
  function plan(sfn: SFn): Map<string, IrType> {
124
170
  const vt = declaredTypes(sfn);
171
+ const storage = localStorageNames(sfn);
172
+ const frameEscaped = localStorageEscapesWritten(sfn);
173
+ const fixed = fixedPointer(sfn);
174
+ const { accessAddress } = localAddresses(sfn);
125
175
  const seen = new Map<string, FieldCtx & { type: IrType }>();
126
176
  const note = (base: Expr, name: string, edit: (c: FieldCtx) => void): void => {
127
177
  const st = structOf(base, vt);
@@ -134,6 +184,9 @@ function plan(sfn: SFn): Map<string, IrType> {
134
184
  word: declared.kind === 'int' && declared.width === 32,
135
185
  written: false,
136
186
  dereferenced: false,
187
+ overLocalStorage: false,
188
+ obtainedPointer: false,
189
+ frameEscaped,
137
190
  type: declared,
138
191
  };
139
192
  edit(cur);
@@ -141,7 +194,14 @@ function plan(sfn: SFn): Map<string, IrType> {
141
194
  };
142
195
  for (const e of walkExprs(sfn.body)) {
143
196
  if (e.k === 'field') {
144
- note(e.base, e.name, () => {});
197
+ note(e.base, e.name, (c) => {
198
+ if (reachesStorage(e.base, storage)) {
199
+ c.overLocalStorage = true;
200
+ }
201
+ if (!fixed(accessAddress(e))) {
202
+ c.obtainedPointer = true;
203
+ }
204
+ });
145
205
  }
146
206
  // a field standing as an access base is one the recovery typed from its own use
147
207
  const inner = e.k === 'index' ? e.base : e.k === 'field' ? e.base : null;
@@ -163,6 +223,61 @@ function plan(sfn: SFn): Map<string, IrType> {
163
223
  return out;
164
224
  }
165
225
 
226
+ /** The locals whose value is a LOCAL OBJECT'S ADDRESS: every local array (the bytes the frame
227
+ * audit typed — `u8 sp0[n]`), then every local assigned an expression that reaches one, or a
228
+ * local's address, to a fixpoint (`p0 = (s32 *)sp0;` carries the bytes into `p0`). Over-wide by
229
+ * design: a local holding `sp0[i]` (a value read OUT of the bytes) is swept in too, which only
230
+ * ever refuses a variation. */
231
+ function localStorageNames(sfn: SFn): { names: Set<string>; locals: Set<string> } {
232
+ const locals = new Set(sfn.locals.map((l) => l.name));
233
+ const names = taintOverAssigns(
234
+ assignsIn(sfn.body),
235
+ sfn.locals.filter((l) => l.type.kind === 'array').map((l) => l.name),
236
+ (a, names) => locals.has(a.name) && reachesStorage(a.value, { names, locals }),
237
+ );
238
+ return { names, locals };
239
+ }
240
+
241
+ /** Does the expression hold a pointer the function did not OBTAIN — one no escaped frame address
242
+ * can come back through? A constant address, a global's address, or a param nothing assigns (the
243
+ * caller's value, which predates this frame), under casts and offsets. Anything else — a local, a
244
+ * call's result, a value loaded from memory — may be the frame. */
245
+ function fixedPointer(sfn: SFn): (e: Expr) => boolean {
246
+ const storage = nameStorage(sfn);
247
+ const assigned = new Set(assignsIn(sfn.body).map((a) => a.name));
248
+ const fixed = (e: Expr): boolean => {
249
+ switch (e.k) {
250
+ case 'const':
251
+ return true;
252
+ case 'addr':
253
+ return storage.get(e.name) !== 'local' && storage.get(e.name) !== 'param';
254
+ case 'var':
255
+ return storage.get(e.name) === 'param' && !assigned.has(e.name);
256
+ case 'cast':
257
+ return fixed(e.e);
258
+ // An offset from a fixed pointer stays in the object it points into, but the tree does not
259
+ // say which operand is the pointer: in `p + off` with `p = get()` and `off` a param, the
260
+ // param is the fixed side and `p` may be the frame. So every operand must be fixed.
261
+ case 'bin':
262
+ return (e.op === '+' || e.op === '-') && fixed(e.l) && fixed(e.r);
263
+ default:
264
+ return false;
265
+ }
266
+ };
267
+ return fixed;
268
+ }
269
+
270
+ /** does the expression name local storage, or take a local's address? */
271
+ function reachesStorage(
272
+ e: Expr,
273
+ storage: { readonly names: ReadonlySet<string>; readonly locals: ReadonlySet<string> },
274
+ ): boolean {
275
+ return [...walkExprs([{ k: 'exprstmt', value: e }])].some((x) => {
276
+ const n = mentionedName(x);
277
+ return n !== undefined && (storage.names.has(n) || (x.k === 'addr' && storage.locals.has(n)));
278
+ });
279
+ }
280
+
166
281
  /** Every `store` in the tree. ORDER-FREE by construction, which is what lets it use the shared
167
282
  * `stmtChildren` (a switch's default sits at `defaultAt`, not last): its one caller only sets
168
283
  * `written` on entries the preceding `walkExprs` pass already created — `note` builds an entry
@@ -189,16 +189,21 @@ function condOtherMentions(cond: Expr, E: Expr, v: string): boolean {
189
189
  };
190
190
  return other ? mentions(other) : false;
191
191
  }
192
- /** cond compares E against a pure operand → same comparison reading the named var. */
192
+ /** cond compares E against a pure operand → same comparison reading the named var.
193
+ * Both rebuilds below SPREAD the compare, so its `unsignedCmp` evidence (l3/ast.ts) rides along:
194
+ * the copy is declared at E's type, which keeps the compare's C meaning but not its asm meaning,
195
+ * and rank.ts's judgment of the finished tree (typing.ts `signedOverUnsignedCompare`) is what
196
+ * says whether `w0 >= a1` still compares the way the machine did. The negation keeps it too: the
197
+ * complement of an unsigned ordering is unsigned. */
193
198
  function rewriteCond(cond: Expr, E: Expr, name: string): Expr | null {
194
199
  if (cond.k !== 'bin' || !(cond.op in FLIP)) {
195
200
  return null;
196
201
  }
197
202
  if (exprEq(cond.l, E) && isPure(cond.r)) {
198
- return { k: 'bin', op: cond.op, l: { k: 'var', name }, r: cond.r };
203
+ return { ...cond, l: { k: 'var', name } };
199
204
  }
200
205
  if (exprEq(cond.r, E) && isPure(cond.l)) {
201
- return { k: 'bin', op: cond.op, l: cond.l, r: { k: 'var', name } };
206
+ return { ...cond, r: { k: 'var', name } };
202
207
  }
203
208
  return null;
204
209
  }
@@ -207,7 +212,7 @@ function flipCmp(cond: Expr): Expr | null {
207
212
  return null;
208
213
  }
209
214
  const op = FLIP[cond.op];
210
- return op ? { k: 'bin', op: op as Extract<Expr, { k: 'bin' }>['op'], l: cond.l, r: cond.r } : null;
215
+ return op ? { ...cond, op: op as Extract<Expr, { k: 'bin' }>['op'] } : null;
211
216
  }
212
217
  /** in `upd`, the occurrence of subtree E replaced by var `name` (E was just assigned to it). */
213
218
  function renameSubexpr(e: Expr, E: Expr, name: string): Expr {