@asmlift/core 0.9.0 → 0.11.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.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
|
@@ -40,15 +40,16 @@
|
|
|
40
40
|
// is NEGATABLE whenever the orientation inverts it — an icmp by opcode swap or a `logic_and`/`logic_or` by
|
|
41
41
|
// De Morgan, both via `negateCondOps`, which the control-flow form below shares. Any deviation falls
|
|
42
42
|
// through untouched (a miss, never a miscompile).
|
|
43
|
-
import { disjointConstSlots } from '../ir/alias';
|
|
43
|
+
import { constDisplacementOf, disjointConstSlots } from '../ir/alias';
|
|
44
44
|
import {
|
|
45
|
+
type AttrVal,
|
|
45
46
|
Block,
|
|
46
47
|
Fn,
|
|
47
48
|
Op,
|
|
48
49
|
Successor,
|
|
49
50
|
Value,
|
|
50
51
|
defOpMap,
|
|
51
|
-
|
|
52
|
+
dominance,
|
|
52
53
|
foldWriteOrder,
|
|
53
54
|
forwardingTarget,
|
|
54
55
|
mkOp,
|
|
@@ -58,12 +59,19 @@ import {
|
|
|
58
59
|
replaceAllUsesWith,
|
|
59
60
|
successorsOf,
|
|
60
61
|
} from '../ir/core';
|
|
61
|
-
import {
|
|
62
|
-
|
|
62
|
+
import {
|
|
63
|
+
effectful,
|
|
64
|
+
forgetHelperPlacement,
|
|
65
|
+
orderSensitive,
|
|
66
|
+
placedAt,
|
|
67
|
+
reevalUnsafe,
|
|
68
|
+
speculationUnsafe,
|
|
69
|
+
} from '../ir/discipline';
|
|
70
|
+
import { MEM_BASE_OPS, NEGATED_FCMP, NEGATED_ICMP } from '../ir/opcodes';
|
|
63
71
|
import { T } from '../ir/types';
|
|
64
72
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
65
73
|
|
|
66
|
-
const BOOL_OPS = new Set([...Object.keys(NEGATED_ICMP), 'logic_and', 'logic_or']);
|
|
74
|
+
const BOOL_OPS = new Set([...Object.keys(NEGATED_ICMP), ...Object.keys(NEGATED_FCMP), 'logic_and', 'logic_or']);
|
|
67
75
|
|
|
68
76
|
/** Run `step` until it stops rewriting, and answer whether it ever did.
|
|
69
77
|
*
|
|
@@ -370,11 +378,14 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
370
378
|
// (`af:adds:ido7.1` and the `divv`/`gcd`/`modv` rows).
|
|
371
379
|
// - ^g holds a side effect — its ops move into ^h, which runs UNCONDITIONALLY. A store in `b`
|
|
372
380
|
// would then execute even when `a` already decided the branch. (`a || (*p = 1)`.)
|
|
373
|
-
// - a value defined in ^g is used more than once inside ^g
|
|
374
|
-
// into a local, which renders as a statement BEFORE
|
|
375
|
-
// computation into an unconditional one. Single-use-and-
|
|
376
|
-
// analysis.ts inlines into the connective's right operand, where
|
|
377
|
-
// re-guards it.
|
|
381
|
+
// - a value defined in ^g is used more than once inside ^g, and `SHARED_VALUE_GATES` refuses it.
|
|
382
|
+
// Such a value the structurer MATERIALIZES into a local, which renders as a statement BEFORE
|
|
383
|
+
// the `if` — turning `b`'s conditional computation into an unconditional one. Single-use-and-
|
|
384
|
+
// local is precisely the shape analysis.ts inlines into the connective's right operand, where
|
|
385
|
+
// C's own short-circuit re-guards it. The table is what keeps a load in `b` from being hoisted
|
|
386
|
+
// across the guard in `a`; what it admits is an ADDRESS (`&gS`, `&gS + k`), which reads nothing,
|
|
387
|
+
// so wherever it is spelled it runs nothing the source did not (l3/basecse.ts's
|
|
388
|
+
// `guarded-operand` keeps the default spelling inline under the operand).
|
|
378
389
|
// - a value defined in ^g is ALSO read past ^g, and a rule in `ARM_REREAD_GATES` refuses
|
|
379
390
|
// re-deriving it in the arm. A read past ^g is otherwise not a refusal: on verified IR it can
|
|
380
391
|
// only sit under the arm ^g's non-shared edge enters (^g dominates nothing else — the shared
|
|
@@ -544,6 +555,9 @@ export interface BranchShortCircuitOptions {
|
|
|
544
555
|
* in {@link ARM_REREAD_GATES} reads — `read-behind-effect`. Threaded by raise/pre-recovery.ts from
|
|
545
556
|
* the target it already holds, the way `narrowlocal`'s `hoistsSingleSetArm` is. Absent ⇒ false. */
|
|
546
557
|
reloadsLocalReread?: boolean;
|
|
558
|
+
/** The USE-COUNT refusals — {@link SHARED_VALUE_GATES} when absent. A parameter for the same two
|
|
559
|
+
* readers as `armReread`: a census (`bench gates --pass shared-value`) and a test's ablation. */
|
|
560
|
+
sharedValue?: readonly Gate<SharedValueSite>[];
|
|
547
561
|
}
|
|
548
562
|
|
|
549
563
|
export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOptions = {}): boolean {
|
|
@@ -639,7 +653,7 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
639
653
|
// `r0` values. The verdict is a conjunction of pure tests, so the order moves no fold. And
|
|
640
654
|
// BEFORE tree ownership, so `onTreeOwned` keeps its meaning — the one thing in the way.
|
|
641
655
|
const reread = armRereadCone(fn, g, otherEdge.block, preds, opts);
|
|
642
|
-
if (!reread || !definedValuesStayLocal(fn, g, reread)) {
|
|
656
|
+
if (!reread || !definedValuesStayLocal(fn, g, reread, defs, opts.sharedValue ?? SHARED_VALUE_GATES)) {
|
|
643
657
|
continue;
|
|
644
658
|
}
|
|
645
659
|
// LAST refusal, so `onTreeOwned` reports a site where tree ownership is the ONE thing in the
|
|
@@ -805,7 +819,9 @@ const NEGATE_BUDGET = 8;
|
|
|
805
819
|
/** The ops computing `!v`, or `null` when `v` cannot be negated.
|
|
806
820
|
*
|
|
807
821
|
* Two cases, and no third:
|
|
808
|
-
* - an `icmp_*` in `NEGATED_ICMP` → the swapped-opcode
|
|
822
|
+
* - an `icmp_*` in `NEGATED_ICMP`, or an `fcmp_*` in `NEGATED_FCMP` → the swapped-opcode
|
|
823
|
+
* comparison over the SAME operands. A float compare negates to its unordered twin, never to
|
|
824
|
+
* the flipped relation, which a NaN would tell apart.
|
|
809
825
|
* - a `logic_and`/`logic_or` → the DUAL connective over its two recursively negated operands.
|
|
810
826
|
* De Morgan: `!(a || b)` is `!a && !b`. This is the only reason the helper is recursive, and
|
|
811
827
|
* it is what lets a chain fold past its first level (`a || !(b || c)`).
|
|
@@ -823,8 +839,9 @@ const NEGATE_BUDGET = 8;
|
|
|
823
839
|
* - the cone exceeds `NEGATE_BUDGET` minted ops.
|
|
824
840
|
*
|
|
825
841
|
* 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
|
|
827
|
-
* ever rebuilt, all of them pure, so no effect can be duplicated or reordered by
|
|
842
|
+
* pass list's own `dce: true` (raise/pre-recovery.ts). Only `icmp_*`/`fcmp_*`/`logic_and`/
|
|
843
|
+
* `logic_or` are ever rebuilt, all of them pure, so no effect can be duplicated or reordered by
|
|
844
|
+
* construction.
|
|
828
845
|
*
|
|
829
846
|
* Nothing is SHARED between paths, and that is the point rather than a limitation. Memoizing `go`
|
|
830
847
|
* in a `Map<Value, Value>` is three lines and would make the budget unnecessary — and it would
|
|
@@ -833,11 +850,11 @@ const NEGATE_BUDGET = 8;
|
|
|
833
850
|
* Nothing collapses the duplicates later either — `numberPureValues` runs as `addrnum`, far ahead
|
|
834
851
|
* of both folds. So the per-path rebuild is the mechanism and NEGATE_BUDGET is its price.
|
|
835
852
|
*
|
|
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
|
|
838
|
-
* un-negated head straight on as the merge VALUE, which is why that caller checks
|
|
839
|
-
* itself before deciding whether to call here at all (see its head gate); the
|
|
840
|
-
* such check, because its result only ever feeds a `cond_br`.
|
|
853
|
+
* What it GUARANTEES about its result, which one caller leans on: every op it mints is an `icmp_*`,
|
|
854
|
+
* an `fcmp_*` or a `logic_and`/`logic_or`, so the returned value is always a C boolean. The value
|
|
855
|
+
* form hands an un-negated head straight on as the merge VALUE, which is why that caller checks
|
|
856
|
+
* booleanness for itself before deciding whether to call here at all (see its head gate); the
|
|
857
|
+
* branch form needs no such check, because its result only ever feeds a `cond_br`.
|
|
841
858
|
*
|
|
842
859
|
* PRECONDITION, earned by the caller and not checked here: every value in the cone must dominate
|
|
843
860
|
* the point the caller splices `ops` into — the minted comparisons reuse the ORIGINAL leaf
|
|
@@ -860,8 +877,18 @@ function negateCondOps(defs: Map<Value, Op>, v: Value, budget: number): { ops: O
|
|
|
860
877
|
return null;
|
|
861
878
|
}
|
|
862
879
|
const out = mkValue(T.unk(32));
|
|
863
|
-
|
|
864
|
-
|
|
880
|
+
const negated = NEGATED_ICMP[d.opcode] ?? NEGATED_FCMP[d.opcode];
|
|
881
|
+
// A float compare whose runtime helper names the relation the source spelled (raise/
|
|
882
|
+
// floathelpers.ts `attrs.relation`) has no negation that calls the same helper — gcc folds
|
|
883
|
+
// `!(a == b)` into `a != b` — so a fold that would negate it off that relation refuses, and one
|
|
884
|
+
// that negates it back onto the relation keeps the stamp for the structurer to spell.
|
|
885
|
+
const relation = d.attrs.relation;
|
|
886
|
+
if (negated && relation === d.opcode) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
if (negated) {
|
|
890
|
+
const attrs: Record<string, AttrVal> = typeof relation === 'string' ? { relation } : {};
|
|
891
|
+
ops.push(mkOp(negated, { operands: [...d.operands], results: [out], attrs }));
|
|
865
892
|
return out;
|
|
866
893
|
}
|
|
867
894
|
if (d.opcode === 'logic_and' || d.opcode === 'logic_or') {
|
|
@@ -1057,10 +1084,28 @@ function armRereadSite(
|
|
|
1057
1084
|
/** Whether leaving ^g for `arm` leaves a natural loop ^g is in — some back-edge `t→h` (h dominates
|
|
1058
1085
|
* t) whose body, h plus everything reaching t without passing h, holds ^g and not the arm. */
|
|
1059
1086
|
function leavesALoop(fn: Fn, g: Block, arm: Block, preds: ReadonlyMap<Block, readonly Block[]>): boolean {
|
|
1060
|
-
|
|
1087
|
+
return naturalLoopBodies(fn, preds).some((body) => body.has(g) && !body.has(arm));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** The natural loop bodies of `fn`'s CFG as `preds` describes it, one per back-edge `t→h` (h
|
|
1091
|
+
* dominates t): h plus everything reaching t without passing h.
|
|
1092
|
+
*
|
|
1093
|
+
* MEMOISED ON THE PREDECESSOR MAP. `recognizeBranchShortCircuit` builds one `preds` per scan of the
|
|
1094
|
+
* function and rescans after every fold, so one map stands for one CFG: every site that scan asks
|
|
1095
|
+
* `loop-exit` about shares one dominator computation instead of paying one each. That was the cost
|
|
1096
|
+
* of a large function's plain lift: Klonoa's sub_0800D188 (1,783 blocks) spent most of its first
|
|
1097
|
+
* five minutes rebuilding these dominator sets per site. A caller that mutates the CFG under a map
|
|
1098
|
+
* it keeps would read stale loops, as it already reads stale predecessors. */
|
|
1099
|
+
function naturalLoopBodies(fn: Fn, preds: ReadonlyMap<Block, readonly Block[]>): readonly Set<Block>[] {
|
|
1100
|
+
const cached = LOOP_BODIES.get(preds);
|
|
1101
|
+
if (cached !== undefined && cached.fn === fn) {
|
|
1102
|
+
return cached.bodies;
|
|
1103
|
+
}
|
|
1104
|
+
const dominates = dominance(fn);
|
|
1105
|
+
const bodies: Set<Block>[] = [];
|
|
1061
1106
|
for (const t of fn.blocks) {
|
|
1062
1107
|
for (const h of successorsOf(t)) {
|
|
1063
|
-
if (!
|
|
1108
|
+
if (!dominates(h, t)) {
|
|
1064
1109
|
continue;
|
|
1065
1110
|
}
|
|
1066
1111
|
const body = new Set<Block>([h]);
|
|
@@ -1071,13 +1116,13 @@ function leavesALoop(fn: Fn, g: Block, arm: Block, preds: ReadonlyMap<Block, rea
|
|
|
1071
1116
|
work.push(...(preds.get(b) ?? []));
|
|
1072
1117
|
}
|
|
1073
1118
|
}
|
|
1074
|
-
|
|
1075
|
-
return true;
|
|
1076
|
-
}
|
|
1119
|
+
bodies.push(body);
|
|
1077
1120
|
}
|
|
1078
1121
|
}
|
|
1079
|
-
|
|
1122
|
+
LOOP_BODIES.set(preds, { fn, bodies });
|
|
1123
|
+
return bodies;
|
|
1080
1124
|
}
|
|
1125
|
+
const LOOP_BODIES = new WeakMap<ReadonlyMap<Block, readonly Block[]>, { fn: Fn; bodies: readonly Set<Block>[] }>();
|
|
1081
1126
|
|
|
1082
1127
|
/** Whether analysis.ts would spell the arm's copy of a READ as a LOCAL rather than inline — a
|
|
1083
1128
|
* MIRROR of analysis.ts's placement and barrier rules (structure/analysis.ts: `emitPos`, the
|
|
@@ -1377,9 +1422,10 @@ function rereadInArm(fn: Fn, g: Block, arm: Block, { copy, drop }: ArmReread): v
|
|
|
1377
1422
|
g.ops = g.ops.filter((op) => !drop.has(op));
|
|
1378
1423
|
}
|
|
1379
1424
|
|
|
1380
|
-
/** True when
|
|
1381
|
-
* `
|
|
1382
|
-
* dropped original leave with it. The use-count half is the residue
|
|
1425
|
+
/** True when no value `g` defines escapes it un-re-derived, and every value `g` reads more than
|
|
1426
|
+
* once itself passes {@link SHARED_VALUE_GATES} (or `gates`) — reads outside `g` move to the arm's
|
|
1427
|
+
* copy, and reads BY a dropped original leave with it. The use-count half is the residue
|
|
1428
|
+
* ARM_REREAD_GATES names, and the table below is that half.
|
|
1383
1429
|
*
|
|
1384
1430
|
* The VALUE form above needs no such check, and the asymmetry is real rather than drift: its feeder
|
|
1385
1431
|
* ends in `br M`, so the feeder has no successor of its own to dominate and every value it defines
|
|
@@ -1392,7 +1438,13 @@ function rereadInArm(fn: Fn, g: Block, arm: Block, { copy, drop }: ArmReread): v
|
|
|
1392
1438
|
* 'the ENTRY block is never folded away' for the branch form. They are deliberately NOT routed
|
|
1393
1439
|
* through one shared `isEntry` helper — a helper enforces nothing, and it is the two tests that
|
|
1394
1440
|
* hold each fold to the refusal. When changing either fold, check the other. */
|
|
1395
|
-
function definedValuesStayLocal(
|
|
1441
|
+
function definedValuesStayLocal(
|
|
1442
|
+
fn: Fn,
|
|
1443
|
+
g: Block,
|
|
1444
|
+
{ copy, drop }: ArmReread,
|
|
1445
|
+
defs: ReadonlyMap<Value, Op>,
|
|
1446
|
+
gates: readonly Gate<SharedValueSite>[],
|
|
1447
|
+
): boolean {
|
|
1396
1448
|
const defined = new Set<Value>(g.ops.flatMap((op) => op.results));
|
|
1397
1449
|
if (defined.size === 0) {
|
|
1398
1450
|
return true;
|
|
@@ -1419,11 +1471,142 @@ function definedValuesStayLocal(fn: Fn, g: Block, { copy, drop }: ArmReread): bo
|
|
|
1419
1471
|
}
|
|
1420
1472
|
}
|
|
1421
1473
|
// ZERO uses is fine — a dead op renders nothing at all, so it cannot escape the short circuit.
|
|
1422
|
-
//
|
|
1423
|
-
|
|
1424
|
-
return [...defined].every(
|
|
1474
|
+
// ONE inlines into the connective's operand. TWO or more is the table's question.
|
|
1475
|
+
const defOf = new Map<Value, Op>(g.ops.flatMap((op) => op.results.map((r): [Value, Op] => [r, op])));
|
|
1476
|
+
return [...defined].every(
|
|
1477
|
+
(value) => (uses.get(value) ?? 0) <= 1 || firstRejection(gates, { g, value, defOf, defs }) === null,
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/** One value ^g reads more than once itself — what analysis.ts would otherwise spell as a local. */
|
|
1482
|
+
export interface SharedValueSite {
|
|
1483
|
+
/** ^g, the second condition block the fold would merge into ^h */
|
|
1484
|
+
readonly g: Block;
|
|
1485
|
+
/** the value, defined in ^g and read there 2+ times */
|
|
1486
|
+
readonly value: Value;
|
|
1487
|
+
/** the op in ^g defining each value ^g defines */
|
|
1488
|
+
readonly defOf: ReadonlyMap<Value, Op>;
|
|
1489
|
+
/** the defining op of every value in the function — an address's symbol and offsets are often
|
|
1490
|
+
* defined ABOVE ^g, where GVN numbered them with an earlier use */
|
|
1491
|
+
readonly defs: ReadonlyMap<Value, Op>;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/** The part of `v`'s operand cone ^g computes — the ops a hoisted local would carry above the `if`. */
|
|
1495
|
+
function gConeOf(c: SharedValueSite): Op[] {
|
|
1496
|
+
const out = new Set<Op>();
|
|
1497
|
+
for (const work = [c.value]; work.length;) {
|
|
1498
|
+
const d = c.defOf.get(work.pop()!);
|
|
1499
|
+
if (d && !out.has(d)) {
|
|
1500
|
+
out.add(d);
|
|
1501
|
+
work.push(...d.operands);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return [...out];
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** Is `v` constant offsets over a symbol or frame address (`&gS + 0x35`), or over a literal address
|
|
1508
|
+
* or a value ^g did not compute (`p - 0x35` off a pointer parameter) that ^g reads only as a
|
|
1509
|
+
* memory ADDRESS? The chain is ir/alias.ts `constDisplacementOf`, the one reading of address
|
|
1510
|
+
* arithmetic, so Thumb's `sub r0, r0, #0x35` for a negative member offset ends where `add` does.
|
|
1511
|
+
* Asked over the WHOLE function's defs, not ^g's: agbcc's `add r0, r2, #0x35` off an `&gS` an
|
|
1512
|
+
* earlier block loaded is GVN's one `gaddr` (`sub_0800D188`'s site). A root ^g did not compute (a
|
|
1513
|
+
* parameter, an earlier block's result) is, by dominance, already computed when the first test
|
|
1514
|
+
* runs, so the cone spelled above the guard (as a local, or l3/basecse.ts's hoisted base) adds only
|
|
1515
|
+
* `± k` arithmetic, which reads nothing and cannot trap. That root is admitted only when `v` is
|
|
1516
|
+
* read as an address alone (`readAsAddress`): the pointer-parameter base is measured (agbcc's
|
|
1517
|
+
* `add r0, r4, #0x35; ldrb; add r0, r0, #0x22` off `struct T *p` folds to the target's bytes,
|
|
1518
|
+
* synthetic:scbase_pfar), while a counter's `i + 1` read twice by a latch's compare is not an
|
|
1519
|
+
* address, and folding it turns synthetic:retpost_notail on gcc2.7.2 and gcc2.7.2kmc into a
|
|
1520
|
+
* structure decline. */
|
|
1521
|
+
function constOffsetCone(c: SharedValueSite): boolean {
|
|
1522
|
+
const disp = constDisplacementOf(c.defs, c.value);
|
|
1523
|
+
const root = disp?.root ?? c.value;
|
|
1524
|
+
const leaf = c.defs.get(root);
|
|
1525
|
+
if (leaf?.opcode === 'gaddr' || leaf?.opcode === 'laddr') {
|
|
1526
|
+
return true;
|
|
1527
|
+
}
|
|
1528
|
+
return (readsNothing(leaf) || (disp !== null && !c.defOf.has(root))) && readAsAddress(c.g, c.value);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/** Is `leaf` an operand-free op that reads nothing and cannot trap — a value spelled the same at
|
|
1532
|
+
* any program point? The property the root argument above rests on, asked of the op rather than
|
|
1533
|
+
* of a list of opcodes: agbcc's `ldr r0, =0x03005220` under `#define gL (*(struct S *)0x03005220)`
|
|
1534
|
+
* is a `const` the second test reads three members off, exactly as it reads them off `&gS`. Unlike
|
|
1535
|
+
* a symbol it is admitted only where read as an address (`readAsAddress`): a mask or a compare
|
|
1536
|
+
* constant read twice is not a base, and its spelling is unmeasured. */
|
|
1537
|
+
function readsNothing(leaf: Op | undefined): boolean {
|
|
1538
|
+
return leaf !== undefined && leaf.operands.length === 0 && !reevalUnsafe(leaf);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/** Does every read ^g makes of `v` end in a memory access's ADDRESS: the base of a load or store,
|
|
1542
|
+
* or the pointer side of `v + x` / `v - x` whose result is in turn read only that way? An index
|
|
1543
|
+
* may be a variable (`p->arr[i]` is `add(p + 0x35, i)`), since it is the sum, not `v`, that it
|
|
1544
|
+
* joins. A SUCCESSOR ARGUMENT is a value read — a loop-carried `p + 1` is the next iteration's
|
|
1545
|
+
* pointer, a latch's `i + 1` its counter — and so is a compare, a mask, or a stored value. */
|
|
1546
|
+
function readAsAddress(g: Block, v: Value): boolean {
|
|
1547
|
+
return g.ops.every((op) => {
|
|
1548
|
+
if (!readsOf(op).includes(v)) {
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
if (op.successors.some((s) => s.args.includes(v))) {
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
const [first, ...rest] = op.operands;
|
|
1555
|
+
if (MEM_BASE_OPS.has(op.opcode)) {
|
|
1556
|
+
return first === v && !rest.includes(v);
|
|
1557
|
+
}
|
|
1558
|
+
const once = op.operands.length === 2 && op.operands.filter((x) => x === v).length === 1;
|
|
1559
|
+
if (once && (op.opcode === 'add' || (op.opcode === 'sub' && first === v))) {
|
|
1560
|
+
return op.results.every((r) => readAsAddress(g, r));
|
|
1561
|
+
}
|
|
1562
|
+
return false;
|
|
1563
|
+
});
|
|
1425
1564
|
}
|
|
1426
1565
|
|
|
1566
|
+
// ── THE USE-COUNT REFUSALS, AS DATA ──────────────────────────────────────────────────────────
|
|
1567
|
+
//
|
|
1568
|
+
// A value ^g reads twice itself is one analysis.ts materializes into a local, and a local is a
|
|
1569
|
+
// statement BEFORE the `if`: `b`'s computation runs whether or not `a` decided the branch. Taken as
|
|
1570
|
+
// an OPTIONAL parameter (`BranchShortCircuitOptions.sharedValue`) and censused from outside core by
|
|
1571
|
+
// `pnpm bench gates --pass shared-value`, through the same `PRE_RECOVERY_PASSES` seam as
|
|
1572
|
+
// `arm-reread`. Asked once per such value, below every other refusal of the fold but tree ownership.
|
|
1573
|
+
//
|
|
1574
|
+
// What the table ADMITS is a shared ADDRESS: agbcc loads `&gS` into one register and reads three
|
|
1575
|
+
// members off it (`(gS.a | gS.d | gS.c) != 0`), so the address is the one value with three readers.
|
|
1576
|
+
// Refused, the `||` splits into a nest and the structurer copies the shared arm AND everything after
|
|
1577
|
+
// it into both branches, once per such site (kleod's `sub_0800D188`, whose `&gUnk_03005220 + 0x35`
|
|
1578
|
+
// is the shape synthetic:scbase_far pins). The base may also be a literal address, a pointer
|
|
1579
|
+
// parameter (`p->a | p->d`) or another value computed before ^g, when ^g reads it only as an
|
|
1580
|
+
// address: a constant-offset cone over it reads nothing and cannot trap, so hoisting it changes no
|
|
1581
|
+
// behaviour.
|
|
1582
|
+
//
|
|
1583
|
+
// RESIDUE, not in the table: the ESCAPE half of `definedValuesStayLocal` (a value read past ^g that
|
|
1584
|
+
// the arm does not re-derive). `armRereadCone` copies the cone of every outside read, so on the
|
|
1585
|
+
// inputs it accepts that half has nothing left to see; it stays as the check that keeps it so.
|
|
1586
|
+
export const SHARED_VALUE_GATES: readonly Gate<SharedValueSite>[] = [
|
|
1587
|
+
{
|
|
1588
|
+
// `if (p == 0 || (*p | *p >> 4) != 0)`: the read, materialized, runs above the null test.
|
|
1589
|
+
// `if (b == 0 || (q = a / b) + q != 0)`: so does the divide, on the path the test kept it off.
|
|
1590
|
+
// A named local is an UNCONDITIONAL statement, so the predicate is `reevalUnsafe` (effects,
|
|
1591
|
+
// reads AND traps), not `orderSensitive`, which has no trap half.
|
|
1592
|
+
id: 'shared-read',
|
|
1593
|
+
why: 'a read or trapping op the second test makes twice would be spelled as a local, run before the first test',
|
|
1594
|
+
sound: true,
|
|
1595
|
+
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a READ the second test consumes twice would run above the first',
|
|
1596
|
+
rejects: (c) => gConeOf(c).some(reevalUnsafe),
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
// A pure, non-trapping value that is not an address `constOffsetCone` admits — `(x * x) != 0`
|
|
1600
|
+
// over a computed `x`. Hoisting it is correct C; what the target spells there is unmeasured, so
|
|
1601
|
+
// it keeps the nest. A pure SPELLING rule: disjoint from `shared-read` (a read or a trap in the cone
|
|
1602
|
+
// is that rule's), so each ablates alone and ablating this one admits nothing unsafe.
|
|
1603
|
+
id: 'shared-pure',
|
|
1604
|
+
why: 'a computed value the second test reuses is kept in the nest until its spelling is measured',
|
|
1605
|
+
sound: false,
|
|
1606
|
+
rejects: (c) => !constOffsetCone(c) && !gConeOf(c).some(reevalUnsafe),
|
|
1607
|
+
},
|
|
1608
|
+
];
|
|
1609
|
+
|
|
1427
1610
|
/** Two successor argument lists that a fold may collapse into one: same values, same order. */
|
|
1428
1611
|
function sameArgs(a: Value[], b: Value[]): boolean {
|
|
1429
1612
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
package/src/raise/softdiv.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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, {
|
|
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
|
}
|