@asmlift/core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/raise/retsink.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// retired 2026-09-13).
|
|
33
33
|
//
|
|
34
34
|
// ONE MODEL, NOT TWO. The predicate is `narrowlocal.ts`'s exported `armIsOneSet` — no op that
|
|
35
|
-
// `
|
|
35
|
+
// `reevalUnsafe` calls unsafe, and EXACTLY one result-producing op — read here by
|
|
36
36
|
// `arms-are-one-set`. It is cited there line by line to `jump.c:474/:480/:482/:483` and
|
|
37
37
|
// `rtlanal.c:1770-1784`, with the three things an op count alone gets wrong, and a reach census over
|
|
38
38
|
// 13733 blocks. It is SHARED rather than re-derived here because it is one optimizer guard and a
|
|
@@ -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,
|
|
@@ -58,11 +59,12 @@ import {
|
|
|
58
59
|
replaceAllUsesWith,
|
|
59
60
|
successorsOf,
|
|
60
61
|
} from '../ir/core';
|
|
61
|
-
import {
|
|
62
|
+
import { effectful, forgetHelperPlacement, orderSensitive, placedAt, speculationUnsafe } from '../ir/discipline';
|
|
63
|
+
import { NEGATED_FCMP, NEGATED_ICMP } from '../ir/opcodes';
|
|
62
64
|
import { T } from '../ir/types';
|
|
63
65
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
64
66
|
|
|
65
|
-
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']);
|
|
66
68
|
|
|
67
69
|
/** Run `step` until it stops rewriting, and answer whether it ever did.
|
|
68
70
|
*
|
|
@@ -226,8 +228,11 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
226
228
|
// value ops (arith, loads, icmp) are safe: the structurer inlines them back into the `&&`/`||` RHS
|
|
227
229
|
// expression, where C's own short-circuit re-guards them. Any side effect ⇒ DECLINE the fold — the
|
|
228
230
|
// merge-variable spelling the fall-through leaves is correct (the side effect stays in B's block),
|
|
229
|
-
// just possibly non-matching.
|
|
230
|
-
|
|
231
|
+
// just possibly non-matching. A qualified read is an access, not a value, and stays in its
|
|
232
|
+
// arm the same way. structure/analysis.ts rests on this refusal: it names a counted op it
|
|
233
|
+
// finds in a connective's guarded operand at its def, above the branch, because no fold
|
|
234
|
+
// put one there.
|
|
235
|
+
if (bfeed.ops.slice(0, -1).some(speculationUnsafe)) {
|
|
231
236
|
continue;
|
|
232
237
|
}
|
|
233
238
|
// NEGATABLE only when the orientation actually inverts the head — asked here rather than up
|
|
@@ -244,7 +249,9 @@ export function recognizeShortCircuit(fn: Fn): boolean {
|
|
|
244
249
|
if (wantNeg && !negation) {
|
|
245
250
|
continue;
|
|
246
251
|
}
|
|
247
|
-
|
|
252
|
+
// hoist B's pure body (defines Vb; harmless if a dead const). A helper op it moves no longer
|
|
253
|
+
// sits where the asm called it (ir/discipline.ts `forgetHelperPlacement`).
|
|
254
|
+
bfeed.ops.slice(0, -1).forEach((op) => before(forgetHelperPlacement(op)));
|
|
248
255
|
foldWriteOrder(fn.writeOrder, bfeed, h); // …and its writes now follow H's (ir/core.ts)
|
|
249
256
|
let condSide = cond;
|
|
250
257
|
if (negation) {
|
|
@@ -577,10 +584,11 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
577
584
|
continue;
|
|
578
585
|
}
|
|
579
586
|
// ^g's body must be pure — see the REFUSALS note.
|
|
580
|
-
//
|
|
587
|
+
// `speculationUnsafe` answers yes for `opaque`: an instruction asmlift could not model, and moving it
|
|
581
588
|
// out of the arm that guards it is the reordering this refuses. Loud either way today — a
|
|
582
589
|
// decline under `onGap: 'strict'`, an ASMLIFT_ERROR marker under `annotate`.
|
|
583
|
-
|
|
590
|
+
// A qualified read stays in its arm too, as in the value form above, and for the same reader.
|
|
591
|
+
if (g.ops.slice(0, -1).some(speculationUnsafe)) {
|
|
584
592
|
continue;
|
|
585
593
|
}
|
|
586
594
|
// Which of ^g's edges rejoins ^h's other successor? That is the shared block. A DIRECT edge
|
|
@@ -648,7 +656,7 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
|
|
|
648
656
|
}
|
|
649
657
|
}
|
|
650
658
|
rereadInArm(fn, g, otherEdge.block, reread);
|
|
651
|
-
const body = g.ops.slice(0, -1);
|
|
659
|
+
const body = g.ops.slice(0, -1).map(forgetHelperPlacement);
|
|
652
660
|
const second = negation ? negation.result : c2;
|
|
653
661
|
const negated: Op[] = negation ? negation.ops : [];
|
|
654
662
|
const res = mkValue(T.unk(32));
|
|
@@ -798,7 +806,9 @@ const NEGATE_BUDGET = 8;
|
|
|
798
806
|
/** The ops computing `!v`, or `null` when `v` cannot be negated.
|
|
799
807
|
*
|
|
800
808
|
* Two cases, and no third:
|
|
801
|
-
* - an `icmp_*` in `NEGATED_ICMP` → the swapped-opcode
|
|
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.
|
|
802
812
|
* - a `logic_and`/`logic_or` → the DUAL connective over its two recursively negated operands.
|
|
803
813
|
* De Morgan: `!(a || b)` is `!a && !b`. This is the only reason the helper is recursive, and
|
|
804
814
|
* it is what lets a chain fold past its first level (`a || !(b || c)`).
|
|
@@ -816,8 +826,9 @@ const NEGATE_BUDGET = 8;
|
|
|
816
826
|
* - the cone exceeds `NEGATE_BUDGET` minted ops.
|
|
817
827
|
*
|
|
818
828
|
* Ops are MINTED, never mutated: the originals stay where the caller hoisted them and die to the
|
|
819
|
-
* pass list's own `dce: true` (raise/pre-recovery.ts). Only `icmp_*`/`logic_and
|
|
820
|
-
* ever rebuilt, all of them pure, so no effect can be duplicated or reordered by
|
|
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.
|
|
821
832
|
*
|
|
822
833
|
* Nothing is SHARED between paths, and that is the point rather than a limitation. Memoizing `go`
|
|
823
834
|
* in a `Map<Value, Value>` is three lines and would make the budget unnecessary — and it would
|
|
@@ -826,11 +837,11 @@ const NEGATE_BUDGET = 8;
|
|
|
826
837
|
* Nothing collapses the duplicates later either — `numberPureValues` runs as `addrnum`, far ahead
|
|
827
838
|
* of both folds. So the per-path rebuild is the mechanism and NEGATE_BUDGET is its price.
|
|
828
839
|
*
|
|
829
|
-
* What it GUARANTEES about its result, which one caller leans on: every op it mints is an `icmp_
|
|
830
|
-
* or a `logic_and`/`logic_or`, so the returned value is always a C boolean. The value
|
|
831
|
-
* un-negated head straight on as the merge VALUE, which is why that caller checks
|
|
832
|
-
* itself before deciding whether to call here at all (see its head gate); the
|
|
833
|
-
* 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`.
|
|
834
845
|
*
|
|
835
846
|
* PRECONDITION, earned by the caller and not checked here: every value in the cone must dominate
|
|
836
847
|
* the point the caller splices `ops` into — the minted comparisons reuse the ORIGINAL leaf
|
|
@@ -853,8 +864,18 @@ function negateCondOps(defs: Map<Value, Op>, v: Value, budget: number): { ops: O
|
|
|
853
864
|
return null;
|
|
854
865
|
}
|
|
855
866
|
const out = mkValue(T.unk(32));
|
|
856
|
-
|
|
857
|
-
|
|
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 }));
|
|
858
879
|
return out;
|
|
859
880
|
}
|
|
860
881
|
if (d.opcode === 'logic_and' || d.opcode === 'logic_or') {
|
|
@@ -938,7 +959,7 @@ const readsOf = (op: Op): Value[] => [...op.operands, ...op.successors.flatMap((
|
|
|
938
959
|
* gate in `opts.armReread` (default {@link ARM_REREAD_GATES}) refuses the site.
|
|
939
960
|
*
|
|
940
961
|
* The copy is exact at the arm's head because nothing runs between: `g`'s body is pure (its
|
|
941
|
-
* caller refuses anything
|
|
962
|
+
* caller refuses anything `speculationUnsafe`), so a load copied there reads what `g`'s own load
|
|
942
963
|
* read, and `g` is the arm's ONLY predecessor, so the copy runs exactly when the original's
|
|
943
964
|
* value would have been live there. On verified IR that also places every escaping read under
|
|
944
965
|
* the arm: `g` dominates nothing else, because the shared block is reached from ^h directly.
|
|
@@ -1086,17 +1107,20 @@ function leavesALoop(fn: Fn, g: Block, arm: Block, preds: ReadonlyMap<Block, rea
|
|
|
1086
1107
|
* - it renders nowhere single: some PURE value between it and a statement has two consumers, so
|
|
1087
1108
|
* `emitPos` is null and analysis.ts materializes it whatever stands in the way
|
|
1088
1109
|
* (`p[2] = p[1] & 0x80; p[3] = p[1] & 0x80;` — one `and`, two stores);
|
|
1089
|
-
* - it has MORE THAN ONE direct reader, and an effect
|
|
1090
|
-
* the multi-render rule,
|
|
1091
|
-
* - it has ONE, and an
|
|
1092
|
-
*
|
|
1093
|
-
* and a `store` UNLESS it is to a provably disjoint slot of the read's own
|
|
1094
|
-
* (`disjointConstSlots`, ir/alias.ts — the helper `isBarrier` calls).
|
|
1110
|
+
* - it has MORE THAN ONE direct reader, and an effect or a helper op analysis.ts names precedes
|
|
1111
|
+
* one of their render positions — the multi-render rule, which exempts nothing;
|
|
1112
|
+
* - it has ONE, and an op precedes its render position that analysis.ts's `isBarrier` counts:
|
|
1113
|
+
* any `astore`/`opaque`, a helper op analysis.ts names, a `call` that is not rendered inside
|
|
1114
|
+
* that same statement, and a `store` UNLESS it is to a provably disjoint slot of the read's own
|
|
1115
|
+
* base (`disjointConstSlots`, ir/alias.ts — the helper `isBarrier` calls).
|
|
1116
|
+
*
|
|
1117
|
+
* `isBarrier`'s pinned-read clause has nothing to mirror: ^g holds no pinned read, since the fold
|
|
1118
|
+
* refuses a ^g with anything `speculationUnsafe` in it.
|
|
1095
1119
|
*
|
|
1096
1120
|
* A reader in any block other than the arm answers yes — the copy then outlives the arm's first
|
|
1097
1121
|
* block. What this does NOT mirror, and which way each gap errs, is ARM_REREAD_GATES' RESIDUE. */
|
|
1098
1122
|
function readHeldAcrossEffect(c: ArmRereadSite): boolean {
|
|
1099
|
-
const reads = [...c.copy].filter(
|
|
1123
|
+
const reads = [...c.copy].filter(orderSensitive);
|
|
1100
1124
|
if (reads.length === 0) {
|
|
1101
1125
|
return false;
|
|
1102
1126
|
}
|
|
@@ -1127,8 +1151,17 @@ function readHeldAcrossEffect(c: ArmRereadSite): boolean {
|
|
|
1127
1151
|
if (cs.length === 1) {
|
|
1128
1152
|
return at(cs[0]);
|
|
1129
1153
|
}
|
|
1130
|
-
return
|
|
1154
|
+
return orderSensitive(op) ? posOf(op) : null;
|
|
1131
1155
|
};
|
|
1156
|
+
// analysis.ts's `namedHelper`: a helper op it names where the asm ran it, which bars a read as a
|
|
1157
|
+
// named call does. It names one whose value renders in another block, or past an effect or
|
|
1158
|
+
// another named helper.
|
|
1159
|
+
const namedHelper = (x: Op): boolean =>
|
|
1160
|
+
placedAt(x) === 'helper' &&
|
|
1161
|
+
(escapes(x.results) ||
|
|
1162
|
+
consumersOf(x.results)
|
|
1163
|
+
.map(at)
|
|
1164
|
+
.some((p) => p === null || c.arm.ops.slice(posOf(x) + 1, p).some((y) => effectful(y) || namedHelper(y))));
|
|
1132
1165
|
for (const l of reads) {
|
|
1133
1166
|
if (escapes(l.results)) {
|
|
1134
1167
|
return true;
|
|
@@ -1139,7 +1172,10 @@ function readHeldAcrossEffect(c: ArmRereadSite): boolean {
|
|
|
1139
1172
|
}
|
|
1140
1173
|
const multi = renders.length > 1;
|
|
1141
1174
|
const bars = (x: Op, pos: number): boolean => {
|
|
1142
|
-
if (
|
|
1175
|
+
if (namedHelper(x)) {
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
if (!effectful(x)) {
|
|
1143
1179
|
return false;
|
|
1144
1180
|
}
|
|
1145
1181
|
if (multi) {
|
|
@@ -1280,7 +1316,7 @@ export const ARM_REREAD_GATES: readonly Gate<ArmRereadSite>[] = [
|
|
|
1280
1316
|
why: 'the target read it before the second test on both of its exits; moved, it runs on one',
|
|
1281
1317
|
sound: false,
|
|
1282
1318
|
guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a READ only the arm consumes would move under the second test',
|
|
1283
|
-
rejects: (c) => [...c.drop].some(
|
|
1319
|
+
rejects: (c) => [...c.drop].some(orderSensitive),
|
|
1284
1320
|
},
|
|
1285
1321
|
{
|
|
1286
1322
|
// THE BYTES HALF of the copy (see `armRereadCone`), and a claim about ONE compiler. The arm of
|
|
@@ -1323,7 +1359,7 @@ export const ARM_REREAD_GATES: readonly Gate<ArmRereadSite>[] = [
|
|
|
1323
1359
|
|
|
1324
1360
|
/** Copy `copy` to the head of `arm`, point every read outside `g` at the copies, and delete `drop`.
|
|
1325
1361
|
* A dropped original is deleted rather than left for the hoist: the arm now performs it, and a
|
|
1326
|
-
* dead READ left in ^h is one the structurer may still spell (
|
|
1362
|
+
* dead READ left in ^h is one the structurer may still spell (ir/discipline.ts `spelledWhenDead`). */
|
|
1327
1363
|
function rereadInArm(fn: Fn, g: Block, arm: Block, { copy, drop }: ArmReread): void {
|
|
1328
1364
|
const copyOf = new Map<Value, Value>();
|
|
1329
1365
|
const copies = g.ops
|
package/src/raise/softdiv.ts
CHANGED
|
@@ -15,27 +15,27 @@
|
|
|
15
15
|
// cannot state: its match keys on a `call`'s STRING `target` attr, which the numeric `attrEquals`
|
|
16
16
|
// cannot express. It is naturally inert on hardware-divide targets (which emit `div`/`divu`,
|
|
17
17
|
// never `bl __divsi3`).
|
|
18
|
-
import { Fn
|
|
19
|
-
import
|
|
20
|
-
import type
|
|
18
|
+
import type { Fn } from '../ir/core';
|
|
19
|
+
import { wordsOf } from '../proto';
|
|
20
|
+
import { type RuntimeHelper, helperOp, isFloatHelper, isWideHelper } from '../runtime-helpers';
|
|
21
|
+
import type { TargetDescription } from '../target';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Object.entries(SOFT_DIV).map(([sym, h]) => [sym, { params: h.params }]),
|
|
34
|
-
);
|
|
23
|
+
/** The 32-bit software divisions, off the target's own helper table. WHICH helpers a compiler emits
|
|
24
|
+
* is a compiler fact and lives there (runtime-helpers.ts); which of them THIS pass answers for is
|
|
25
|
+
* the question here, and it is the narrow one: a division the ISA has no instruction for. A helper
|
|
26
|
+
* that computes on a value wider than a register is a different question — no hardware capability
|
|
27
|
+
* can make one unnecessary — and `raise/widehelpers.ts` answers it, ungated. So is a float helper
|
|
28
|
+
* of any width, which computes on floats the ISA has no instruction for either, and
|
|
29
|
+
* `raise/floathelpers.ts` answers it. */
|
|
30
|
+
const softDivisions = (target: TargetDescription): Record<string, RuntimeHelper> =>
|
|
31
|
+
Object.fromEntries(
|
|
32
|
+
Object.entries(target.runtimeHelpers ?? {}).filter(([, h]) => h.op && !isWideHelper(h) && !isFloatHelper(h)),
|
|
33
|
+
);
|
|
35
34
|
|
|
36
35
|
/** Rewrite each recognised soft-division helper call to its division op, in place. Returns whether
|
|
37
36
|
* anything changed. Runs BEFORE type recovery so the new op's operands get signed/unsigned typing. */
|
|
38
|
-
export function recognizeSoftDiv(fn: Fn): boolean {
|
|
37
|
+
export function recognizeSoftDiv(fn: Fn, target: TargetDescription): boolean {
|
|
38
|
+
const table = softDivisions(target);
|
|
39
39
|
let changed = false;
|
|
40
40
|
for (const b of fn.blocks) {
|
|
41
41
|
for (let i = 0; i < b.ops.length; i++) {
|
|
@@ -43,18 +43,16 @@ export function recognizeSoftDiv(fn: Fn): boolean {
|
|
|
43
43
|
if (op.opcode !== 'call') {
|
|
44
44
|
continue;
|
|
45
45
|
}
|
|
46
|
-
const helper =
|
|
47
|
-
if (!helper) {
|
|
46
|
+
const helper = table[op.attrs.target as string];
|
|
47
|
+
if (!helper?.op) {
|
|
48
48
|
continue;
|
|
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 !== 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
|
-
|
|
56
|
-
const div = mkOp(helper.op, { operands: [...op.operands], results: [op.results[0]] });
|
|
57
|
-
b.ops.splice(i, 1, div);
|
|
55
|
+
b.ops.splice(i, 1, helperOp(helper.op, op.attrs.target as string, op.operands, op.results[0]));
|
|
58
56
|
changed = true;
|
|
59
57
|
}
|
|
60
58
|
}
|
|
@@ -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
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// UNAMBIGUOUS here (the scaled operand is the index, read from the machine code) — the unscaled
|
|
19
19
|
// `add(x, y)` byte form stays out of scope (genuinely ambiguous without types).
|
|
20
20
|
import { Fn, Op, Value, defOpMap, mkOp } from '../ir/core';
|
|
21
|
+
import { carryDiscipline } from '../ir/discipline';
|
|
21
22
|
import { nextStructIndex } from '../ir/struct-names';
|
|
22
23
|
import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
23
24
|
import { collectStructs } from './structs';
|
|
@@ -92,8 +93,16 @@ function withPadding(dataFields: StructField[], stride: number): StructField[] {
|
|
|
92
93
|
* pokeemerald:GetGenderFromSpeciesAndPersonality, whose address is materialized twice). A base
|
|
93
94
|
* whose element pointers
|
|
94
95
|
* disagree on stride declines entirely: two strides over one base is a reinterpreted view or
|
|
95
|
-
* a 2D layout, genuinely ambiguous — decline over guess.
|
|
96
|
-
|
|
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 {
|
|
97
106
|
const defs = defOpMap(fn);
|
|
98
107
|
let count = 0;
|
|
99
108
|
// The NAME index is not the recognition count: `count` is this function's return value — how
|
|
@@ -102,7 +111,7 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
102
111
|
// time, because this pass runs once per function and nothing else mints the prefix, and
|
|
103
112
|
// ir/struct-names.ts states why the computed 0 is kept over the assumed one.
|
|
104
113
|
let name = nextStructIndex(
|
|
105
|
-
[...collectStructs(fn)].map((s) => s.name),
|
|
114
|
+
[...collectStructs(fn, true)].map((s) => s.name),
|
|
106
115
|
'Elem',
|
|
107
116
|
);
|
|
108
117
|
|
|
@@ -147,7 +156,7 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
147
156
|
// load/store BASE with a field offset inside one element.
|
|
148
157
|
const elemSet = new Set(elems.map((e) => e.add.results[0]));
|
|
149
158
|
const indexOf = new Map(elems.map((e) => [e.add.results[0], e.index]));
|
|
150
|
-
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 }[] = [];
|
|
151
160
|
for (const bb of fn.blocks) {
|
|
152
161
|
for (const op of bb.ops) {
|
|
153
162
|
const isMem = (op.opcode === 'load' || op.opcode === 'store') && elemSet.has(op.operands[0]);
|
|
@@ -169,6 +178,7 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
169
178
|
off,
|
|
170
179
|
width,
|
|
171
180
|
signed: op.opcode === 'load' ? (op.attrs.signed as boolean) : width === 4,
|
|
181
|
+
stamp: op.opcode === 'load' ? undefined : (op.attrs.signed as boolean | undefined),
|
|
172
182
|
});
|
|
173
183
|
}
|
|
174
184
|
}
|
|
@@ -192,13 +202,14 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
192
202
|
// silently drops one side's zero/sign-extension (adversarially learned: `arr[i].f +
|
|
193
203
|
// (u16)arr[i].f` lost its zext and a byte-exact match with it). A STORE's `signed` is the
|
|
194
204
|
// width===4 CONVENTION, not a machine fact, so it never conflicts — the field takes its
|
|
195
|
-
// signedness from the loads when any exist.
|
|
196
|
-
|
|
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 }>();
|
|
197
208
|
for (const a of accesses) {
|
|
198
209
|
const isLoad = a.op.opcode === 'load';
|
|
199
210
|
const prev = byOff.get(a.off);
|
|
200
211
|
if (!prev) {
|
|
201
|
-
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 });
|
|
202
213
|
} else if (prev.width !== a.width) {
|
|
203
214
|
clean = false;
|
|
204
215
|
} else if (isLoad) {
|
|
@@ -207,6 +218,8 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
207
218
|
} else if (prev.loadSigned !== a.signed) {
|
|
208
219
|
clean = false;
|
|
209
220
|
}
|
|
221
|
+
} else if (a.stamp !== undefined) {
|
|
222
|
+
prev.stamped = prev.stamped === true || a.stamp;
|
|
210
223
|
}
|
|
211
224
|
}
|
|
212
225
|
const offs = [...byOff.entries()].sort(([x], [y]) => x - y);
|
|
@@ -221,16 +234,19 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
221
234
|
}
|
|
222
235
|
maxAlign = Math.max(maxAlign, width);
|
|
223
236
|
}
|
|
224
|
-
|
|
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) {
|
|
225
241
|
clean = false;
|
|
226
242
|
}
|
|
227
243
|
if (!clean) {
|
|
228
244
|
continue;
|
|
229
245
|
}
|
|
230
|
-
const dataFields: StructField[] = offs.map(([off, { width, loadSigned }]) => ({
|
|
246
|
+
const dataFields: StructField[] = offs.map(([off, { width, loadSigned, stamped }]) => ({
|
|
231
247
|
off,
|
|
232
|
-
// store-only field:
|
|
233
|
-
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),
|
|
234
250
|
name: `field_${off}`,
|
|
235
251
|
}));
|
|
236
252
|
const elemStruct = T.struct(`Elem${name++}`, withPadding(dataFields, stride), stride);
|
|
@@ -254,17 +270,21 @@ export function recognizeStructArrays(fn: Fn): number {
|
|
|
254
270
|
operands: [base, index],
|
|
255
271
|
results: [op.results[0]],
|
|
256
272
|
// listOrder rides along: an ldmia-expanded load keeps its stream-order caveat as an aload
|
|
257
|
-
attrs: {
|
|
273
|
+
attrs: carryDiscipline(op, {
|
|
258
274
|
elemSize: stride,
|
|
259
275
|
signed: op.attrs.signed as boolean,
|
|
260
276
|
fieldOff: op.attrs.off as number,
|
|
261
277
|
...(op.attrs.listOrder === true && { listOrder: true }),
|
|
262
|
-
},
|
|
278
|
+
}),
|
|
263
279
|
});
|
|
264
280
|
} else if (op.opcode === 'store') {
|
|
265
281
|
bb.ops[i] = mkOp('astore', {
|
|
266
282
|
operands: [base, index, op.operands[1]],
|
|
267
|
-
attrs:
|
|
283
|
+
attrs: carryDiscipline(op, {
|
|
284
|
+
elemSize: stride,
|
|
285
|
+
fieldOff: op.attrs.off as number,
|
|
286
|
+
...(op.attrs.signed !== undefined && { signed: op.attrs.signed }),
|
|
287
|
+
}),
|
|
268
288
|
});
|
|
269
289
|
}
|
|
270
290
|
}
|