@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
@@ -42,6 +42,7 @@
42
42
  // through untouched (a miss, never a miscompile).
43
43
  import { disjointConstSlots } from '../ir/alias';
44
44
  import {
45
+ type AttrVal,
45
46
  Block,
46
47
  Fn,
47
48
  Op,
@@ -59,11 +60,11 @@ import {
59
60
  successorsOf,
60
61
  } from '../ir/core';
61
62
  import { effectful, forgetHelperPlacement, orderSensitive, placedAt, speculationUnsafe } from '../ir/discipline';
62
- import { NEGATED_ICMP } from '../ir/opcodes';
63
+ import { NEGATED_FCMP, NEGATED_ICMP } from '../ir/opcodes';
63
64
  import { T } from '../ir/types';
64
65
  import { type Gate, firstRejection } from '../l3/gates';
65
66
 
66
- const BOOL_OPS = new Set([...Object.keys(NEGATED_ICMP), 'logic_and', 'logic_or']);
67
+ const BOOL_OPS = new Set([...Object.keys(NEGATED_ICMP), ...Object.keys(NEGATED_FCMP), 'logic_and', 'logic_or']);
67
68
 
68
69
  /** Run `step` until it stops rewriting, and answer whether it ever did.
69
70
  *
@@ -805,7 +806,9 @@ const NEGATE_BUDGET = 8;
805
806
  /** The ops computing `!v`, or `null` when `v` cannot be negated.
806
807
  *
807
808
  * Two cases, and no third:
808
- * - an `icmp_*` in `NEGATED_ICMP` → the swapped-opcode comparison over the SAME operands.
809
+ * - an `icmp_*` in `NEGATED_ICMP`, or an `fcmp_*` in `NEGATED_FCMP` → the swapped-opcode
810
+ * comparison over the SAME operands. A float compare negates to its unordered twin, never to
811
+ * the flipped relation, which a NaN would tell apart.
809
812
  * - a `logic_and`/`logic_or` → the DUAL connective over its two recursively negated operands.
810
813
  * De Morgan: `!(a || b)` is `!a && !b`. This is the only reason the helper is recursive, and
811
814
  * it is what lets a chain fold past its first level (`a || !(b || c)`).
@@ -823,8 +826,9 @@ const NEGATE_BUDGET = 8;
823
826
  * - the cone exceeds `NEGATE_BUDGET` minted ops.
824
827
  *
825
828
  * Ops are MINTED, never mutated: the originals stay where the caller hoisted them and die to the
826
- * pass list's own `dce: true` (raise/pre-recovery.ts). Only `icmp_*`/`logic_and`/`logic_or` are
827
- * ever rebuilt, all of them pure, so no effect can be duplicated or reordered by construction.
829
+ * pass list's own `dce: true` (raise/pre-recovery.ts). Only `icmp_*`/`fcmp_*`/`logic_and`/
830
+ * `logic_or` are ever rebuilt, all of them pure, so no effect can be duplicated or reordered by
831
+ * construction.
828
832
  *
829
833
  * Nothing is SHARED between paths, and that is the point rather than a limitation. Memoizing `go`
830
834
  * in a `Map<Value, Value>` is three lines and would make the budget unnecessary — and it would
@@ -833,11 +837,11 @@ const NEGATE_BUDGET = 8;
833
837
  * Nothing collapses the duplicates later either — `numberPureValues` runs as `addrnum`, far ahead
834
838
  * of both folds. So the per-path rebuild is the mechanism and NEGATE_BUDGET is its price.
835
839
  *
836
- * What it GUARANTEES about its result, which one caller leans on: every op it mints is an `icmp_*`
837
- * or a `logic_and`/`logic_or`, so the returned value is always a C boolean. The value form hands an
838
- * un-negated head straight on as the merge VALUE, which is why that caller checks booleanness for
839
- * itself before deciding whether to call here at all (see its head gate); the branch form needs no
840
- * such check, because its result only ever feeds a `cond_br`.
840
+ * What it GUARANTEES about its result, which one caller leans on: every op it mints is an `icmp_*`,
841
+ * an `fcmp_*` or a `logic_and`/`logic_or`, so the returned value is always a C boolean. The value
842
+ * form hands an un-negated head straight on as the merge VALUE, which is why that caller checks
843
+ * booleanness for itself before deciding whether to call here at all (see its head gate); the
844
+ * branch form needs no such check, because its result only ever feeds a `cond_br`.
841
845
  *
842
846
  * PRECONDITION, earned by the caller and not checked here: every value in the cone must dominate
843
847
  * the point the caller splices `ops` into — the minted comparisons reuse the ORIGINAL leaf
@@ -860,8 +864,18 @@ function negateCondOps(defs: Map<Value, Op>, v: Value, budget: number): { ops: O
860
864
  return null;
861
865
  }
862
866
  const out = mkValue(T.unk(32));
863
- if (NEGATED_ICMP[d.opcode]) {
864
- ops.push(mkOp(NEGATED_ICMP[d.opcode], { operands: [...d.operands], results: [out] }));
867
+ const negated = NEGATED_ICMP[d.opcode] ?? NEGATED_FCMP[d.opcode];
868
+ // A float compare whose runtime helper names the relation the source spelled (raise/
869
+ // floathelpers.ts `attrs.relation`) has no negation that calls the same helper — gcc folds
870
+ // `!(a == b)` into `a != b` — so a fold that would negate it off that relation refuses, and one
871
+ // that negates it back onto the relation keeps the stamp for the structurer to spell.
872
+ const relation = d.attrs.relation;
873
+ if (negated && relation === d.opcode) {
874
+ return null;
875
+ }
876
+ if (negated) {
877
+ const attrs: Record<string, AttrVal> = typeof relation === 'string' ? { relation } : {};
878
+ ops.push(mkOp(negated, { operands: [...d.operands], results: [out], attrs }));
865
879
  return out;
866
880
  }
867
881
  if (d.opcode === 'logic_and' || d.opcode === 'logic_or') {
@@ -49,7 +49,7 @@ export function recognizeSoftDiv(fn: Fn, target: TargetDescription): boolean {
49
49
  }
50
50
  // Fold only when BOTH arguments were recovered (the signature makes this the norm). A
51
51
  // mis-recovered arity leaves the call untouched rather than fabricating a wrong divide.
52
- if (op.operands.length !== wordsOf(helper.params) || op.results.length !== 1) {
52
+ if (op.operands.length !== wordsOf(helper.params, target) || op.results.length !== 1) {
53
53
  continue;
54
54
  }
55
55
  b.ops.splice(i, 1, helperOp(helper.op, op.attrs.target as string, op.operands, op.results[0]));
@@ -0,0 +1,153 @@
1
+ // asmlift — the signedness of a NARROW store, read off the constant it stores.
2
+ //
3
+ // A store does not say whether its lvalue is signed: `sh` writes the low half of a register either
4
+ // way, so a 1- or 2-byte `store` gets the unsigned view by convention (`storeSigned` below). But a
5
+ // compiler that converts the constant to the lvalue's type before it materializes it
6
+ // (`compilerBehaviors.narrowStoreConstantTakesLvalueType`) leaves that type in the register's UPPER
7
+ // bits, which the store throws away:
8
+ //
9
+ // u16 *p; p[6] = -1; li v0,0xffff → const 65535 → unsigned
10
+ // s16 *p; p[6] = 0xFFFF; li v0,-1 → const -1 → signed
11
+ //
12
+ // This pass records that as the store's `signed` attr, the slot an `astore` already carries for a
13
+ // member's recorded signedness; raise/arrays.ts and raise/struct-arrays.ts carry it into the
14
+ // `astore` they make. It changes no op and no value: what it moves is which view of the cell the C
15
+ // spells, and both views store the same bytes.
16
+ //
17
+ // It refuses (leaves the attr unset, so the convention stands) for:
18
+ // - a target that does not declare the behavior;
19
+ // - a store that already carries `signed`, or a word-wide one;
20
+ // - a stored value that is not a `const` — a `neg`/`shl` the materialization left unfolded, a
21
+ // block param, anything computed;
22
+ // - a constant whose narrow sign bit is clear: both lvalue types materialize it the same way;
23
+ // - a constant whose upper bits are neither the zero- nor the sign-extension of its low bits:
24
+ // no conversion to a narrow type produces one;
25
+ // - a constant some OTHER use reads too, or whose 32-bit value another store writes at another
26
+ // width. The conversion is a fact about a LITERAL on the right of the store. A variable or a
27
+ // chained assignment whose value is a known constant is stored unconverted, in the register
28
+ // a wider use reads: `s32 v = -1; *q = v; p[1] = v;` with `u16 *p` is `li v0,-1; sw v0;
29
+ // sh v0` on gcc2.7.2 and gcc2.7.2kmc, and `li t6,-1; sw t6; li t7,-1; sh t7` on ido7.1, which
30
+ // rematerializes it. When that wide use is there, the register says the wider type's sign,
31
+ // not the lvalue's (the NONLITERAL bodies of
32
+ // packages/cli/test/matching/narrow-store-constant.test.ts).
33
+ //
34
+ // What it does NOT refuse is such a variable with no wide use: `s32 v = -1; p[6] = v;` (`u16 *p`),
35
+ // or one only returned. Its register is the variable's, not the lvalue's, so the stamp can name a
36
+ // type the source did not declare (`s16 *` there). It is not refused because nothing tells it
37
+ // apart from a literal, and the spelling it gives recompiles to the register the target holds.
38
+ import { Fn, Op, Value, defOpMap } from '../ir/core';
39
+ import type { TargetDescription } from '../target';
40
+
41
+ /** THE view a store's lvalue is seeded at, `width` bytes wide (`width` for a `store`, `elemSize`
42
+ * for an `astore`): the `signed` a recovery recorded on it — this pass's stamp, or a member
43
+ * array's own declaration (raise/memberarrays.ts) — else the convention, word ⇒ signed and
44
+ * narrow ⇒ unsigned. raise/recover.ts, raise/structs.ts, raise/memberarrays.ts, rank-declare.ts
45
+ * and the structurer seed from it; raise/struct-arrays.ts keeps the stamp beside its loads'
46
+ * sign instead. What each does when a LOAD disagrees is its own: raise/structs.ts,
47
+ * raise/struct-arrays.ts and rank-declare.ts prefer the load, and the structurer spells a member
48
+ * that disagrees with the stamp as a cast cell (structure/structure.ts `stampedStoreLval`). */
49
+ export function storeSigned(op: Op, width: number): boolean {
50
+ return (op.attrs.signed as boolean | undefined) ?? width === 4;
51
+ }
52
+
53
+ /** The lvalue signedness a constant stored at `width` bytes witnesses, read off the 32-bit register
54
+ * it was materialized into: `true` sign-extended, `false` zero-extended, `null` when it says
55
+ * neither (see the refusals at the top of this file). */
56
+ export function storedConstantSign(value: number, width: number): boolean | null {
57
+ if (!Number.isInteger(value) || value < -(2 ** 31) || value >= 2 ** 32 || (width !== 1 && width !== 2)) {
58
+ return null;
59
+ }
60
+ const bits = width * 8;
61
+ const reg = value | 0;
62
+ const low = reg & ((1 << bits) - 1);
63
+ if ((low & (1 << (bits - 1))) === 0) {
64
+ return null;
65
+ }
66
+ if (reg === low) {
67
+ return false;
68
+ }
69
+ return reg === (low << (32 - bits)) >> (32 - bits) ? true : null;
70
+ }
71
+
72
+ /** Stamp `signed` on every narrow `store` whose stored constant witnesses its lvalue's signedness.
73
+ * Returns how many it stamped. */
74
+ export function seedNarrowStoreSigns(fn: Fn, target: TargetDescription): number {
75
+ if (target.compilerBehaviors.narrowStoreConstantTakesLvalueType !== true) {
76
+ return 0;
77
+ }
78
+ const defs = defOpMap(fn);
79
+ const stores: Op[] = fn.blocks.flatMap((b) => b.ops.filter((op) => op.opcode === 'store'));
80
+ // the values read other than as the value a store stores. Not a `ret`'s: before the return
81
+ // type is known it reads the return register whether the function returns it or not, and gcc
82
+ // stores a constant from that register in its delay slot (`li v0,-1; jr ra; sh v0,12(a0)`).
83
+ // Nor an edge's into a block param only a `ret` reads, directly or through more such params:
84
+ // that is the same register live out of a branch, `beqz a1,L; ...; li v0,0x8000; sh v0,4(a0);
85
+ // L: jr ra`.
86
+ const retOnly = retOnlyParams(fn);
87
+ const otherUses = new Set<Value>();
88
+ const bump = (v: Value) => otherUses.add(v);
89
+ for (const b of fn.blocks) {
90
+ for (const op of b.ops) {
91
+ op.operands.forEach((v, i) => ((op.opcode === 'store' && i === 1) || op.opcode === 'ret' ? undefined : bump(v)));
92
+ op.successors.forEach((s) => s.args.forEach((v, i) => (retOnly.has(s.block.params[i]) ? undefined : bump(v))));
93
+ }
94
+ }
95
+ // a stored constant's 32-bit register → the widths it is stored at, across every const op
96
+ const widthsOf = new Map<number, Set<number>>();
97
+ for (const op of stores) {
98
+ const d = defs.get(op.operands[1]);
99
+ if (d?.opcode === 'const') {
100
+ const reg = (d.attrs.value as number) | 0;
101
+ (widthsOf.get(reg) ?? widthsOf.set(reg, new Set()).get(reg)!).add(op.attrs.width as number);
102
+ }
103
+ }
104
+ let n = 0;
105
+ for (const op of stores) {
106
+ const d = defs.get(op.operands[1]);
107
+ if (
108
+ op.attrs.signed !== undefined ||
109
+ d?.opcode !== 'const' ||
110
+ otherUses.has(op.operands[1]) ||
111
+ widthsOf.get((d.attrs.value as number) | 0)!.size > 1
112
+ ) {
113
+ continue;
114
+ }
115
+ const signed = storedConstantSign(d.attrs.value as number, op.attrs.width as number);
116
+ if (signed !== null) {
117
+ op.attrs.signed = signed;
118
+ n++;
119
+ }
120
+ }
121
+ return n;
122
+ }
123
+
124
+ /** The block params (not the entry's: those are the function's parameters) that nothing reads but
125
+ * a `ret` and the edges into other such params. */
126
+ function retOnlyParams(fn: Fn): Set<Value> {
127
+ const read = new Set<Value>();
128
+ for (const b of fn.blocks) {
129
+ for (const op of b.ops) {
130
+ if (op.opcode !== 'ret') {
131
+ op.operands.forEach((v) => read.add(v));
132
+ }
133
+ }
134
+ }
135
+ const out = new Set(fn.blocks.slice(1).flatMap((b) => b.params.filter((p) => !read.has(p))));
136
+ // an edge into a param that is not ret-only reads its argument: drop arguments until none moves
137
+ for (let moved = true; moved;) {
138
+ moved = false;
139
+ for (const b of fn.blocks) {
140
+ for (const op of b.ops) {
141
+ for (const s of op.successors) {
142
+ s.args.forEach((v, i) => {
143
+ if (out.has(v) && !out.has(s.block.params[i])) {
144
+ out.delete(v);
145
+ moved = true;
146
+ }
147
+ });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ return out;
153
+ }
@@ -93,8 +93,16 @@ function withPadding(dataFields: StructField[], stride: number): StructField[] {
93
93
  * pokeemerald:GetGenderFromSpeciesAndPersonality, whose address is materialized twice). A base
94
94
  * whose element pointers
95
95
  * disagree on stride declines entirely: two strides over one base is a reinterpreted view or
96
- * a 2D layout, genuinely ambiguous — decline over guess. */
97
- export function recognizeStructArrays(fn: Fn): number {
96
+ * a 2D layout, genuinely ambiguous — decline over guess.
97
+ *
98
+ * `aggregateBoundary` is the size the compiler rounds EVERY struct to (target.ts
99
+ * `compilerBehaviors.aggregateBoundary`): agbcc 4, so `struct { u16 a; u8 _pad0[4]; }` is 8
100
+ * bytes there, not the 6 its fields add up to. A group whose stride that rounding would change
101
+ * declines, and its accesses keep the byte-offset spelling (`*(u16 *)(base + i * 6)`), which
102
+ * says the stride itself. ABSENT means unmeasured (target.ts), and then every group declines the
103
+ * same way: no boundary is safe to assume, since one too small is that very mis-stride on agbcc,
104
+ * and the caller must say which layout it means — a target's value, or 1 for the natural one. */
105
+ export function recognizeStructArrays(fn: Fn, aggregateBoundary: number | undefined): number {
98
106
  const defs = defOpMap(fn);
99
107
  let count = 0;
100
108
  // The NAME index is not the recognition count: `count` is this function's return value — how
@@ -148,7 +156,7 @@ export function recognizeStructArrays(fn: Fn): number {
148
156
  // load/store BASE with a field offset inside one element.
149
157
  const elemSet = new Set(elems.map((e) => e.add.results[0]));
150
158
  const indexOf = new Map(elems.map((e) => [e.add.results[0], e.index]));
151
- const accesses: { op: Op; elem: Value; off: number; width: number; signed: boolean }[] = [];
159
+ const accesses: { op: Op; elem: Value; off: number; width: number; signed: boolean; stamp?: boolean }[] = [];
152
160
  for (const bb of fn.blocks) {
153
161
  for (const op of bb.ops) {
154
162
  const isMem = (op.opcode === 'load' || op.opcode === 'store') && elemSet.has(op.operands[0]);
@@ -170,6 +178,7 @@ export function recognizeStructArrays(fn: Fn): number {
170
178
  off,
171
179
  width,
172
180
  signed: op.opcode === 'load' ? (op.attrs.signed as boolean) : width === 4,
181
+ stamp: op.opcode === 'load' ? undefined : (op.attrs.signed as boolean | undefined),
173
182
  });
174
183
  }
175
184
  }
@@ -193,13 +202,14 @@ export function recognizeStructArrays(fn: Fn): number {
193
202
  // silently drops one side's zero/sign-extension (adversarially learned: `arr[i].f +
194
203
  // (u16)arr[i].f` lost its zext and a byte-exact match with it). A STORE's `signed` is the
195
204
  // width===4 CONVENTION, not a machine fact, so it never conflicts — the field takes its
196
- // signedness from the loads when any exist.
197
- const byOff = new Map<number, { width: number; loadSigned: boolean | null }>();
205
+ // signedness from the loads when any exist, else from the stores raise/storesign.ts stamped
206
+ // (the signed view when any says signed, as raise/structs.ts `buildStruct` prefers it).
207
+ const byOff = new Map<number, { width: number; loadSigned: boolean | null; stamped: boolean | null }>();
198
208
  for (const a of accesses) {
199
209
  const isLoad = a.op.opcode === 'load';
200
210
  const prev = byOff.get(a.off);
201
211
  if (!prev) {
202
- byOff.set(a.off, { width: a.width, loadSigned: isLoad ? a.signed : null });
212
+ byOff.set(a.off, { width: a.width, loadSigned: isLoad ? a.signed : null, stamped: a.stamp ?? null });
203
213
  } else if (prev.width !== a.width) {
204
214
  clean = false;
205
215
  } else if (isLoad) {
@@ -208,6 +218,8 @@ export function recognizeStructArrays(fn: Fn): number {
208
218
  } else if (prev.loadSigned !== a.signed) {
209
219
  clean = false;
210
220
  }
221
+ } else if (a.stamp !== undefined) {
222
+ prev.stamped = prev.stamped === true || a.stamp;
211
223
  }
212
224
  }
213
225
  const offs = [...byOff.entries()].sort(([x], [y]) => x - y);
@@ -222,16 +234,19 @@ export function recognizeStructArrays(fn: Fn): number {
222
234
  }
223
235
  maxAlign = Math.max(maxAlign, width);
224
236
  }
225
- if (stride % maxAlign !== 0) {
237
+ // The DECLARED struct is rounded to its widest member AND to the compiler's aggregate
238
+ // boundary; a stride neither divides is one `sizeof` cannot reproduce, and recompiled it reads
239
+ // the wrong bytes and stores past a frame buffer sized from the asm (sa3 sub_8038910).
240
+ if (aggregateBoundary === undefined || stride % Math.max(maxAlign, aggregateBoundary) !== 0) {
226
241
  clean = false;
227
242
  }
228
243
  if (!clean) {
229
244
  continue;
230
245
  }
231
- const dataFields: StructField[] = offs.map(([off, { width, loadSigned }]) => ({
246
+ const dataFields: StructField[] = offs.map(([off, { width, loadSigned, stamped }]) => ({
232
247
  off,
233
- // store-only field: the width===4 convention, exactly what the old access carried
234
- type: scalarTypeForAccess(width, loadSigned ?? width === 4),
248
+ // a store-only field: its stamped stores' view, else the width===4 convention
249
+ type: scalarTypeForAccess(width, loadSigned ?? stamped ?? width === 4),
235
250
  name: `field_${off}`,
236
251
  }));
237
252
  const elemStruct = T.struct(`Elem${name++}`, withPadding(dataFields, stride), stride);
@@ -265,7 +280,11 @@ export function recognizeStructArrays(fn: Fn): number {
265
280
  } else if (op.opcode === 'store') {
266
281
  bb.ops[i] = mkOp('astore', {
267
282
  operands: [base, index, op.operands[1]],
268
- attrs: carryDiscipline(op, { elemSize: stride, fieldOff: op.attrs.off as number }),
283
+ attrs: carryDiscipline(op, {
284
+ elemSize: stride,
285
+ fieldOff: op.attrs.off as number,
286
+ ...(op.attrs.signed !== undefined && { signed: op.attrs.signed }),
287
+ }),
269
288
  });
270
289
  }
271
290
  }