@asmlift/core 0.8.0 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- 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 +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +276 -80
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
|
@@ -29,21 +29,37 @@
|
|
|
29
29
|
// coalescing (canTakeName + seeding) and emission (structureBlock + the loop emitters) — they
|
|
30
30
|
// share varName/backArgName mutation and the activeSub/loopCtx dynamic state.
|
|
31
31
|
//
|
|
32
|
-
// Scope: reducible
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
32
|
+
// Scope: reducible natural loops — GUARDED self-loop `while` (the guard-fusion un-rotation),
|
|
33
|
+
// UNGUARDED self-loop `do-while` (single block, header === latch), test-at-top `while`,
|
|
34
|
+
// bottom-test `do-while`, `while (1)` for any other latch set (a conditional `continue`, a
|
|
35
|
+
// mid-tested header), PROPERLY-nested loops, in-body `break`/early-`return`, comparison-tree and
|
|
36
|
+
// jump-table `switch`, and a switch arm that FALLS THROUGH into the next one (both regimes — see
|
|
37
|
+
// `ArmExit` in switch-recover.ts). Still DECLINED (loud StructureError, never wrong code):
|
|
38
|
+
// irreducible/overlapping loops, exits to more than one live merge, a `break` out of a nested loop's
|
|
39
|
+
// body, a `break` whose exit copies would clobber, and mixed-entry self-loops (a guarded header also
|
|
40
|
+
// entered by a plain br).
|
|
41
|
+
// Fall-through carries two REFUSALS of its own rather than a decline: a target language whose
|
|
42
|
+
// `case` cannot fall through (`spellSwitchFallthrough` false) sends Regime A back to if-recovery,
|
|
43
|
+
// and arms that do not linearize into one chain — two arms falling into the same sibling, or a fall
|
|
44
|
+
// into the `default:` — refuse in `chainArms`, which answers null.
|
|
45
|
+
import { constAddressOf, globalBaseOf } from '../ir/alias';
|
|
46
|
+
import {
|
|
47
|
+
Block,
|
|
48
|
+
Fn,
|
|
49
|
+
Op,
|
|
50
|
+
Successor,
|
|
51
|
+
Value,
|
|
52
|
+
callArgs,
|
|
53
|
+
calleeName,
|
|
54
|
+
calleeValue,
|
|
55
|
+
defOpMap,
|
|
56
|
+
dominators,
|
|
57
|
+
mergeClasses,
|
|
58
|
+
successorsOf,
|
|
59
|
+
} from '../ir/core';
|
|
60
|
+
import { effectful, placedAt, qualified, qualifiedBy, spelledWhenDead } from '../ir/discipline';
|
|
61
|
+
import { CAST_WIDTHS, opSig } from '../ir/opcodes';
|
|
62
|
+
import { type IrType, T, intWidth, scalarTypeForAccess, typeEquals, unionViewAt } from '../ir/types';
|
|
47
63
|
import {
|
|
48
64
|
BinOp,
|
|
49
65
|
Expr,
|
|
@@ -53,15 +69,21 @@ import {
|
|
|
53
69
|
exprChildren,
|
|
54
70
|
exprHasEffect,
|
|
55
71
|
gapReasonFor,
|
|
72
|
+
isLoop,
|
|
56
73
|
mapExprChildren,
|
|
57
74
|
mapStmtExprs,
|
|
75
|
+
mentionedName,
|
|
58
76
|
negateCond,
|
|
59
77
|
stmtChildren,
|
|
78
|
+
stmtExprs,
|
|
79
|
+
stmtsEqual,
|
|
60
80
|
walkExprs,
|
|
61
81
|
} from '../l3/ast';
|
|
62
82
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
63
|
-
import { exprCType, provablyNonNegative, ptrElemBytes, renderedIntSignedness } from '../l3/typing';
|
|
83
|
+
import { exprCType, exprIntWidth, provablyNonNegative, ptrElemBytes, renderedIntSignedness } from '../l3/typing';
|
|
84
|
+
import { castOffDeclaration, qualifiedMemoryAccess } from '../l3/volstore';
|
|
64
85
|
import { foldConstPair, isConstFoldOpcode } from '../raise/const';
|
|
86
|
+
import { stampDeclaredVolatile } from '../raise/declared-volatile';
|
|
65
87
|
import { returnType } from '../raise/recover';
|
|
66
88
|
import { collectStructs } from '../raise/structs';
|
|
67
89
|
import {
|
|
@@ -78,7 +100,7 @@ import {
|
|
|
78
100
|
scalarCellType,
|
|
79
101
|
structFieldInnerExtents,
|
|
80
102
|
} from '../symbols';
|
|
81
|
-
import { analyze } from './analysis';
|
|
103
|
+
import { analyze, shortCircuitGuardedValues } from './analysis';
|
|
82
104
|
import { makeBitfieldSpelling } from './bitfields';
|
|
83
105
|
import {
|
|
84
106
|
addOffset,
|
|
@@ -90,11 +112,20 @@ import {
|
|
|
90
112
|
globalOf,
|
|
91
113
|
subscriptsFromExtents,
|
|
92
114
|
} from './globalaccess';
|
|
93
|
-
import {
|
|
115
|
+
import {
|
|
116
|
+
PREUPDATE_SINK_GATES,
|
|
117
|
+
type SinkCandidate,
|
|
118
|
+
makeLoopHazards,
|
|
119
|
+
preUpdateCopyHome,
|
|
120
|
+
sunkCopyOverDroppedUndef,
|
|
121
|
+
updateWriteSet,
|
|
122
|
+
} from './hazards';
|
|
123
|
+
import { type StaticLayout, localStaticShapes, nameLocalStatics } from './local-statics';
|
|
94
124
|
import { type NaturalLoop, analyzeLoops } from './loops';
|
|
95
125
|
import { type NameMerge, coalesceNames } from './namecoalesce';
|
|
126
|
+
import { testRereadsOnly } from './redundant-test';
|
|
96
127
|
import { unspelledEpilogues } from './retspell';
|
|
97
|
-
import { type ArmExit, makeSwitchRecovery } from './switch-recover';
|
|
128
|
+
import { type ArmExit, type SwitchBoundCase, makeSwitchRecovery } from './switch-recover';
|
|
98
129
|
|
|
99
130
|
// Lower a constant-offset memory access to its lvalue/rvalue Expr. If the base was recovered as a
|
|
100
131
|
// struct pointer (raise/structs.ts), the byte offset resolves to a NAMED field (`base->field_<off>`);
|
|
@@ -229,6 +260,22 @@ function spellsAccessType(declared: boolean | undefined, width: number, signed:
|
|
|
229
260
|
return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
|
|
230
261
|
}
|
|
231
262
|
|
|
263
|
+
/** The declared type of a frame-local object, from the three facts its `laddr` carries: `count`
|
|
264
|
+
* elements of `width` bytes, signed or not. ONE element is a SCALAR typed by the access every
|
|
265
|
+
* access agreed on. More is STORAGE the frontend's frame-object audit sized without typing —
|
|
266
|
+
* no access pins an element type there, so the audit stamps bytes and this declares the array
|
|
267
|
+
* that spells them. Both arms are the audit's own facts; neither infers anything here. */
|
|
268
|
+
function laddrType(op: Op): IrType {
|
|
269
|
+
// a call's struct-return storage: the audit typed its address as a pointer to that struct
|
|
270
|
+
const t = op.results[0].type;
|
|
271
|
+
if (op.attrs.aggregate === true && t.kind === 'ptr') {
|
|
272
|
+
return t.to;
|
|
273
|
+
}
|
|
274
|
+
const elem = T.int((op.attrs.width as number) * 8, op.attrs.signed as boolean);
|
|
275
|
+
const count = op.attrs.count as number;
|
|
276
|
+
return count === 1 ? elem : T.array(elem, count);
|
|
277
|
+
}
|
|
278
|
+
|
|
232
279
|
/** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
|
|
233
280
|
* The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
|
|
234
281
|
* qualifier the name reintroduces changes what the compiler emits:
|
|
@@ -636,6 +683,23 @@ function globalConstByte(baseExpr: Expr, off: number): { name: string; byte: num
|
|
|
636
683
|
return null;
|
|
637
684
|
}
|
|
638
685
|
|
|
686
|
+
/** The whole-element subscript a constant byte displacement denotes at a given element width — the
|
|
687
|
+
* `idxVal + off / width` fold every indexed spelling below shares, with the rule
|
|
688
|
+
* structure/globalaccess.ts `elementIndex` applies to a VARIABLE residual applied to the constant
|
|
689
|
+
* one. A displacement that is not a whole number of elements has no subscript spelling at all
|
|
690
|
+
* (`[0.5]` is not C, and agbcc answers `array subscript is not an integer`), so it declines LOUD
|
|
691
|
+
* here, where the fraction would be computed, rather than leaving the layer above to have refused
|
|
692
|
+
* every base that can reach it. */
|
|
693
|
+
function displacementIndex(off: number, width: number): number {
|
|
694
|
+
if (off % width !== 0) {
|
|
695
|
+
throw new StructureError(
|
|
696
|
+
`a ${width}-byte access at byte ${off} of its base is not a whole number of elements from it — ` +
|
|
697
|
+
`no subscript spells it`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return off / width;
|
|
701
|
+
}
|
|
702
|
+
|
|
639
703
|
function memAccess(
|
|
640
704
|
base: Value,
|
|
641
705
|
baseExpr: Expr,
|
|
@@ -742,7 +806,7 @@ function memAccess(
|
|
|
742
806
|
return {
|
|
743
807
|
k: 'index',
|
|
744
808
|
base: { k: 'var', name: gbb!.name },
|
|
745
|
-
idx: addOffset(multi.idx, off
|
|
809
|
+
idx: addOffset(multi.idx, displacementIndex(off, width)),
|
|
746
810
|
width,
|
|
747
811
|
signed,
|
|
748
812
|
lead: multi.lead,
|
|
@@ -761,7 +825,7 @@ function memAccess(
|
|
|
761
825
|
if (off === 0 && idxVal.k === 'const' && idxVal.value === 0 && scalarGlobals.has(g.name)) {
|
|
762
826
|
return { k: 'var', name: g.name };
|
|
763
827
|
}
|
|
764
|
-
const idx = addOffset(idxVal, off
|
|
828
|
+
const idx = addOffset(idxVal, displacementIndex(off, width));
|
|
765
829
|
// ARRAY-declared global (symbol map): index the bare name — `gSym[i]`, the spelling the
|
|
766
830
|
// dogfood proved agbcc needs for ROM tables — with the element type registered in the env
|
|
767
831
|
// so the stride check passes and no cast is added. Element-width match only.
|
|
@@ -781,9 +845,33 @@ function memAccess(
|
|
|
781
845
|
// the array-element form — wrong for a pointer). Anything else is cast to the recovered
|
|
782
846
|
// struct pointer type; the cast node prints with `->`.
|
|
783
847
|
const ok = rt?.kind === 'ptr' && rt.to.kind === 'struct' && rt.to.name === bt.to.name && baseExpr.k !== 'index';
|
|
784
|
-
|
|
848
|
+
const structBase = ok ? baseExpr : { k: 'cast' as const, to: bt, e: baseExpr };
|
|
849
|
+
// A byte range recovered as a UNION member (raise/structs.ts) is read through the view of the
|
|
850
|
+
// access's own width and extension: `p->field_0.word`, `p->field_0.half[1]`, `p->field_0.shalf`.
|
|
851
|
+
const u = unionViewAt(bt.to, off, width, signed, isStore);
|
|
852
|
+
if (u === undefined) {
|
|
853
|
+
return { k: 'field', base: structBase, name: `field_${off}` };
|
|
854
|
+
}
|
|
855
|
+
// An internal invariant: the builder made a view for every width and narrow extension a union's
|
|
856
|
+
// own base accessed, and a value that inherits the type through recoverTypes carries a pointee
|
|
857
|
+
// of its own instead.
|
|
858
|
+
if (u.view === undefined) {
|
|
859
|
+
throw new StructureError(
|
|
860
|
+
`a ${width}-byte ${isStore ? 'store' : signed ? 'signed load' : 'unsigned load'} at byte ${off} has no view in the union member '${u.member.name}'`,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const cell: Expr = { k: 'field', base: structBase, name: u.member.name };
|
|
864
|
+
const view: Expr = { k: 'field', base: cell, name: u.view.name, dot: true };
|
|
865
|
+
return u.index === null ? view : { k: 'index', base: view, idx: { k: 'const', value: u.index }, width, signed };
|
|
785
866
|
}
|
|
786
|
-
return {
|
|
867
|
+
return {
|
|
868
|
+
k: 'index',
|
|
869
|
+
base: baseExpr,
|
|
870
|
+
idx: { k: 'const', value: displacementIndex(off, width) },
|
|
871
|
+
width,
|
|
872
|
+
signed,
|
|
873
|
+
...addressEvidence,
|
|
874
|
+
};
|
|
787
875
|
}
|
|
788
876
|
|
|
789
877
|
// A variable-index array access `base[index]`; `base[index].field_K` when a `fieldOff` marks an
|
|
@@ -932,7 +1020,7 @@ const CMP_TO_BIN: Record<string, BinOp> = {
|
|
|
932
1020
|
icmp_eq: '==',
|
|
933
1021
|
icmp_ne: '!=',
|
|
934
1022
|
};
|
|
935
|
-
const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
1023
|
+
export const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
936
1024
|
add: '+',
|
|
937
1025
|
sub: '-',
|
|
938
1026
|
mul: '*',
|
|
@@ -952,10 +1040,48 @@ const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
|
952
1040
|
logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
|
|
953
1041
|
};
|
|
954
1042
|
|
|
1043
|
+
// The float ops (ir/opcodes.ts) and their L3 operators (l3/ast.ts BinOp). A table of their own and
|
|
1044
|
+
// not entries in ARITH_TO_BIN, because every rule that table's consumers apply — the pointer
|
|
1045
|
+
// stride, the integer legalizations, the constant fold, the commutative re-spelling — is an
|
|
1046
|
+
// integer rule.
|
|
1047
|
+
const FLOAT_TO_BIN: Record<string, BinOp> = { fadd: 'f+', fsub: 'f-', fmul: 'f*', fdiv: 'f/' };
|
|
1048
|
+
|
|
955
1049
|
// The operators whose operand order the machine does not fix — candidates for the def-order
|
|
956
1050
|
// re-spelling in lowerDef. `&&`/`||` are excluded: short-circuit order IS semantics.
|
|
957
1051
|
const COMMUTATIVE_BIN: ReadonlySet<BinOp> = new Set(['+', '*', '&', '|', '^']);
|
|
958
1052
|
|
|
1053
|
+
// Spell a loop update at the place the bottom test reads the variable: the one `name` leaf becomes
|
|
1054
|
+
// `name++`. Returns null unless the rendered test MENTIONS `name` exactly once, and that mention is
|
|
1055
|
+
// a readable leaf. `&v1` is a mention the `++` cannot be placed on and cannot be counted out of —
|
|
1056
|
+
// a callee reads the object through it — so a test holding one alongside a bare `v1` is refused
|
|
1057
|
+
// here rather than left to `assertPostIncrUnshared`, which would abort the lift instead.
|
|
1058
|
+
//
|
|
1059
|
+
// `preUpdateCondFold` decided that on the DEF TREE, and this asks the same question of what the
|
|
1060
|
+
// lowering actually produced. The two can disagree — an idiom fold spells a value its operands do
|
|
1061
|
+
// not show, a substitution stops the render short of a leaf the walk reached — and the two
|
|
1062
|
+
// disagreements are not equally bad: a test that named the variable twice would be C89-undefined,
|
|
1063
|
+
// and one that names it not at all would drop the update altogether. Both answer null, and the
|
|
1064
|
+
// caller declines.
|
|
1065
|
+
//
|
|
1066
|
+
// No input reaches either answer today, so it is pinned directly, on hand-built expressions
|
|
1067
|
+
// (test/loop-preupdate-cond.test.ts) — the discipline `sunkCopyOverDroppedUndef` is held to.
|
|
1068
|
+
export function spellUpdateInCond(cond: Expr, name: string, by: 1 | -1): Expr | null {
|
|
1069
|
+
let found = 0;
|
|
1070
|
+
let mentions = 0;
|
|
1071
|
+
const rec = (e: Expr): Expr => {
|
|
1072
|
+
if (mentionedName(e) === name) {
|
|
1073
|
+
mentions++;
|
|
1074
|
+
}
|
|
1075
|
+
if (e.k === 'var' && e.name === name) {
|
|
1076
|
+
found++;
|
|
1077
|
+
return { k: 'postincr', name, by };
|
|
1078
|
+
}
|
|
1079
|
+
return mapExprChildren(e, rec);
|
|
1080
|
+
};
|
|
1081
|
+
const out = rec(cond);
|
|
1082
|
+
return found === 1 && mentions === 1 ? out : null;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
959
1085
|
// Recovered info for a self-loop header: its exit block and the per-parameter back-edge
|
|
960
1086
|
// arg it feeds (the value on the header→header edge). The back-edge arg is the "next"
|
|
961
1087
|
// value of the phi; mapping it back to the phi turns the latch test into the while test.
|
|
@@ -980,6 +1106,18 @@ interface LoopArm {
|
|
|
980
1106
|
owned: Set<Block>;
|
|
981
1107
|
}
|
|
982
1108
|
|
|
1109
|
+
// A loop with no test of its own, `while (1)`: its latches are conditional continues, and it leaves
|
|
1110
|
+
// by a `break` to `exit` or an early `return`.
|
|
1111
|
+
interface ForeverLoopInfo {
|
|
1112
|
+
header: Block;
|
|
1113
|
+
latches: Block[];
|
|
1114
|
+
forwardPreds: Block[];
|
|
1115
|
+
exit: Block;
|
|
1116
|
+
body: Set<Block>;
|
|
1117
|
+
arms: LoopArm[];
|
|
1118
|
+
breaks: Set<Block>; // the body blocks whose edge to `exit` is a `break`
|
|
1119
|
+
}
|
|
1120
|
+
|
|
983
1121
|
// A test-at-top multi-block `while`. The header is a pure test whose cond_br enters `bodyEntry`
|
|
984
1122
|
// (inside the loop) or leaves to `exit` (the single loop exit). Unlike LoopInfo the condition reads
|
|
985
1123
|
// the header's params directly (top-of-iteration values) — no back-edge substitution.
|
|
@@ -991,6 +1129,7 @@ interface WhileLoopInfo {
|
|
|
991
1129
|
forwardPreds: Block[]; // header preds outside the loop body (the entry/init side)
|
|
992
1130
|
body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
|
|
993
1131
|
arms: LoopArm[]; // the early-`return` exits out of the body (`earlyReturnArm`)
|
|
1132
|
+
breaks: Set<Block>; // the body blocks whose edge to `exit` itself is a `break`
|
|
994
1133
|
}
|
|
995
1134
|
|
|
996
1135
|
// Opcodes whose NUMBER OF EXECUTIONS is observable. Moving one of these out of a loop changes what
|
|
@@ -1171,6 +1310,8 @@ function reHomesParamMerge(
|
|
|
1171
1310
|
export interface CarrierName {
|
|
1172
1311
|
/** the merge is a pure alias — every in-edge hands it the same value */
|
|
1173
1312
|
readonly pureAlias: boolean;
|
|
1313
|
+
/** the name offered is that value's OWN, not a loop variable's name offered through its back edge */
|
|
1314
|
+
readonly ownName: boolean;
|
|
1174
1315
|
/** the name's declaration and the parameter disagree about carrier width */
|
|
1175
1316
|
readonly widthDiffers: boolean;
|
|
1176
1317
|
/** …or, at a sub-word width, about signedness — which at that width is part of the width */
|
|
@@ -1185,6 +1326,9 @@ export interface CarrierName {
|
|
|
1185
1326
|
readonly carrierWritten: boolean;
|
|
1186
1327
|
/** a value with no name of its own re-derives the name at a use past the copy */
|
|
1187
1328
|
readonly reDerivesName: boolean;
|
|
1329
|
+
/** …or reads it through a loop's back-edge argument, which carries the loop variable's name
|
|
1330
|
+
* (`backArgName`) and renders as that name past its loop */
|
|
1331
|
+
readonly backArgReadsName: boolean;
|
|
1188
1332
|
}
|
|
1189
1333
|
|
|
1190
1334
|
/** `canTakeName`'s admission: may this block parameter be SPELLED with a name that already exists,
|
|
@@ -1239,6 +1383,28 @@ export const CARRIER_NAME_GATES: readonly Gate<CarrierName>[] = [
|
|
|
1239
1383
|
guardedBy: 'name-clobber.test.ts: the inlined difference keeps reading the value the asm computed it from',
|
|
1240
1384
|
rejects: (c) => !c.pureAlias && c.reDerivesName,
|
|
1241
1385
|
},
|
|
1386
|
+
{
|
|
1387
|
+
// A back-edge argument is in no `varName`: it takes the header's name unconditionally
|
|
1388
|
+
// (`seedLoopParams`), and the exit region's substitution renders it as that name after the loop.
|
|
1389
|
+
// So a merge there offered the same name overwrites it on every other in-edge. `int u; do { … u =
|
|
1390
|
+
// …; } while (--d > 0); return (c != 1 ? u : u + t) * u;` at mwcc: the merge took `u`'s name, and
|
|
1391
|
+
// its `else` copy overwrote the `u` the multiply still reads.
|
|
1392
|
+
id: 'back-arg-live',
|
|
1393
|
+
why: "a loop's back-edge value renders under the loop variable's name after the loop, and the copies would overwrite it",
|
|
1394
|
+
sound: true,
|
|
1395
|
+
guardedBy: 'name-clobber.test.ts: a back-edge argument live across a merge reads the name it carries',
|
|
1396
|
+
rejects: (c) => !c.pureAlias && c.backArgReadsName,
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
// `carrier-live` and `re-derives` waive a pure alias because the alias and its value are equal on
|
|
1400
|
+
// every path under the value's own name. A loop variable's name holds the value only once the back
|
|
1401
|
+
// edge that carries it has run.
|
|
1402
|
+
id: 'alias-under-its-own-name',
|
|
1403
|
+
why: "a loop variable's name holds the value it is offered for only after the back edge that carries it",
|
|
1404
|
+
sound: true,
|
|
1405
|
+
guardedBy: 'carrier-name.test.ts: a redundant merge shares only its value’s own name, not a later loop variable’s',
|
|
1406
|
+
rejects: (c) => c.pureAlias && !c.ownName && (c.carrierLive || c.reDerivesName),
|
|
1407
|
+
},
|
|
1242
1408
|
];
|
|
1243
1409
|
|
|
1244
1410
|
/** A nested loop's header parameter offered its ENCLOSING loop's name, as
|
|
@@ -1390,11 +1556,11 @@ export interface StructureOptions {
|
|
|
1390
1556
|
// body). GCC freely uses `!=`; IDO prefers `==`/`<`. A compiler behavior, not an `arch ==`
|
|
1391
1557
|
// branch — default true (permissive; the decline path keeps it sound either way).
|
|
1392
1558
|
switchAllowsNeqCase?: boolean;
|
|
1393
|
-
// Comparison-tree switch recovery: treat a relational test
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1396
|
-
//
|
|
1397
|
-
|
|
1559
|
+
// Comparison-tree switch recovery: treat a relational test as a case where the values that can
|
|
1560
|
+
// reach it leave exactly one on a side its compiler's dispatch lands a body on — the BRANCH
|
|
1561
|
+
// (`'taken'`) or either side (`'either'`). A compiler behavior declared in
|
|
1562
|
+
// TargetDescription.compilerBehaviors. Absent: every relational edge navigates.
|
|
1563
|
+
switchBoundCase?: SwitchBoundCase;
|
|
1398
1564
|
// Comparison-tree switch recovery: emit the case arms in the order the ASSEMBLY lays their
|
|
1399
1565
|
// bodies out, rather than sorted by ascending case value. A compiler behavior declared in
|
|
1400
1566
|
// TargetDescription.compilerBehaviors — a compiler opts in on evidence that it neither reorders
|
|
@@ -1440,6 +1606,13 @@ export interface StructureOptions {
|
|
|
1440
1606
|
// spell "no" on a public option type. The translation happens once, in `structureOptionsFor`
|
|
1441
1607
|
// (target.ts), which is where the target-to-structurer mapping lives.
|
|
1442
1608
|
spillSlotOrder?: 'ascending' | 'descending';
|
|
1609
|
+
// Each declared callee's parameter types, carried to the backend on `SFn.declaredArgs` (see
|
|
1610
|
+
// there). Built by `structureOptionsFor` from the prototype table.
|
|
1611
|
+
declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
|
|
1612
|
+
// Each declared callee's return type, carried on `SFn.declaredReturns` the same way.
|
|
1613
|
+
declaredReturns?: Readonly<Record<string, string>>;
|
|
1614
|
+
// The emitted C is compiled as C++ (`SFn.dialect`).
|
|
1615
|
+
dialect?: 'c++';
|
|
1443
1616
|
// Commutative load pairs re-spell in def (evaluation) order — see the swap in lowerDef. Default
|
|
1444
1617
|
// true; verified byte-exact on agbcc and IDO. A compiler behavior declared in
|
|
1445
1618
|
// TargetDescription.compilerBehaviors: the first compiler whose scheduler is shown re-ordering
|
|
@@ -1485,6 +1658,10 @@ export interface StructureOptions {
|
|
|
1485
1658
|
// `deviceMemoryWriters`, which no structurer rule reads). Used as a REFUSAL: absent, a dead read
|
|
1486
1659
|
// at a literal address is dropped.
|
|
1487
1660
|
deviceRegisters?: readonly [number, number];
|
|
1661
|
+
// Does the compiler extend a `volatile` narrow signed read in a register, never in the load? The
|
|
1662
|
+
// `declared` stamp structuring re-derives reads it (TargetDescription.compilerBehaviors'
|
|
1663
|
+
// `volatileReadsExtendInRegister`), so it leaves unplaced the read the lift's stamp did.
|
|
1664
|
+
volatileReadsExtendInRegister?: boolean;
|
|
1488
1665
|
// Spell `(x << a) >> b` extracts of a struct global as the map's named bitfield member. On by
|
|
1489
1666
|
// default; rank.ts enumerates the OFF spelling as the `/no-bitfield` variation, because the named
|
|
1490
1667
|
// read recompiles at the DECLARATION's access width — where that diverges from the asm's load
|
|
@@ -1552,11 +1729,24 @@ export interface StructureOptions {
|
|
|
1552
1729
|
// computation. Off by default; rank.ts enumerates the ON spelling as the `/derived-home` variation —
|
|
1553
1730
|
// see analysis.ts AnalyzeOptions.
|
|
1554
1731
|
homeDerivedReads?: boolean;
|
|
1732
|
+
// Can this compiler FUSE a float multiply into the add or subtract that reads it, when both are
|
|
1733
|
+
// written in one expression? Then every such product is named (analysis.ts), so the spelling
|
|
1734
|
+
// compiles to the unfused pair the object holds. A compiler opts in; the target says which
|
|
1735
|
+
// (`compilerBehaviors.contractsFloatProducts`).
|
|
1736
|
+
contractsFloatProducts?: boolean;
|
|
1737
|
+
// How this compiler lays out a function-scope static (`compilerBehaviors.staticLayout`), which
|
|
1738
|
+
// decides the definition that puts one where the target has it. Absent ⇒ unmeasured, and a
|
|
1739
|
+
// function that defines a static declines.
|
|
1740
|
+
staticLayout?: StaticLayout;
|
|
1555
1741
|
// Materialize a pure value that one join's incoming edges render into the SAME parameter slot
|
|
1556
1742
|
// from 2+ places — the value the source computed once above the branch and the copy machinery
|
|
1557
1743
|
// sinks into every arm. Off by default; rank.ts enumerates the ON spelling as the `/merge-home`
|
|
1558
1744
|
// variation — see analysis.ts AnalyzeOptions.
|
|
1559
1745
|
homeMergeFeeds?: boolean;
|
|
1746
|
+
// Materialize a `zext`/`sext` with 2+ consumers, none of them in its own block — the register the
|
|
1747
|
+
// asm narrowed into once and every later block read. Off by default; rank.ts enumerates the ON
|
|
1748
|
+
// spelling as the `/escape-home` variation — see analysis.ts AnalyzeOptions.
|
|
1749
|
+
homeEscapingExtensions?: boolean;
|
|
1560
1750
|
// Emit a memory read as a named temp in ITS OWN block when every place it renders sits in a
|
|
1561
1751
|
// block that block strictly dominates. A compiler behavior (TargetDescription
|
|
1562
1752
|
// .compilerBehaviors), not a differ-refereed variation: where the compiler has neither a scheduler
|
|
@@ -1621,6 +1811,12 @@ export interface StructureOptions {
|
|
|
1621
1811
|
* A SUPERSET of `inferredSymbols`' names, and its own field for exactly that reason: a struct
|
|
1622
1812
|
* element is licensed here and has no shape there. */
|
|
1623
1813
|
orderLicensedGlobals?: ReadonlySet<string>;
|
|
1814
|
+
/** The globals the project map declares POINTERS, handed to a structuring that runs without the
|
|
1815
|
+
* map (rank.ts `/raw-globals`). Spelling a global raw does not make it an integer: C still scales
|
|
1816
|
+
* arithmetic on its value by the declared pointee, so the byte-arithmetic rules read this where
|
|
1817
|
+
* they would read the map's `shape:'pointer'`. The operand-order rules do not read it: no element
|
|
1818
|
+
* or field spelling folds from a name with no pointee. */
|
|
1819
|
+
pointerGlobals?: ReadonlySet<string>;
|
|
1624
1820
|
}
|
|
1625
1821
|
|
|
1626
1822
|
/** The named global an `index` node's base denotes DIRECTLY, or undefined: the bare `&gSym` an
|
|
@@ -1676,12 +1872,17 @@ export interface StructureHooks {
|
|
|
1676
1872
|
/** `enclosingCarrierName`'s admission rules (`ENCLOSING_CARRIER_GATES`), ablatable the same way —
|
|
1677
1873
|
* and, wrapped in `tallying`, the census of which rule refuses a nest. */
|
|
1678
1874
|
enclosingCarrierGates?: readonly Gate<EnclosingCarrier>[];
|
|
1875
|
+
/** `sinkablePreUpdateSlots`'s per-slot rules (`PREUPDATE_SINK_GATES`), ablatable the same way. */
|
|
1876
|
+
preUpdateSinkGates?: readonly Gate<SinkCandidate>[];
|
|
1679
1877
|
/** Every branch-sense site this structuring reached, in emission order: the block index
|
|
1680
1878
|
* `StructureOptions.branchSenseFlipSites` names, whether the site is JOINED or divergent, and
|
|
1681
1879
|
* which sense it actually emitted. The enumeration domain — a site only exists once structuring
|
|
1682
1880
|
* has decided both arms are real, so it cannot be computed ahead of the pass. */
|
|
1683
1881
|
onBranchSenseSite?: (site: { block: number; ordinal: number; joined: boolean; negated: boolean }) => void;
|
|
1684
|
-
/** Every
|
|
1882
|
+
/** Every branch `followOverReturns` gave a follow: the branch's block and the follow's. Both
|
|
1883
|
+
* callers fire it — a divergent `if` under `followEarlyReturns`, and EVERY jump table whose arms
|
|
1884
|
+
* all return, unconditionally — so the sites it reports are not the enumeration domain of the
|
|
1885
|
+
* shared-tail twins (that domain is `hasDivergentSharedRet`, which asks for a `cond_br`). */
|
|
1685
1886
|
onEarlyReturnFollow?: (site: { block: number; follow: number }) => void;
|
|
1686
1887
|
}
|
|
1687
1888
|
|
|
@@ -1709,6 +1910,7 @@ function assertDefaultAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHo
|
|
|
1709
1910
|
homeLoopExprs: false,
|
|
1710
1911
|
homeDerivedReads: false,
|
|
1711
1912
|
homeMergeFeeds: false,
|
|
1913
|
+
homeEscapingExtensions: false,
|
|
1712
1914
|
anchorConstCopies: false,
|
|
1713
1915
|
anchorLoopEntryConsts: false,
|
|
1714
1916
|
followEarlyReturns: false,
|
|
@@ -1727,22 +1929,37 @@ interface EarlyReturnArmDeps {
|
|
|
1727
1929
|
function isRet(blk: Block): boolean {
|
|
1728
1930
|
return blk.ops[blk.ops.length - 1]?.opcode === 'ret';
|
|
1729
1931
|
}
|
|
1730
|
-
/** The `ret`s reachable from
|
|
1731
|
-
* when there is none, and when `b` does not branch
|
|
1732
|
-
* the start block excluded.
|
|
1932
|
+
/** The `ret`s reachable from EVERY successor of `b` — the region `followOverReturns` keeps. Empty
|
|
1933
|
+
* when there is none, and when `b` does not branch at all. `reachFrom` is forward reachability,
|
|
1934
|
+
* the start block excluded.
|
|
1935
|
+
*
|
|
1936
|
+
* THE QUESTION IS ABOUT A BRANCH, not about an `if`: "the arms all return, so post-dominance gives
|
|
1937
|
+
* this block no join — where does the construct end?" is asked by a jump table exactly as by a
|
|
1938
|
+
* two-way branch, and the answer is derived the same way. The `if`-shaped reading belongs to the
|
|
1939
|
+
* CALLER that wants it (`hasDivergentSharedRet`), not here.
|
|
1940
|
+
*
|
|
1941
|
+
* THE DEDUP IS DEFENSIVE AND NO CALLER CAN REACH IT TODAY. It is what makes "two successors that
|
|
1942
|
+
* are the same block" answer `[]` rather than "a branch with a shared `ret`". Both callers ask
|
|
1943
|
+
* only when `ipdom.get(b)` is null, and a branch whose successors are all ONE block has that block
|
|
1944
|
+
* as its post-dominator, so they never ask it of that shape. It stays because the answer belongs
|
|
1945
|
+
* to this function rather than to its callers' guards, and dropping it is a no-op on the corpus:
|
|
1946
|
+
* the reader over every row of the committed artifact (`decompile` per row, no compiling) emits
|
|
1947
|
+
* byte-identical source with and without it. */
|
|
1733
1948
|
function sharedRetsOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block>): Block[] {
|
|
1734
|
-
const
|
|
1735
|
-
if (
|
|
1949
|
+
const succs = [...new Set(successorsOf(b))];
|
|
1950
|
+
if (succs.length < 2) {
|
|
1736
1951
|
return [];
|
|
1737
1952
|
}
|
|
1738
1953
|
const retsFrom = (x: Block): Block[] => [x, ...reachFrom(x)].filter(isRet);
|
|
1739
|
-
|
|
1740
|
-
|
|
1954
|
+
return succs.map(retsFrom).reduce((shared, rets) => {
|
|
1955
|
+
const here = new Set(rets);
|
|
1956
|
+
return shared.filter((r) => here.has(r));
|
|
1957
|
+
});
|
|
1741
1958
|
}
|
|
1742
1959
|
/** Is there an `if` whose arms reach no common block before EXIT but share a `ret` — the only shape
|
|
1743
1960
|
* `followEarlyReturns` changes? The enumeration gate of both shared-tail twins (rank.ts), asked of
|
|
1744
1961
|
* the fn as raised and again of the sunk fn; a superset, since it asks `sharedRetsOf` and none of
|
|
1745
|
-
* the follow's three later refusals. */
|
|
1962
|
+
* the follow's three later refusals. An `if`, specifically: see the restriction at the `some`. */
|
|
1746
1963
|
export function hasDivergentSharedRet(fn: Fn): boolean {
|
|
1747
1964
|
const ipdom = postDominators(fn);
|
|
1748
1965
|
const reach = new Map<Block, Set<Block>>();
|
|
@@ -1761,7 +1978,13 @@ export function hasDivergentSharedRet(fn: Fn): boolean {
|
|
|
1761
1978
|
}
|
|
1762
1979
|
return out;
|
|
1763
1980
|
};
|
|
1764
|
-
|
|
1981
|
+
// `cond_br` HERE, not in `sharedRetsOf`: this is the enumeration gate of the shared-tail twins,
|
|
1982
|
+
// and those twins are `if`-shaped variations. A jump table with a shared `ret` has the same
|
|
1983
|
+
// no-join shape and no such twin, so asking for it would spend fan on a duplicate candidate.
|
|
1984
|
+
return fn.blocks.some(
|
|
1985
|
+
(b) =>
|
|
1986
|
+
b.ops[b.ops.length - 1]?.opcode === 'cond_br' && ipdom.get(b) === null && sharedRetsOf(b, reachFrom).length > 0,
|
|
1987
|
+
);
|
|
1765
1988
|
}
|
|
1766
1989
|
/** does a path from `from` reach `to` without passing through `avoid`? */
|
|
1767
1990
|
function reachesAvoiding(from: Block, to: Block, avoid: Block): boolean {
|
|
@@ -1822,7 +2045,7 @@ function earlyReturnArm(
|
|
|
1822
2045
|
} // re-enters the loop → not an exit
|
|
1823
2046
|
if (entryOwned && dom.get(bb)!.has(to)) {
|
|
1824
2047
|
owned.add(bb);
|
|
1825
|
-
} else if (bb.ops.some(
|
|
2048
|
+
} else if (bb.ops.some(effectful)) {
|
|
1826
2049
|
return null;
|
|
1827
2050
|
}
|
|
1828
2051
|
const t = bb.ops[bb.ops.length - 1];
|
|
@@ -1860,11 +2083,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
1860
2083
|
orderArgCopiesByWriteOrder = true,
|
|
1861
2084
|
preferDefPosCopyOrder = false,
|
|
1862
2085
|
switchAllowsNeqCase = true,
|
|
1863
|
-
|
|
2086
|
+
switchBoundCase,
|
|
1864
2087
|
switchArmsFollowLayout = false,
|
|
1865
2088
|
switchRequiresFrontLoadedTests = false,
|
|
1866
2089
|
spellSwitchFallthrough = true,
|
|
1867
2090
|
spillSlotOrder,
|
|
2091
|
+
declaredArgs,
|
|
2092
|
+
declaredReturns,
|
|
2093
|
+
dialect,
|
|
1868
2094
|
defOrderLoadPairs = true,
|
|
1869
2095
|
anchorConstCopies = false,
|
|
1870
2096
|
anchorLoopEntryConsts = false,
|
|
@@ -1878,7 +2104,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
1878
2104
|
homeLoopExprs = false,
|
|
1879
2105
|
homeDerivedReads = false,
|
|
1880
2106
|
homeMergeFeeds = false,
|
|
2107
|
+
homeEscapingExtensions = false,
|
|
1881
2108
|
readsStayWhereWritten = false,
|
|
2109
|
+
contractsFloatProducts = false,
|
|
2110
|
+
staticLayout,
|
|
1882
2111
|
unsignedCompareSpelling = false,
|
|
1883
2112
|
coalesceMergeNames = false,
|
|
1884
2113
|
freshParamMerge = false,
|
|
@@ -1887,16 +2116,43 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
1887
2116
|
symbols: mapSymbols,
|
|
1888
2117
|
inferredSymbols,
|
|
1889
2118
|
orderLicensedGlobals,
|
|
2119
|
+
pointerGlobals,
|
|
1890
2120
|
} = opts;
|
|
2121
|
+
// A read this stamp is the first to place was plain to every raising pass, and in a `&&`/`||`'s
|
|
2122
|
+
// guarded operand a short-circuit fold may have lifted it out of the arm it ran in: which side of
|
|
2123
|
+
// the branch the asm read it on is lost. The analysis names a counted op it finds there above the
|
|
2124
|
+
// branch, which is right only for one no fold could move, so this read declines instead.
|
|
2125
|
+
const stampedLate = stampDeclaredVolatile(fn, mapSymbols, opts);
|
|
2126
|
+
if (stampedLate.length > 0) {
|
|
2127
|
+
const defsNow = defOpMap(fn);
|
|
2128
|
+
const guarded = shortCircuitGuardedValues(fn, defsNow);
|
|
2129
|
+
const moved = stampedLate.find((op) => op.results.some((r) => guarded.has(r)));
|
|
2130
|
+
if (moved !== undefined) {
|
|
2131
|
+
throw new StructureError(
|
|
2132
|
+
`cannot structure '${fn.name}': a '&&'/'||' would guard a read of the volatile object ` +
|
|
2133
|
+
`'${globalBaseOf(defsNow, moved.operands[0]) ?? 'a volatile object'}', and which side of the branch the asm read it on is not recoverable`,
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
1891
2137
|
// THE shape dictionary the rendering context asks, map-first. Built as a lookup rather than a
|
|
1892
2138
|
// merged Map because the map is the PROJECT's and is asked by name for a whole project's worth
|
|
1893
2139
|
// of symbols — copying it per structuring is work proportional to the project, and a ranked run
|
|
1894
2140
|
// structures one function thousands of times (the same argument the `laddr` name minter makes).
|
|
2141
|
+
//
|
|
2142
|
+
// A function-scope static is asked by its linker name, which no map holds, and comes FIRST: its
|
|
2143
|
+
// shape is its definition's, never the one raise/globalshape.ts infers from its accesses.
|
|
2144
|
+
const localStatics = localStaticShapes(fn, staticLayout);
|
|
2145
|
+
if ('refused' in localStatics) {
|
|
2146
|
+
throw new StructureError(
|
|
2147
|
+
`cannot structure '${fn.name}': names a function-scope static ('${localStatics.symbol}') ${localStatics.refused}`,
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
const staticInfo = localStatics.infos;
|
|
1895
2151
|
const symbols: SymbolLookup | undefined =
|
|
1896
|
-
mapSymbols !== undefined || (inferredSymbols !== undefined && inferredSymbols.size > 0)
|
|
2152
|
+
mapSymbols !== undefined || (inferredSymbols !== undefined && inferredSymbols.size > 0) || staticInfo.size > 0
|
|
1897
2153
|
? {
|
|
1898
|
-
get: (n) => mapSymbols?.get(n) ?? inferredSymbols?.get(n),
|
|
1899
|
-
has: (n) => mapSymbols?.has(n) === true || inferredSymbols?.has(n) === true,
|
|
2154
|
+
get: (n) => staticInfo.get(n) ?? mapSymbols?.get(n) ?? inferredSymbols?.get(n),
|
|
2155
|
+
has: (n) => staticInfo.has(n) || mapSymbols?.has(n) === true || inferredSymbols?.has(n) === true,
|
|
1900
2156
|
}
|
|
1901
2157
|
: undefined;
|
|
1902
2158
|
// Only the MAP makes the named bitfield spelling available, so with no map this is not a question.
|
|
@@ -1913,8 +2169,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
1913
2169
|
// IS this target's compiler behaviors, so resetting one would probe a spelling asmlift never emits
|
|
1914
2170
|
// here.
|
|
1915
2171
|
//
|
|
1916
|
-
//
|
|
1917
|
-
// each for its own reason, and
|
|
2172
|
+
// THE STRUCTURE VARIATIONS `assertDefaultAccepts` DOES NOT RESET (rank-variations.ts) ARE
|
|
2173
|
+
// DELIBERATE NON-MEMBERS, each for its own reason, and this is the whole of that list — the one
|
|
2174
|
+
// place it is stated, derived from the guard's own reset list and held to it by
|
|
2175
|
+
// test/variation-offers.test.ts. Every variation NOT named here is one the guard resets, so the
|
|
2176
|
+
// invariant it protects — a candidate spelling never unlocks a function the default declines —
|
|
2177
|
+
// rests on the guard for those and on the argument beside each name for these:
|
|
1918
2178
|
// - `/site-sense` (senseFromFoldEvidence) decides which way each folded branch is written. It
|
|
1919
2179
|
// negates a branch sense per site and touches no copy, like the per-function sense booleans;
|
|
1920
2180
|
// - `/reread-globals` (rereadGlobals) is an ANALYSIS option, and it only ever RELAXES: it
|
|
@@ -1934,6 +2194,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
1934
2194
|
homeLoopExprs ||
|
|
1935
2195
|
homeDerivedReads ||
|
|
1936
2196
|
homeMergeFeeds ||
|
|
2197
|
+
homeEscapingExtensions ||
|
|
1937
2198
|
anchorConstCopies ||
|
|
1938
2199
|
followEarlyReturns
|
|
1939
2200
|
) {
|
|
@@ -1953,10 +2214,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
1953
2214
|
};
|
|
1954
2215
|
|
|
1955
2216
|
// ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
|
|
1956
|
-
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom, emitPos, memWriteBetween } =
|
|
1957
|
-
fn,
|
|
1958
|
-
returnsVoid,
|
|
1959
|
-
{
|
|
2217
|
+
const { useSitesOf, opIndex, opBlock, liveIn, materialize, preUpdateHomes, reachFrom, emitPos, memWriteBetween } =
|
|
2218
|
+
analyze(fn, returnsVoid, {
|
|
1960
2219
|
defs,
|
|
1961
2220
|
dom,
|
|
1962
2221
|
rereadGlobals,
|
|
@@ -1965,14 +2224,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
1965
2224
|
homeLoopExprs,
|
|
1966
2225
|
homeDerivedReads,
|
|
1967
2226
|
homeMergeFeeds,
|
|
2227
|
+
homeEscapingExtensions,
|
|
1968
2228
|
readsStayWhereWritten,
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
const si = symbols?.get(n);
|
|
1972
|
-
return si?.volatile === true || (si?.layout ?? []).some((f) => f.volatile === true);
|
|
1973
|
-
},
|
|
1974
|
-
},
|
|
1975
|
-
);
|
|
2229
|
+
contractsFloatProducts,
|
|
2230
|
+
});
|
|
1976
2231
|
|
|
1977
2232
|
// THE FOLLOW OF A DIVERGENT `if`, over the paths that do not return early. Post-dominance gives
|
|
1978
2233
|
// such an `if` no join — its arms reach two different `ret`s, and EXIT is the only block on every
|
|
@@ -1983,21 +2238,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
1983
2238
|
// rank.ts's `/shared-ret` twin, and as its `/shared-tail` twin after the store-tail sink: as a
|
|
1984
2239
|
// default it costs rows, measured there.
|
|
1985
2240
|
//
|
|
1986
|
-
// WHICH REGION: the `ret`s reachable from
|
|
2241
|
+
// WHICH REGION: the `ret`s reachable from EVERY successor. Every block that cannot reach one of
|
|
1987
2242
|
// them is an early-return region and is deleted; the follow is `b`'s post-dominator over what is
|
|
1988
2243
|
// left. Sound for the same reason an ordinary follow is: every kept path from `b` passes it, and a
|
|
1989
2244
|
// deleted block reaches no kept `ret` and so never the follow, so each arm structures up to the
|
|
1990
|
-
// follow or into a `return`.
|
|
1991
|
-
//
|
|
2245
|
+
// follow or into a `return`.
|
|
2246
|
+
//
|
|
2247
|
+
// IT CAN DUPLICATE ACROSS THE ARMS, AND ABOVE TWO SUCCESSORS IT DOES. With two, it cannot: a
|
|
2248
|
+
// deleted region both arms reach would have its `ret` in the intersection, and so would not have
|
|
2249
|
+
// been deleted. With three or more that argument does not carry — a `ret` reachable from two of
|
|
2250
|
+
// three arms is NOT in the intersection, so its region is deleted from `keep` and re-emitted in
|
|
2251
|
+
// each arm that reaches it. The copies sit on mutually exclusive paths, so the program stays
|
|
2252
|
+
// correct; the cost is bytes, which is the only currency here. The `switch_br` caller pays it
|
|
2253
|
+
// knowingly: its alternative is not a shorter spelling but a decline (see that caller).
|
|
1992
2254
|
//
|
|
1993
2255
|
// PER `if`, never function-wide: the deletion set is a function of that `if`'s own shared `ret`s
|
|
1994
2256
|
// (memoized on them), because a nested `if` shares a different set, or none.
|
|
1995
2257
|
//
|
|
1996
|
-
// REFUSES (keeping the divergent arms) when no `ret` is reachable from
|
|
2258
|
+
// REFUSES (keeping the divergent arms) when no `ret` is reachable from every successor, when the
|
|
1997
2259
|
// kept graph gives `b` no post-dominator but EXIT, and when the enclosing region's `stop` is
|
|
1998
2260
|
// reachable from `b` without passing the follow — that path must reach `stop`, and structuring
|
|
1999
|
-
// the arms towards a different follow would emit `stop`'s region inside an arm.
|
|
2000
|
-
//
|
|
2261
|
+
// the arms towards a different follow would emit `stop`'s region inside an arm.
|
|
2262
|
+
//
|
|
2263
|
+
// THE TWO CALLERS DIFFER ON LOOPS AND ONLY ONE OF THEM GUARDS IT. The `if` caller asks outside a
|
|
2264
|
+
// loop body only (`followEarlyReturns && loopCtx === null`, with `clampToLoop` as its second
|
|
2265
|
+
// net), for the reason written there. The `switch_br` caller does not consult `loopCtx` at all,
|
|
2266
|
+
// and no input has been found that reaches this from inside a loop body: every `ret` the arms of
|
|
2267
|
+
// an in-loop branch reach is reachable from the latch too, so the intersection keeps the whole
|
|
2268
|
+
// graph, `pd` is `ipdom` itself — and the caller only asks when `ipdom.get(b)` is already null,
|
|
2269
|
+
// so the answer comes back null and the `?? stop` fallback stands. That is an observation about
|
|
2270
|
+
// the shape rather than a check, which is why it is written down here.
|
|
2001
2271
|
const followsByShared = new Map<string, Map<Block, Block | null>>();
|
|
2002
2272
|
const followOverReturns = (b: Block, stop: Block | null): Block | null => {
|
|
2003
2273
|
const shared = sharedRetsOf(b, reachFrom);
|
|
@@ -2028,10 +2298,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2028
2298
|
// `((T *)&gSym)[i]` address-cast form (a struct value does not decay, so casting the bare name is
|
|
2029
2299
|
// invalid C; casting the address is valid and byte-exact). Computed once here.
|
|
2030
2300
|
//
|
|
2031
|
-
//
|
|
2032
|
-
//
|
|
2033
|
-
//
|
|
2034
|
-
//
|
|
2301
|
+
// THE ACCESS SPELLING IS PART OF THE QUESTION, not only the offset set. The bare name is ONE
|
|
2302
|
+
// declaration, so it spells one width and one signedness; a symbol read at off-0 two different
|
|
2303
|
+
// ways has no bare spelling at all, and emitting one loses the second access entirely
|
|
2304
|
+
// (`gState + gState` for a halfword plus a byte). Such a symbol is an AGGREGATE here, so every
|
|
2305
|
+
// access takes the address-cast form at its OWN width — `*(u16 *)&gState + *(u8 *)&gState`,
|
|
2306
|
+
// which is what the source wrote and what raise/structs.ts's constant-address sibling emits for
|
|
2307
|
+
// the same shape at a literal address.
|
|
2308
|
+
//
|
|
2309
|
+
// TWO QUESTIONS, AND THEY ARE ASKED OF DIFFERENT ACCESSES. WIDTH is asked of every load and store: a
|
|
2310
|
+
// `u16` declaration cannot carry a word store either. SIGNEDNESS is asked of LOADS ONLY — a store
|
|
2311
|
+
// writes the same bytes through either declaration (`*(s16 *)&g = x` and `*(u16 *)&g = x` are one
|
|
2312
|
+
// instruction), while a load does not: `lh` and `lhu` on one cell are two declarations, and
|
|
2313
|
+
// spelling both `g` would lose the zero-extension. Unreachable in Thumb, where `ldrsh`/`ldrsb`
|
|
2314
|
+
// need a register offset and the `add` that forms it already marks the symbol aggregate; live on
|
|
2315
|
+
// MIPS, where `%hi/%lo` addressing carries no such `add`.
|
|
2035
2316
|
// FRAME-LOCAL OBJECT NAMES (laddr). Minted HERE, not in the frontend, because identifiers live
|
|
2036
2317
|
// in this layer's namespace: params, locals, every gaddr symbol, and the project's symbol map —
|
|
2037
2318
|
// none of which the frontend can see. A frontend-chosen `sp0` silently shadowed a project global
|
|
@@ -2050,10 +2331,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2050
2331
|
for (const op of b.ops) {
|
|
2051
2332
|
if (op.opcode === 'gaddr') {
|
|
2052
2333
|
taken.add(op.attrs.sym as string);
|
|
2053
|
-
} else if (op
|
|
2334
|
+
} else if (calleeName(op) !== undefined) {
|
|
2054
2335
|
// a CALLEE's name is in this namespace too: a function really named sp0 would be
|
|
2055
2336
|
// shadowed by the minted local, and `sp0()` on a u16 object is a compile error
|
|
2056
|
-
taken.add(op
|
|
2337
|
+
taken.add(calleeName(op)!);
|
|
2057
2338
|
}
|
|
2058
2339
|
}
|
|
2059
2340
|
}
|
|
@@ -2090,11 +2371,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2090
2371
|
})();
|
|
2091
2372
|
|
|
2092
2373
|
const scalarGlobals = new Set<string>();
|
|
2374
|
+
/** The scalar GLOBALS (never a frame object) the IR loads as a pointer at least once: one load
|
|
2375
|
+
* typed a pointer is the value used as an address somewhere in the function, while its other
|
|
2376
|
+
* loads may type an integer only because they reach a call argument or a compare. */
|
|
2377
|
+
const pointerLoadedGlobals = new Set<string>();
|
|
2378
|
+
/** Every scalar GLOBAL (never a frame object) whose value the function loads as a word, the
|
|
2379
|
+
* only width a pointer has: a `u8`/`u16` global is an integer whatever it is added to. */
|
|
2380
|
+
const wordLoadedGlobals = new Set<string>();
|
|
2093
2381
|
{
|
|
2094
2382
|
const offsets = new Map<string, Set<number>>();
|
|
2383
|
+
const widths = new Map<string, Set<number>>();
|
|
2384
|
+
const loadSigns = new Map<string, Set<boolean>>();
|
|
2385
|
+
const loadsPointer = new Map<string, boolean>();
|
|
2095
2386
|
const bumpAgg = (sym: string) => offsets.set(sym, new Set([-1])); // -1 marks "variable index"
|
|
2096
2387
|
for (const b of fn.blocks) {
|
|
2097
2388
|
for (const op of b.ops) {
|
|
2389
|
+
// A frame object declared as an aggregate (`laddrType`) is one whatever its accesses agree
|
|
2390
|
+
// on — the frame-side twin of the symbol map's declaration-shape override. Spelled
|
|
2391
|
+
// scalar, a store assigns to the array and a load reads its address.
|
|
2392
|
+
if (op.opcode === 'laddr' && laddrType(op).kind !== 'int') {
|
|
2393
|
+
bumpAgg(laddrName.get(op)!);
|
|
2394
|
+
}
|
|
2098
2395
|
const gaddrSym = (v: Value) => {
|
|
2099
2396
|
const dv = defs.get(v);
|
|
2100
2397
|
// laddr participates identically: `sp0` is scalar-spelled at off 0 and cast-spelled
|
|
@@ -2110,6 +2407,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2110
2407
|
const s = gaddrSym(op.operands[0]);
|
|
2111
2408
|
if (s) {
|
|
2112
2409
|
(offsets.get(s) ?? offsets.set(s, new Set()).get(s)!).add(op.attrs.off as number);
|
|
2410
|
+
(widths.get(s) ?? widths.set(s, new Set()).get(s)!).add(op.attrs.width as number);
|
|
2411
|
+
if (op.opcode === 'load') {
|
|
2412
|
+
(loadSigns.get(s) ?? loadSigns.set(s, new Set()).get(s)!).add(op.attrs.signed as boolean);
|
|
2413
|
+
if (defs.get(op.operands[0])?.opcode === 'gaddr') {
|
|
2414
|
+
loadsPointer.set(s, loadsPointer.get(s) === true || op.results[0].type.kind === 'ptr');
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2113
2417
|
}
|
|
2114
2418
|
} else if (op.opcode === 'add' || op.opcode === 'sub') {
|
|
2115
2419
|
// ANY arithmetic on the symbol's address is interior addressing ⇒ aggregate — even when
|
|
@@ -2132,10 +2436,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2132
2436
|
}
|
|
2133
2437
|
}
|
|
2134
2438
|
for (const [sym, offs] of offsets) {
|
|
2135
|
-
if (offs.size === 1 && offs.has(0)) {
|
|
2439
|
+
if (offs.size === 1 && offs.has(0) && widths.get(sym)?.size === 1 && (loadSigns.get(sym)?.size ?? 1) === 1) {
|
|
2136
2440
|
scalarGlobals.add(sym);
|
|
2137
2441
|
}
|
|
2138
2442
|
}
|
|
2443
|
+
for (const [sym, ptr] of loadsPointer) {
|
|
2444
|
+
if (scalarGlobals.has(sym)) {
|
|
2445
|
+
if (widths.get(sym)?.has(4)) {
|
|
2446
|
+
wordLoadedGlobals.add(sym);
|
|
2447
|
+
}
|
|
2448
|
+
if (ptr) {
|
|
2449
|
+
pointerLoadedGlobals.add(sym);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2139
2453
|
// Declaration-shape OVERRIDE (symbol map): a project-declared array/struct global is an
|
|
2140
2454
|
// AGGREGATE whatever the usage inference saw — a lone off-0 access to `extern u16 tbl[]`
|
|
2141
2455
|
// must still spell through the aggregate/array forms, never the bare scalar `tbl`.
|
|
@@ -2179,15 +2493,128 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2179
2493
|
}
|
|
2180
2494
|
: undefined;
|
|
2181
2495
|
|
|
2182
|
-
/**
|
|
2183
|
-
* pointer
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
*
|
|
2188
|
-
*
|
|
2189
|
-
|
|
2190
|
-
|
|
2496
|
+
/** The declared shape of a global as the pointer-value rules below read it: the map's, or a
|
|
2497
|
+
* pointer the map declares to a structuring that does not spell from it (`pointerGlobals`). */
|
|
2498
|
+
const declaredShape = (name: string): SymbolInfo['shape'] =>
|
|
2499
|
+
symCtx?.info(name)?.shape ?? (pointerGlobals?.has(name) ? 'pointer' : undefined);
|
|
2500
|
+
/** Whether no declaration a spelling folds from types the global: the map's is the only one.
|
|
2501
|
+
* The operand-order rules read this, not declaredShape: a declared pointer keeps `x + (u8 *)p`
|
|
2502
|
+
* for the element and field spellings to read, and a `pointerGlobals` name has no pointee for
|
|
2503
|
+
* them, so its sum is spelled in the asm's order like any other undeclared global's. */
|
|
2504
|
+
const mapUndeclared = (name: string): boolean => symCtx?.info(name)?.shape === undefined;
|
|
2505
|
+
|
|
2506
|
+
/** A POINTER VALUE whose type the project's header owns: a bare `gSym` naming a pointer global
|
|
2507
|
+
* (the VALUE of a pointer cell), or a named MEMBER whose declaration is a pointer (`gSym.pBuf`,
|
|
2508
|
+
* `gPtr->pBuf`). Load, store and compare of such a 4-byte cell are identical for any
|
|
2509
|
+
* object-pointer type, so the declared pointee never matters to THEM; arithmetic on the loaded
|
|
2510
|
+
* value is the opposite case, where the pointee's size scales what is added and every stride
|
|
2511
|
+
* must therefore be made explicit (`(u8 *)gPtr + K`). `ctype` cannot see any of this: it types
|
|
2512
|
+
* only params/locals, so both spellings render `undefined` there.
|
|
2513
|
+
*
|
|
2514
|
+
* A bare global is one when its declared shape is `'pointer'` (declaredShape), or when no
|
|
2515
|
+
* declaration this pass can read says anything (no map, a symtab-only name, entries that
|
|
2516
|
+
* disagree and were dropped to the bare name, a name the map lacks) and the IR loads it as a
|
|
2517
|
+
* pointer at least once. The project still declares it, typically `struct S *`. A map
|
|
2518
|
+
* `shape:'scalar'` is a declaration and is excluded; so is a global the IR never loads as a
|
|
2519
|
+
* pointer (`gCount + 1`, or the `u8` index in `gIdx + gItems`), where casting would make a
|
|
2520
|
+
* pointer of an integer the source added as one. */
|
|
2521
|
+
const isPtrValue = (x: Expr): boolean => {
|
|
2522
|
+
if (x.k !== 'var') {
|
|
2523
|
+
return ptrMemberDecl(x, symCtx) !== null;
|
|
2524
|
+
}
|
|
2525
|
+
const shape = declaredShape(x.name);
|
|
2526
|
+
return shape === 'pointer' || (shape === undefined && pointerLoadedGlobals.has(x.name));
|
|
2527
|
+
};
|
|
2528
|
+
|
|
2529
|
+
/** A global's word VALUE that no declaration this pass can read types, whatever the IR loaded it
|
|
2530
|
+
* as. */
|
|
2531
|
+
const isUndeclaredGlobalValue = (x: Expr): boolean =>
|
|
2532
|
+
x.k === 'var' && wordLoadedGlobals.has(x.name) && declaredShape(x.name) === undefined;
|
|
2533
|
+
|
|
2534
|
+
/** A pointer value or a global's word as the integer the asm added. A global no declaration
|
|
2535
|
+
* types and the IR never loads as a pointer may be declared a float, whose value `(u32)g`
|
|
2536
|
+
* converts, so it goes through `(u8 *)` first: `(u32)(u8 *)g` is `(u32)g`'s bytes under every
|
|
2537
|
+
* integer and pointer declaration, and no C under a float one. KNOWN GAP: under an array or a
|
|
2538
|
+
* function declaration `g` decays to its address, which this spells with no diagnostic; only a
|
|
2539
|
+
* pun reads the word there, and intoDeclaredTemp's KNOWN GAP says why none is spelled. */
|
|
2540
|
+
const globalWord = (x: Expr): Expr => ({
|
|
2541
|
+
k: 'cast',
|
|
2542
|
+
to: T.u(32),
|
|
2543
|
+
e:
|
|
2544
|
+
x.k === 'var' && declaredShape(x.name) === undefined && !pointerLoadedGlobals.has(x.name)
|
|
2545
|
+
? { k: 'cast', to: T.ptr(T.u(8)), e: x }
|
|
2546
|
+
: x,
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
/** An integer operand with every undeclared global it adds or subtracts made a word as well:
|
|
2550
|
+
* bare, `a0 + g` is pointer arithmetic under a pointer declaration of g. Other operators reject
|
|
2551
|
+
* a pointer operand, and a load or a cast types its own value. */
|
|
2552
|
+
const intWords = (x: Expr): Expr =>
|
|
2553
|
+
isUndeclaredGlobalValue(x)
|
|
2554
|
+
? globalWord(x)
|
|
2555
|
+
: x.k === 'bin' && (x.op === '+' || x.op === '-')
|
|
2556
|
+
? { ...x, l: intWords(x.l), r: intWords(x.r) }
|
|
2557
|
+
: x;
|
|
2558
|
+
|
|
2559
|
+
/** A byte sum the pointer-value arithmetic rule below made of a global's value (`(u8 *)g + K`),
|
|
2560
|
+
* and the same sum as the integer it also is (`(u32)g + K`), which an integer added in front of
|
|
2561
|
+
* it takes. KNOWN GAP: an integer READER of the byte sum converts a pointer to an integer. The
|
|
2562
|
+
* backend casts it where the reader's type is known (an assignment, a store through a typed
|
|
2563
|
+
* slot, a return: cfamily `legalizePointerWrites`), and a compare against an integer compares
|
|
2564
|
+
* it as a `u32`. A call argument and a global no declaration types keep the pointer, which agbcc
|
|
2565
|
+
* and KMC gcc warn about and CodeWarrior rejects; only a pointer cell's sum passed to a parameter
|
|
2566
|
+
* the prototype declares an integer is converted (l3/ptrcell.ts). The integer sum is no fix: gcc
|
|
2567
|
+
* orders a pointer sum's operands and an integer sum's differently, so the bytes differ. */
|
|
2568
|
+
const castGlobal = (x: Expr, undeclared = false): x is Extract<Expr, { k: 'cast' }> =>
|
|
2569
|
+
x.k === 'cast' &&
|
|
2570
|
+
typeEquals(x.to, T.ptr(T.u(8))) &&
|
|
2571
|
+
x.e.k === 'var' &&
|
|
2572
|
+
(isPtrValue(x.e) || isUndeclaredGlobalValue(x.e)) &&
|
|
2573
|
+
(!undeclared || mapUndeclared(x.e.name));
|
|
2574
|
+
/** The integer sum the rule spells in the asm's order instead (`(u32)g + x`), cast back to the
|
|
2575
|
+
* byte pointer it stands for: `(u8 *)((u32)g + x)`. A byte sum converted whole (byteSumAsInt's
|
|
2576
|
+
* `(u32)((u8 *)g + x - gB)`) is one of its words. */
|
|
2577
|
+
const intGlobalWord = (x: Expr, undeclared: boolean): boolean =>
|
|
2578
|
+
x.k === 'cast' &&
|
|
2579
|
+
typeEquals(x.to, T.u(32)) &&
|
|
2580
|
+
(castGlobal(x.e, undeclared) ||
|
|
2581
|
+
(x.e.k === 'bin' && isByteGlobalSum(x.e, undeclared)) ||
|
|
2582
|
+
(x.e.k === 'var' &&
|
|
2583
|
+
(isPtrValue(x.e) || isUndeclaredGlobalValue(x.e)) &&
|
|
2584
|
+
(!undeclared || mapUndeclared(x.e.name))));
|
|
2585
|
+
const isIntGlobalSum = (x: Expr, undeclared: boolean): boolean =>
|
|
2586
|
+
x.k === 'bin' &&
|
|
2587
|
+
(x.op === '+' || x.op === '-') &&
|
|
2588
|
+
(intGlobalWord(x.l, undeclared) ||
|
|
2589
|
+
intGlobalWord(x.r, undeclared) ||
|
|
2590
|
+
isIntGlobalSum(x.l, undeclared) ||
|
|
2591
|
+
isIntGlobalSum(x.r, undeclared));
|
|
2592
|
+
const restoredIntSum = (x: Expr, undeclared: boolean): Expr | undefined =>
|
|
2593
|
+
x.k === 'cast' && typeEquals(x.to, T.ptr(T.u(8))) && isIntGlobalSum(x.e, undeclared) ? x.e : undefined;
|
|
2594
|
+
const isByteGlobalSum = (x: Expr, undeclared = false): boolean =>
|
|
2595
|
+
restoredIntSum(x, undeclared) !== undefined ||
|
|
2596
|
+
(x.k === 'bin' &&
|
|
2597
|
+
(x.op === '+' || x.op === '-') &&
|
|
2598
|
+
(castGlobal(x.l, undeclared) ||
|
|
2599
|
+
castGlobal(x.r, undeclared) ||
|
|
2600
|
+
isByteGlobalSum(x.l, undeclared) ||
|
|
2601
|
+
isByteGlobalSum(x.r, undeclared)));
|
|
2602
|
+
/** The integer a byte sum is. Every global it adds bare goes a word with it. A lone global it
|
|
2603
|
+
* subtracts stays the pointer difference, `(u32)((u8 *)g + x - gB)`: the asm's integer under an
|
|
2604
|
+
* integer or a byte-pointer declaration of `gB`, and no C under a wider pointer or array, a
|
|
2605
|
+
* function or a float one, where the word `(u32)(u8 *)gB` would subtract an address. Under a byte
|
|
2606
|
+
* array it subtracts the array's address: globalWord's KNOWN GAP. A difference has one operand
|
|
2607
|
+
* order, so the asm's needs no integer sum. A sum of globals it subtracts goes words: bare,
|
|
2608
|
+
* `gB2 - gB3` is an element count under a wider pointer declaration. */
|
|
2609
|
+
const byteSumAsInt = (x: Expr): Expr =>
|
|
2610
|
+
restoredIntSum(x, false) ??
|
|
2611
|
+
(castGlobal(x)
|
|
2612
|
+
? globalWord(x.e)
|
|
2613
|
+
: x.k === 'bin' && isByteGlobalSum(x)
|
|
2614
|
+
? x.op === '-' && isUndeclaredGlobalValue(x.r)
|
|
2615
|
+
? { k: 'cast', to: T.u(32), e: x }
|
|
2616
|
+
: { ...x, l: byteSumAsInt(x.l), r: byteSumAsInt(x.r) }
|
|
2617
|
+
: intWords(x));
|
|
2191
2618
|
|
|
2192
2619
|
/** Operands `-`/`~` cannot take as spelled: a rendered pointer, a bare `&gSym`, a pointer
|
|
2193
2620
|
* global's value. All three are ill-formed C under a unary arithmetic operator — the asm did
|
|
@@ -2244,7 +2671,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2244
2671
|
P !== b &&
|
|
2245
2672
|
pt?.opcode === 'br' &&
|
|
2246
2673
|
P.params.length === 0 &&
|
|
2247
|
-
P.ops.every((o) => !
|
|
2674
|
+
P.ops.every((o) => !effectful(o) && !materialize.has(o)) &&
|
|
2248
2675
|
// at least one def the LOOP BODY reads — the loop-invariant-motion shape this claim
|
|
2249
2676
|
// exists for. A block that only computes the init args is the do-while path's
|
|
2250
2677
|
// ordinary entry chain, and that path's sink machinery handles it better.
|
|
@@ -2270,7 +2697,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2270
2697
|
// reducible single-entry body, and a SINGLE real (non-ret) exit — early returns (ret-terminated
|
|
2271
2698
|
// targets) are allowed in-body. The shape then splits on WHERE the exit lives: the HEADER exits
|
|
2272
2699
|
// (pure test-at-top) → `while`; the LATCH exits (body-first) → `do-while`. Anything that fails
|
|
2273
|
-
// declines to plain if-recovery, which re-enters the header and
|
|
2700
|
+
// is left to `while (1)` (below), or declines to plain if-recovery, which re-enters the header and
|
|
2701
|
+
// fails loud via `onStack`.
|
|
2702
|
+
//
|
|
2703
|
+
// What every loop form asks first (`admissible`): each inner loop whose header sits in this body
|
|
2704
|
+
// is PROPERLY nested — its ENTIRE body is contained in ours (a forest descendant). Structuring
|
|
2705
|
+
// then recurses naturally: when the outer body reaches the inner header, structureBlock
|
|
2706
|
+
// dispatches to the inner's own emitter. An OVERLAPPING loop (shared blocks, neither containing
|
|
2707
|
+
// the other → irreducible) DECLINES. If a contained inner is itself unstructurable, the outer's
|
|
2708
|
+
// body structuring loud-fails at the inner back-edge (onStack) — a safe decline, not a
|
|
2709
|
+
// miscompile. And the body is single-entry: every block but the header is entered ONLY from
|
|
2710
|
+
// inside it — no jump into the loop interior. A `break` edge must also not leave from an inner
|
|
2711
|
+
// loop's body (`innerBodyHas`), which would be a two-level exit.
|
|
2712
|
+
const innerBodyHas = (nl: NaturalLoop, b: Block): boolean =>
|
|
2713
|
+
[...forest.byHeader.values()].some((l2) => l2.header !== nl.header && nl.body.has(l2.header) && l2.body.has(b));
|
|
2714
|
+
const admissible = (nl: NaturalLoop): boolean =>
|
|
2715
|
+
[...forest.byHeader.values()].every(
|
|
2716
|
+
(l2) => l2.header === nl.header || !nl.body.has(l2.header) || [...l2.body].every((b) => nl.body.has(b)),
|
|
2717
|
+
) && [...nl.body].every((bb) => bb === nl.header || (preds.get(bb) ?? []).every((p) => nl.body.has(p)));
|
|
2274
2718
|
const whileLoops = new Map<Block, WhileLoopInfo>();
|
|
2275
2719
|
const doWhileLoops = new Map<Block, DoWhileInfo>();
|
|
2276
2720
|
for (const nl of forest.byHeader.values()) {
|
|
@@ -2282,32 +2726,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2282
2726
|
continue;
|
|
2283
2727
|
} // single latch only
|
|
2284
2728
|
const latch = nl.selfLoop ? h : nl.latches[0];
|
|
2285
|
-
|
|
2286
|
-
// nested — its ENTIRE body is contained in ours (a forest descendant). Structuring then recurses
|
|
2287
|
-
// naturally: when the outer body reaches the inner header, structureBlock dispatches to the inner's
|
|
2288
|
-
// own emitWhile/emitDoWhile. An OVERLAPPING loop (shared blocks, neither containing the other →
|
|
2289
|
-
// irreducible) DECLINES. If a contained inner is itself unstructurable, the outer's body
|
|
2290
|
-
// structuring loud-fails at the inner back-edge (onStack) — a safe decline, not a miscompile.
|
|
2291
|
-
if (
|
|
2292
|
-
[...forest.byHeader.keys()].some(
|
|
2293
|
-
(h2) => h2 !== h && nl.body.has(h2) && ![...forest.byHeader.get(h2)!.body].every((b) => nl.body.has(b)),
|
|
2294
|
-
)
|
|
2295
|
-
) {
|
|
2296
|
-
continue;
|
|
2297
|
-
}
|
|
2298
|
-
// Reducible entry (single-entry): every body block except the header is entered ONLY from
|
|
2299
|
-
// inside the body — no jump into the loop interior.
|
|
2300
|
-
let reducible = true;
|
|
2301
|
-
for (const bb of nl.body) {
|
|
2302
|
-
if (bb === h) {
|
|
2303
|
-
continue;
|
|
2304
|
-
}
|
|
2305
|
-
if ((preds.get(bb) ?? []).some((p) => !nl.body.has(p))) {
|
|
2306
|
-
reducible = false;
|
|
2307
|
-
break;
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
if (!reducible) {
|
|
2729
|
+
if (!admissible(nl)) {
|
|
2311
2730
|
continue;
|
|
2312
2731
|
}
|
|
2313
2732
|
|
|
@@ -2326,7 +2745,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2326
2745
|
// whose result also feeds the body would be evaluated twice per iteration). A `load` is fine —
|
|
2327
2746
|
// but NOT a materialized one: its temp assignment renders only via sideEffects(), which a
|
|
2328
2747
|
// condition-only header never emits, so its uses would read an unassigned variable.
|
|
2329
|
-
const headerPure = !h.ops.some((op) =>
|
|
2748
|
+
const headerPure = !h.ops.some((op) => effectful(op) || materialize.has(op));
|
|
2330
2749
|
|
|
2331
2750
|
let exitFrom: Block,
|
|
2332
2751
|
exit: Block,
|
|
@@ -2347,11 +2766,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2347
2766
|
continue; // neither a clean pre-tested nor bottom-tested single-exit shape
|
|
2348
2767
|
}
|
|
2349
2768
|
// Single loop exit (ret-aware): the chosen exit is the ONE real exit; every OTHER edge leaving
|
|
2350
|
-
// the body must be an early `return` (`earlyReturnArm`)
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
2353
|
-
//
|
|
2769
|
+
// the body must be an early `return` (`earlyReturnArm`), a ret-terminated target, or — for a
|
|
2770
|
+
// `while` — a `break`: an edge landing on the header's own exit, which C spells by leaving the
|
|
2771
|
+
// loop and running the exit region once, after it. A second exit that lands on any OTHER live
|
|
2772
|
+
// merge has no single-level spelling → decline, as does a `break` edge out of a NESTED loop's
|
|
2773
|
+
// body (a two-level exit). A `do-while` takes no break: an edge into its exit that no arm owns
|
|
2774
|
+
// leaves the loop to `while (1)`, whose breaks carry their own copies, where the do-while's
|
|
2775
|
+
// would fall into the latch's. An edge to the header's exit is a break whatever the exit holds:
|
|
2776
|
+
// the asm runs ONE copy of the exit region for both edges, and agbcc and mwcc keep a
|
|
2777
|
+
// source-duplicated return tail duplicated, so copying the tail into the arm spells another
|
|
2778
|
+
// object. Where that exit ends in a return, the edge is also an arm or a ret target, and emission
|
|
2779
|
+
// copies the tail where the break spelling refuses. The arms and breaks are kept: emission needs
|
|
2780
|
+
// to know which edges out of the body end an iteration rather than continue it.
|
|
2354
2781
|
const arms: LoopArm[] = [];
|
|
2782
|
+
const breaks = new Set<Block>();
|
|
2355
2783
|
let singleExit = true;
|
|
2356
2784
|
for (const e of nl.exitEdges) {
|
|
2357
2785
|
if (e.from === exitFrom && e.to === exit) {
|
|
@@ -2360,7 +2788,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2360
2788
|
const owned = earlyReturnArm({ dom, reachFrom }, e.from, e.to, nl.body, exit);
|
|
2361
2789
|
if (owned) {
|
|
2362
2790
|
arms.push({ from: e.from, to: e.to, owned });
|
|
2363
|
-
}
|
|
2791
|
+
}
|
|
2792
|
+
if (kind === 'while' && e.to === exit && !innerBodyHas(nl, e.from)) {
|
|
2793
|
+
breaks.add(e.from);
|
|
2794
|
+
} else if (!owned && (!isRet(e.to) || (kind === 'dowhile' && e.to === exit))) {
|
|
2364
2795
|
singleExit = false;
|
|
2365
2796
|
break;
|
|
2366
2797
|
}
|
|
@@ -2378,12 +2809,67 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2378
2809
|
forwardPreds: nl.forwardPreds,
|
|
2379
2810
|
body: nl.body,
|
|
2380
2811
|
arms,
|
|
2812
|
+
breaks,
|
|
2381
2813
|
});
|
|
2382
2814
|
} else {
|
|
2383
2815
|
doWhileLoops.set(h, { header: h, latch, exit, forwardPreds: nl.forwardPreds, body: nl.body, arms });
|
|
2384
2816
|
}
|
|
2385
2817
|
}
|
|
2386
2818
|
|
|
2819
|
+
// A loop none of the shapes above spells — several latches with a conditional `continue` in
|
|
2820
|
+
// mid-body beside the bottom one, or one latch under a header that computes before it tests (a
|
|
2821
|
+
// mid-tested loop) — is `while (1)`: every edge back to the header is the
|
|
2822
|
+
// implicit continue at the foot of its region, and every edge out is a `break` to the one exit
|
|
2823
|
+
// chosen below or an early `return`. Same fail-closed preconditions as above: `admissible`, and
|
|
2824
|
+
// no break out of an inner loop's body. The exit is the first target under which every other edge
|
|
2825
|
+
// out is an early-return arm or lands on a `ret` block with no effect in it, trying first, in
|
|
2826
|
+
// block order, the targets laid out after the loop's last body block: a compiler places the code
|
|
2827
|
+
// after a loop there and an early `return` inside the loop's span, so block order alone would take
|
|
2828
|
+
// that `return` for the break and the code after the loop for an early return.
|
|
2829
|
+
const foreverLoops = new Map<Block, ForeverLoopInfo>();
|
|
2830
|
+
const foreverShape = (nl: NaturalLoop): ForeverLoopInfo | null => {
|
|
2831
|
+
const last = Math.max(...[...nl.body].map((b) => fn.blocks.indexOf(b)));
|
|
2832
|
+
const rank = (x: Block) => fn.blocks.indexOf(x) + (fn.blocks.indexOf(x) > last ? 0 : fn.blocks.length);
|
|
2833
|
+
const targets = [...new Set(nl.exitEdges.map((e) => e.to))].sort((x, y) => rank(x) - rank(y));
|
|
2834
|
+
for (const exit of targets) {
|
|
2835
|
+
const arms: LoopArm[] = [];
|
|
2836
|
+
const breaks = new Set<Block>();
|
|
2837
|
+
const fits = nl.exitEdges.every((e) => {
|
|
2838
|
+
if (e.to === exit) {
|
|
2839
|
+
breaks.add(e.from);
|
|
2840
|
+
return !innerBodyHas(nl, e.from);
|
|
2841
|
+
}
|
|
2842
|
+
const owned = earlyReturnArm({ dom, reachFrom }, e.from, e.to, nl.body, exit);
|
|
2843
|
+
if (owned) {
|
|
2844
|
+
arms.push({ from: e.from, to: e.to, owned });
|
|
2845
|
+
}
|
|
2846
|
+
// a `ret` target the edge does not own is copied into the body where the edge leaves, so it
|
|
2847
|
+
// must hold nothing a second copy would run twice in the source
|
|
2848
|
+
return owned !== null || (isRet(e.to) && !e.to.ops.some(effectful));
|
|
2849
|
+
});
|
|
2850
|
+
if (fits) {
|
|
2851
|
+
return {
|
|
2852
|
+
header: nl.header,
|
|
2853
|
+
latches: nl.latches,
|
|
2854
|
+
forwardPreds: nl.forwardPreds,
|
|
2855
|
+
exit,
|
|
2856
|
+
body: nl.body,
|
|
2857
|
+
arms,
|
|
2858
|
+
breaks,
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return null;
|
|
2863
|
+
};
|
|
2864
|
+
for (const nl of forest.byHeader.values()) {
|
|
2865
|
+
const h = nl.header;
|
|
2866
|
+
const shape =
|
|
2867
|
+
!loops.has(h) && !whileLoops.has(h) && !doWhileLoops.has(h) && admissible(nl) ? foreverShape(nl) : null;
|
|
2868
|
+
if (shape !== null) {
|
|
2869
|
+
foreverLoops.set(h, shape);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2387
2873
|
// --- coalesce SSA values to variable names ---
|
|
2388
2874
|
const varName = new Map<Value, string>();
|
|
2389
2875
|
const varType = new Map<string, IrType>();
|
|
@@ -2407,6 +2893,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2407
2893
|
* which is the chain-rooted widening rather than this rule. */
|
|
2408
2894
|
const paramSeededMerges = new Set<Value>();
|
|
2409
2895
|
const backArgName = new Map<Value, string>();
|
|
2896
|
+
/** The loop header each `backArgName` entry was seeded by. */
|
|
2897
|
+
const backArgHeader = new Map<Value, Block>();
|
|
2410
2898
|
// The C static type of a rendered expression, over the declared variable types — what decides
|
|
2411
2899
|
// whether a memory access's base may be dereferenced as spelled (memAccess/arrayAccess).
|
|
2412
2900
|
const vtEnv = (n: string): IrType | undefined => varType.get(n);
|
|
@@ -2442,18 +2930,34 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2442
2930
|
if (t === undefined) {
|
|
2443
2931
|
return value;
|
|
2444
2932
|
}
|
|
2445
|
-
// ──
|
|
2933
|
+
// ── pointer values whose type this side of the program does not own ────────────────────────
|
|
2446
2934
|
// A `gaddr` at least states a type the IR knows. `gSym.pBuf` and a pointer global's own value
|
|
2447
|
-
// state one only the
|
|
2448
|
-
// `undefined`, so the test above cannot see them at all. Assigning one
|
|
2449
|
-
// temp's type and the project's declaration of that pointer are the
|
|
2450
|
-
// here established: the project's header says
|
|
2451
|
-
// says `struct Struct0 *`, and `-Werror` makes
|
|
2452
|
-
// pasted into. The destination's type is the one
|
|
2453
|
-
// map-declared cells below it can be named exactly rather
|
|
2454
|
-
// holds for an INTEGER temp too, and there the diagnostic
|
|
2455
|
-
// integer from pointer without a cast` — same site, same
|
|
2456
|
-
|
|
2935
|
+
// (isPtrValue) state one only the project's header knows, and `ctype` — which types params and
|
|
2936
|
+
// locals — reads them as `undefined`, so the test above cannot see them at all. Assigning one
|
|
2937
|
+
// bare declares that the temp's type and the project's declaration of that pointer are the
|
|
2938
|
+
// same type, which nothing here established: the project's header says
|
|
2939
|
+
// `struct Unk_03005284 *` where the recovered temp says `struct Struct0 *`, and `-Werror` makes
|
|
2940
|
+
// the mismatch fatal in the tree the source is pasted into. The destination's type is the one
|
|
2941
|
+
// this pass DID choose, so unlike the map-declared cells below it can be named exactly rather
|
|
2942
|
+
// than defused through `void *`. That holds for an INTEGER temp too, and there the diagnostic
|
|
2943
|
+
// is the mirror one, `assignment makes integer from pointer without a cast` — same site, same
|
|
2944
|
+
// argument, same `(T)` answer.
|
|
2945
|
+
//
|
|
2946
|
+
// A value that RENDERS a pointer of another type is the same assignment with its type in plain
|
|
2947
|
+
// sight: the byte arithmetic below spells `(u8 *)gPtr + 3544` and `(u8 *)a0 + 2` for the
|
|
2948
|
+
// address alone, and a `u16 *` or `s32 *` temp takes neither without the cast.
|
|
2949
|
+
//
|
|
2950
|
+
// KNOWN GAP: a global no declaration types, read at offset 0 only, may be declared an ARRAY
|
|
2951
|
+
// (`u16 *gArr[4]`, the asm reading `gArr[0]`). Its value spelling is then the array's address,
|
|
2952
|
+
// and this cast compiles that wrong value with no diagnostic, where the bare value keeps
|
|
2953
|
+
// agbcc's `incompatible pointer type`, fatal under -Werror. The cast is load-bearing all the
|
|
2954
|
+
// same: a substitution variation (`/unmerge`) carries the temp's value into the arithmetic it
|
|
2955
|
+
// feeds, and bare, that arithmetic scales by the declared pointee. A pun, `*(T *)&gArr`, is no
|
|
2956
|
+
// fix: where the declaration is not exactly T it loses gcc's fold of the read (agbcc
|
|
2957
|
+
// c-typeck.c), it drops a `volatile` declaration's qualifier, and agbcc's strict aliasing at
|
|
2958
|
+
// -O2 moves it past a store through the declared type.
|
|
2959
|
+
const vt = ctype(value);
|
|
2960
|
+
if (isPtrValue(value) || (vt?.kind === 'ptr' && !typeEquals(vt, t))) {
|
|
2457
2961
|
return { k: 'cast', to: t, e: value };
|
|
2458
2962
|
}
|
|
2459
2963
|
if (t.kind !== 'ptr' || value.k !== 'addr') {
|
|
@@ -2580,7 +3084,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2580
3084
|
}
|
|
2581
3085
|
return { carrierLive, carrierWritten };
|
|
2582
3086
|
};
|
|
2583
|
-
const canTakeName = (p: Value, B: Block, name: string, pureAlias = false): boolean => {
|
|
3087
|
+
const canTakeName = (p: Value, B: Block, name: string, pureAlias = false, ownName = true): boolean => {
|
|
2584
3088
|
const lin = liveIn.get(B)!;
|
|
2585
3089
|
let scan: ReturnType<typeof carrierScan> | undefined;
|
|
2586
3090
|
const scanned = (): ReturnType<typeof carrierScan> => (scan ??= carrierScan(p, B, name));
|
|
@@ -2593,24 +3097,67 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2593
3097
|
// computes 10 — agbcc emits exactly that asm, so this is not a generated-IR curiosity. The
|
|
2594
3098
|
// walk stops at any value with a name of its own (it reads THAT name) and at a materialized
|
|
2595
3099
|
// def (it is assigned at its own position, which the clause above already judges).
|
|
2596
|
-
|
|
3100
|
+
//
|
|
3101
|
+
// `viaBackArg` asks the `back-arg-live` question instead: the walk is the same, and what it
|
|
3102
|
+
// looks for is a loop's back-edge argument carrying `name` through `backArgName`, so that the two
|
|
3103
|
+
// gates' evidence stays disjoint and each one's ablation is its own claim.
|
|
3104
|
+
// The exit regions that render a back-edge argument AS its loop variable's name are the ones
|
|
3105
|
+
// under the loop's substitution — a self-loop's and a do-while's (`withSub`). A test-at-top
|
|
3106
|
+
// `while` installs none: past it the argument is re-derived from its operands, which the plain
|
|
3107
|
+
// walk judges.
|
|
3108
|
+
const rendersAsBackArgName = (w: Value): boolean => {
|
|
3109
|
+
const h = backArgHeader.get(w);
|
|
3110
|
+
return h !== undefined && (loops.has(h) || doWhileLoops.has(h));
|
|
3111
|
+
};
|
|
3112
|
+
const reDerives = (w: Value, seen: Set<Value>, viaBackArg: boolean): boolean => {
|
|
2597
3113
|
if (w === p || seen.has(w)) {
|
|
2598
3114
|
return false;
|
|
2599
3115
|
}
|
|
2600
3116
|
seen.add(w);
|
|
2601
3117
|
const nm = varName.get(w);
|
|
2602
3118
|
if (nm !== undefined) {
|
|
2603
|
-
return nm === name;
|
|
3119
|
+
return !viaBackArg && nm === name;
|
|
3120
|
+
}
|
|
3121
|
+
if (viaBackArg && backArgName.get(w) === name && rendersAsBackArgName(w)) {
|
|
3122
|
+
return true;
|
|
2604
3123
|
}
|
|
2605
3124
|
const d = defs.get(w);
|
|
2606
3125
|
if (!d || materialize.has(d)) {
|
|
2607
3126
|
return false;
|
|
2608
3127
|
}
|
|
2609
|
-
return d.operands.some((o) => reDerives(o, seen));
|
|
3128
|
+
return d.operands.some((o) => reDerives(o, seen, viaBackArg));
|
|
3129
|
+
};
|
|
3130
|
+
// `back-arg-live` looks where the in-edge copies LAND, which is more than `B`'s entry when the
|
|
3131
|
+
// edge leaves a loop from its latch: the do-while and self-loop emitters sink that exit copy into
|
|
3132
|
+
// the body, so what is live into that loop's header is live at the copy. A predecessor all of
|
|
3133
|
+
// whose edges hand the slot a value already under `name` writes nothing. The sink gate
|
|
3134
|
+
// (`dest-free-inside-loop`) refuses an unnamed value that renders the name through `varName`, but
|
|
3135
|
+
// not one that renders it as an earlier loop's back-edge argument, so this rule is that case's
|
|
3136
|
+
// only guard. `re-derives` is the sink gate's there, and reads `B`'s entry alone.
|
|
3137
|
+
let landed: Set<Value> | undefined;
|
|
3138
|
+
const landing = (): Set<Value> => {
|
|
3139
|
+
if (landed) {
|
|
3140
|
+
return landed;
|
|
3141
|
+
}
|
|
3142
|
+
landed = new Set(lin);
|
|
3143
|
+
const slot = B.params.indexOf(p);
|
|
3144
|
+
const writes = new Map<Block, boolean>();
|
|
3145
|
+
for (const { pred, succ } of inEdgeRecords(preds, B)) {
|
|
3146
|
+
writes.set(pred, (writes.get(pred) ?? false) || varName.get(succ.args[slot]) !== name);
|
|
3147
|
+
}
|
|
3148
|
+
for (const [pr, w] of writes) {
|
|
3149
|
+
for (const s of w ? successorsOf(pr) : []) {
|
|
3150
|
+
if (forest.byHeader.get(s)?.body.has(pr)) {
|
|
3151
|
+
liveIn.get(s)!.forEach((v) => landed!.add(v));
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
return landed;
|
|
2610
3156
|
};
|
|
2611
3157
|
return (
|
|
2612
3158
|
firstRejection(hooks.carrierNameGates ?? CARRIER_NAME_GATES, {
|
|
2613
3159
|
pureAlias,
|
|
3160
|
+
ownName,
|
|
2614
3161
|
widthDiffers: carrierWidth(varType.get(name)) !== carrierWidth(p.type),
|
|
2615
3162
|
signDiffers: carrierSign(varType.get(name)) !== carrierSign(p.type),
|
|
2616
3163
|
siblingHolds: B.params.some((q) => q !== p && varName.get(q) === name),
|
|
@@ -2624,7 +3171,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2624
3171
|
// NAMED live values are `carrier-live`'s, so the two rules stay disjoint and each one's
|
|
2625
3172
|
// ablation is its own claim. The walk itself still crosses into a named OPERAND, which
|
|
2626
3173
|
// is the whole point of it.
|
|
2627
|
-
return [...lin].some((w) => !varName.has(w) && reDerives(w, new Set()));
|
|
3174
|
+
return [...lin].some((w) => !varName.has(w) && reDerives(w, new Set(), false));
|
|
3175
|
+
},
|
|
3176
|
+
get backArgReadsName() {
|
|
3177
|
+
return [...landing()].some((w) => !varName.has(w) && reDerives(w, new Set(), true));
|
|
2628
3178
|
},
|
|
2629
3179
|
}) === null
|
|
2630
3180
|
);
|
|
@@ -2790,6 +3340,42 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2790
3340
|
const nm = a === undefined ? undefined : varName.get(a);
|
|
2791
3341
|
return refused === null && nm !== undefined && canTakeName(p, header, nm) ? nm : undefined;
|
|
2792
3342
|
};
|
|
3343
|
+
/** Where in `header` an op reading a loop variable RENDERS, for the in-place adoption below: at
|
|
3344
|
+
* its own index when it is a statement, at the consumer it inlines into otherwise (`emitPos`) —
|
|
3345
|
+
* `%t = v1 - v0` ahead of the adopted def, inlined into a call after it, reads the name after
|
|
3346
|
+
* the assignment has overwritten it. A tree the header's TERMINATOR carries renders there, at the
|
|
3347
|
+
* bottom, with one exception: a self-loop's EXIT arg renders at its ROOT's own index, which is
|
|
3348
|
+
* where a sunk exit copy is rebuilt (`preUpdateCopyHome`), while a kept one renders after the
|
|
3349
|
+
* loop, where the hazard checks already count the in-place write (`loopWriteSet`). A tree the
|
|
3350
|
+
* back edge also carries is an update copy, and renders at the bottom. So a read never renders
|
|
3351
|
+
* EARLIER than its SSA position says, and the rule only ever refuses more. Null — refuse — where
|
|
3352
|
+
* the read renders in several places or outside the header, or in an exit arg with no home. */
|
|
3353
|
+
const readRenderIdx = (op: Op, header: Block): number | null => {
|
|
3354
|
+
const pos = emitPos(op);
|
|
3355
|
+
if (pos === null || pos.blk !== header) {
|
|
3356
|
+
return null;
|
|
3357
|
+
}
|
|
3358
|
+
const term = header.ops[header.ops.length - 1];
|
|
3359
|
+
if (pos.idx !== opIndex.get(term) || !term.successors.some((sc) => sc.block === header)) {
|
|
3360
|
+
return pos.idx;
|
|
3361
|
+
}
|
|
3362
|
+
for (let cur = op; cur !== term;) {
|
|
3363
|
+
const cons = [...new Set((useSitesOf.get(cur.results[0]) ?? []).map((u) => u.op))];
|
|
3364
|
+
if (cons.length !== 1) {
|
|
3365
|
+
return null;
|
|
3366
|
+
}
|
|
3367
|
+
if (cons[0] === term) {
|
|
3368
|
+
const root = cur.results[0];
|
|
3369
|
+
if (term.successors.some((sc) => sc.block === header && sc.args.includes(root))) {
|
|
3370
|
+
return pos.idx;
|
|
3371
|
+
}
|
|
3372
|
+
const home = preUpdateCopyHome(defs, opBlock, root, header);
|
|
3373
|
+
return home === null ? null : opIndex.get(home)!;
|
|
3374
|
+
}
|
|
3375
|
+
cur = cons[0];
|
|
3376
|
+
}
|
|
3377
|
+
return pos.idx;
|
|
3378
|
+
};
|
|
2793
3379
|
// ONE seeding routine for self-loop and structured-loop headers. On a coalesceLoopInit target,
|
|
2794
3380
|
// keep the induction variable in its entry (forward-edge) value's register — reproducing a
|
|
2795
3381
|
// compiler that mutates the arg register across the loop instead of copying to a fresh local,
|
|
@@ -2823,7 +3409,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2823
3409
|
// param sits at-or-before the def in the header (the def's own operands included): the
|
|
2824
3410
|
// assignment splits the iteration into old-value-before / new-value-after, and a later
|
|
2825
3411
|
// read of the OLD value would silently get the new one — position-granular, because
|
|
2826
|
-
// liveness is block-granular and the param is killed from its own block's liveIn.
|
|
3412
|
+
// liveness is block-granular and the param is killed from its own block's liveIn. And the
|
|
3413
|
+
// position is where the read RENDERS, not where SSA put it (`readRenderIdx`).
|
|
2827
3414
|
if (name === undefined) {
|
|
2828
3415
|
const ba = backArgs?.[i];
|
|
2829
3416
|
const d = ba !== undefined ? defs.get(ba) : undefined;
|
|
@@ -2834,7 +3421,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2834
3421
|
d !== undefined &&
|
|
2835
3422
|
materialize.has(d) &&
|
|
2836
3423
|
opBlock.get(d) === header &&
|
|
2837
|
-
(useSitesOf.get(p) ?? []).every((s) =>
|
|
3424
|
+
(useSitesOf.get(p) ?? []).every((s) => {
|
|
3425
|
+
const at = s.op === d ? s.idx : readRenderIdx(s.op, header);
|
|
3426
|
+
return at !== null && at <= opIndex.get(d)!;
|
|
3427
|
+
}) &&
|
|
2838
3428
|
canTakeName(p, header, nm)
|
|
2839
3429
|
) {
|
|
2840
3430
|
name = nm;
|
|
@@ -2858,6 +3448,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2858
3448
|
// agbcc compiles to an unconditional branch.
|
|
2859
3449
|
if (backArgs) {
|
|
2860
3450
|
backArgName.set(backArgs[i], varName.get(p)!);
|
|
3451
|
+
backArgHeader.set(backArgs[i], header);
|
|
2861
3452
|
}
|
|
2862
3453
|
});
|
|
2863
3454
|
};
|
|
@@ -2875,16 +3466,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2875
3466
|
// (agbcc copies the init to a new reg). Never reuse a name already taken by a SIMULTANEOUSLY-LIVE
|
|
2876
3467
|
// sibling header param — two loop-carried values seeded from one source must not collapse (a silent
|
|
2877
3468
|
// clobber). The latch's back-edge arg carries the param's name so the loop update assigns it.
|
|
3469
|
+
// A `while (1)` is seeded the same way, and its back-edge args are its latches' when they all hand
|
|
3470
|
+
// the header the same ones: latches that differ leave the param a name of its own, and each
|
|
3471
|
+
// back edge a copy into it. Seeding is what keeps the merge naming below from handing the param a
|
|
3472
|
+
// value the body computes while the param is still read.
|
|
3473
|
+
const onlyBackArgs = (header: Block, latches: readonly Block[]): readonly Value[] | null => {
|
|
3474
|
+
const [first, ...rest] = latches.map((l) => successorTo(l, header)?.args ?? null);
|
|
3475
|
+
return first !== null && rest.every((a) => a !== null && a.every((v, k) => v === first[k])) ? first : null;
|
|
3476
|
+
};
|
|
2878
3477
|
const structuredLoops = [
|
|
2879
|
-
...[...whileLoops.values()].map((l) => ({
|
|
3478
|
+
...[...whileLoops.values(), ...doWhileLoops.values()].map((l) => ({
|
|
2880
3479
|
header: l.header,
|
|
2881
|
-
|
|
3480
|
+
backArgs: successorTo(l.latch, l.header)?.args ?? null,
|
|
2882
3481
|
forwardPreds: l.forwardPreds,
|
|
2883
3482
|
body: l.body,
|
|
2884
3483
|
})),
|
|
2885
|
-
...[...
|
|
3484
|
+
...[...foreverLoops.values()].map((l) => ({
|
|
2886
3485
|
header: l.header,
|
|
2887
|
-
|
|
3486
|
+
backArgs: onlyBackArgs(l.header, l.latches),
|
|
2888
3487
|
forwardPreds: l.forwardPreds,
|
|
2889
3488
|
body: l.body,
|
|
2890
3489
|
})),
|
|
@@ -2913,9 +3512,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2913
3512
|
return names;
|
|
2914
3513
|
};
|
|
2915
3514
|
for (const l of structuredLoops) {
|
|
2916
|
-
const back = successorTo(l.latch, l.header);
|
|
2917
3515
|
// exclusion seeded with enclosing-loop names → never coalesce onto them
|
|
2918
|
-
seedLoopParams(l.header, l.forwardPreds,
|
|
3516
|
+
seedLoopParams(l.header, l.forwardPreds, l.backArgs, enclosingNames(l));
|
|
2919
3517
|
}
|
|
2920
3518
|
let changed = true;
|
|
2921
3519
|
while (changed) {
|
|
@@ -2933,8 +3531,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2933
3531
|
incoming.push({ v: succ.args[i], pr: pred });
|
|
2934
3532
|
}
|
|
2935
3533
|
// A redundant phi (every edge passes the SAME value) is a pure alias of it — sharing the
|
|
2936
|
-
// name is sound even while the value stays live (they are equal on every path).
|
|
2937
|
-
// waives
|
|
3534
|
+
// value's own name is sound even while the value stays live (they are equal on every path).
|
|
3535
|
+
// This waives `carrier-live` and `re-derives`; the sibling-param check always applies.
|
|
2938
3536
|
const allSame = incoming.length > 0 && incoming.every((c) => c.v === incoming[0].v);
|
|
2939
3537
|
// prefer a carrier that already has a name; then a loop var whose update this receives —
|
|
2940
3538
|
// but only one whose name survives the C3 interference check (else the edge copies into
|
|
@@ -2949,7 +3547,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2949
3547
|
...incoming.filter((c) => backArgName.has(c.v)),
|
|
2950
3548
|
]) {
|
|
2951
3549
|
const nm = varName.get(c.v) ?? backArgName.get(c.v)!;
|
|
2952
|
-
if (carriesPreUpdate(c.v, c.pr, b) || !canTakeName(p, b, nm, allSame)) {
|
|
3550
|
+
if (carriesPreUpdate(c.v, c.pr, b) || !canTakeName(p, b, nm, allSame, varName.has(c.v))) {
|
|
2953
3551
|
continue;
|
|
2954
3552
|
}
|
|
2955
3553
|
// `freshParamMerge` (the `/fresh-merge` variation): this merge takes its own home rather
|
|
@@ -3288,6 +3886,26 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3288
3886
|
return { k: 'var', name: '?' };
|
|
3289
3887
|
};
|
|
3290
3888
|
|
|
3889
|
+
// A qualified access is one the recompile must make where the machine did, so its spelling
|
|
3890
|
+
// carries the qualifier or the function declines: a plain spelling is one agbcc may delete or
|
|
3891
|
+
// hoist. It carries it where ir/discipline.ts `qualifiedBy` says: on its cast, or through the
|
|
3892
|
+
// object's declaration and on its cast where it reaches the object through one, since a cast's
|
|
3893
|
+
// pointee drops the declaration's qualifiers.
|
|
3894
|
+
const pinnedAccess = (op: Op, access: Expr): Expr => {
|
|
3895
|
+
const by = qualifiedBy(op);
|
|
3896
|
+
if (by === null || (by === 'declaration' && !castOffDeclaration(access, ctype))) {
|
|
3897
|
+
return access;
|
|
3898
|
+
}
|
|
3899
|
+
const q = qualifiedMemoryAccess(access, ctype);
|
|
3900
|
+
if (q === null) {
|
|
3901
|
+
throw new StructureError(
|
|
3902
|
+
`cannot structure '${fn.name}': a ${placedAt(op)} ${op.opcode} that must stay volatile is reached through a ` +
|
|
3903
|
+
'pointer with no type to put the qualifier on',
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
return q;
|
|
3907
|
+
};
|
|
3908
|
+
|
|
3291
3909
|
// Lower ONE def's operation to an Expr, rendering operands through `e`. Shared between the
|
|
3292
3910
|
// inline-at-use path (exprWith) and the materialized-temp path (sideEffects), so both spell a
|
|
3293
3911
|
// given op identically.
|
|
@@ -3295,12 +3913,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3295
3913
|
if (d.opcode === 'const') {
|
|
3296
3914
|
return { k: 'const', value: d.attrs.value as number };
|
|
3297
3915
|
}
|
|
3916
|
+
if (d.opcode === 'fconst') {
|
|
3917
|
+
return { k: 'fconst', bits: d.attrs.bits as string };
|
|
3918
|
+
}
|
|
3298
3919
|
// a bitfield extract recognized over the ops (see the precompute above): the member read,
|
|
3299
3920
|
// not the shift pair
|
|
3300
3921
|
const bf = bitfieldSpelling.get(d);
|
|
3301
3922
|
if (bf) {
|
|
3302
3923
|
return { k: 'field', base: { k: 'var', name: bf.global }, name: bf.field, dot: true };
|
|
3303
3924
|
}
|
|
3925
|
+
if (FLOAT_TO_BIN[d.opcode]) {
|
|
3926
|
+
return { k: 'bin', op: FLOAT_TO_BIN[d.opcode], l: e(d.operands[0]), r: e(d.operands[1]) };
|
|
3927
|
+
}
|
|
3928
|
+
if (d.opcode === 'fneg') {
|
|
3929
|
+
return { k: 'un', op: 'f-', e: e(d.operands[0]) };
|
|
3930
|
+
}
|
|
3304
3931
|
if (CMP_TO_BIN[d.opcode]) {
|
|
3305
3932
|
// A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
|
|
3306
3933
|
// arithmetic case below (see intifyAddr): its C type comes from the PROJECT's own
|
|
@@ -3347,10 +3974,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3347
3974
|
!(provablyNonNegative(l, vtEnv) && provablyNonNegative(r, vtEnv))
|
|
3348
3975
|
) {
|
|
3349
3976
|
const irUnsigned = (v: Value): boolean => v.type.kind === 'int' && !v.type.signed;
|
|
3977
|
+
// AT THE OPERAND'S OWN RANK: `(u32)a` over a 64-bit `a` pins nothing, it truncates the
|
|
3978
|
+
// compare to its low half. `exprIntWidth` is total and answers 64 only where it proves it.
|
|
3979
|
+
const pinUnsigned = (x: Expr): Expr => ({ k: 'cast', to: T.u(exprIntWidth(x, vtEnv)), e: x });
|
|
3350
3980
|
if (!irUnsigned(d.operands[0]) && irUnsigned(d.operands[1])) {
|
|
3351
|
-
r =
|
|
3981
|
+
r = pinUnsigned(r);
|
|
3352
3982
|
} else {
|
|
3353
|
-
l =
|
|
3983
|
+
l = pinUnsigned(l);
|
|
3354
3984
|
}
|
|
3355
3985
|
}
|
|
3356
3986
|
// The SIGNED direction of the same hole, and a DEFAULT rather than an alternative of that variation —
|
|
@@ -3369,14 +3999,26 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3369
3999
|
//
|
|
3370
4000
|
// `undefined` takes the cast exactly as a definite `false` does (see
|
|
3371
4001
|
// renderedIntSignedness): a call's signedness is the project header's, not this function's.
|
|
3372
|
-
// A POINTER-rendered side
|
|
3373
|
-
//
|
|
3374
|
-
//
|
|
4002
|
+
// A POINTER-rendered side takes it too. C compares an address unsigned (`p < q` is agbcc
|
|
4003
|
+
// `bcc`, IDO `sltu`), so a signed opcode over one is a source that compared the address as
|
|
4004
|
+
// an integer, and the byte arithmetic's `(u8 *)g + K` left bare against an `int` compiles
|
|
4005
|
+
// `bcs` where the asm says `blt`.
|
|
3375
4006
|
if (/^icmp_s/.test(d.opcode)) {
|
|
3376
4007
|
const pinSigned = (x: Expr): Expr =>
|
|
3377
|
-
renderedIntSignedness(x, vtEnv) === true
|
|
4008
|
+
renderedIntSignedness(x, vtEnv) === true ? x : { k: 'cast', to: T.s(exprIntWidth(x, vtEnv)), e: x };
|
|
3378
4009
|
l = pinSigned(l);
|
|
3379
4010
|
r = pinSigned(r);
|
|
4011
|
+
} else {
|
|
4012
|
+
// A pointer-rendered side against an INTEGER one (the byte arithmetic's `(u8 *)g + K`
|
|
4013
|
+
// against an `int`) is a constraint violation IDO 7.1 and CodeWarrior reject and agbcc
|
|
4014
|
+
// warns about. The asm compared two words, and unsigned, as C compares a pointer, so the
|
|
4015
|
+
// pointer side is compared as the `u32` it is. A literal 0 is the null pointer constant.
|
|
4016
|
+
const intSide = (x: Expr): boolean => ctype(x)?.kind === 'int' && !(x.k === 'const' && x.value === 0);
|
|
4017
|
+
if (ptrSide(l) && intSide(r)) {
|
|
4018
|
+
l = { k: 'cast', to: T.u(32), e: l };
|
|
4019
|
+
} else if (ptrSide(r) && intSide(l)) {
|
|
4020
|
+
r = { k: 'cast', to: T.u(32), e: r };
|
|
4021
|
+
}
|
|
3380
4022
|
}
|
|
3381
4023
|
return { k: 'bin', op: CMP_TO_BIN[d.opcode], l, r };
|
|
3382
4024
|
}
|
|
@@ -3393,7 +4035,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3393
4035
|
// out of source order — an ldmia-fed add reads def-reordered; cross-block positions do not
|
|
3394
4036
|
// order evaluation), neither stamped `listOrder` (an ldmia-expanded load's own position is
|
|
3395
4037
|
// LIST order), both operand VALUES un-named (see below), no pointer side (load-bearing for
|
|
3396
|
-
// the stride rules below), and no effect moves (call, marker).
|
|
4038
|
+
// the stride rules below), and no effect moves (call, marker). A global's value that the
|
|
4039
|
+
// pointer-value rule below spells as a base is a pointer side too, though `ctype` types no
|
|
4040
|
+
// global: that rule keeps the add's operand order, which a swap here would undo.
|
|
4041
|
+
const ptrGlobalSide = (x: Expr): boolean =>
|
|
4042
|
+
x.k === 'var' &&
|
|
4043
|
+
mapUndeclared(x.name) &&
|
|
4044
|
+
(pointerLoadedGlobals.has(x.name) || (d.results[0]?.type.kind === 'ptr' && wordLoadedGlobals.has(x.name)));
|
|
3397
4045
|
if (COMMUTATIVE_BIN.has(ARITH_TO_BIN[d.opcode]) && d.operands.length === 2) {
|
|
3398
4046
|
const [da, db] = [defs.get(d.operands[0]), defs.get(d.operands[1])];
|
|
3399
4047
|
if (
|
|
@@ -3416,6 +4064,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3416
4064
|
activeSub?.has(d.operands[1]) !== true &&
|
|
3417
4065
|
ctype(l)?.kind !== 'ptr' &&
|
|
3418
4066
|
ctype(r)?.kind !== 'ptr' &&
|
|
4067
|
+
!ptrGlobalSide(l) &&
|
|
4068
|
+
!ptrGlobalSide(r) &&
|
|
3419
4069
|
!exprHasEffect(l) &&
|
|
3420
4070
|
!exprHasEffect(r) &&
|
|
3421
4071
|
opBlock.get(da) === opBlock.get(db) &&
|
|
@@ -3470,17 +4120,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3470
4120
|
// computed offset addresses TWICE the intended byte, and nothing downstream can see the
|
|
3471
4121
|
// error — in the sa3 decomp that address is what a `CpuSet` call writes THROUGH.
|
|
3472
4122
|
//
|
|
3473
|
-
//
|
|
3474
|
-
//
|
|
3475
|
-
//
|
|
3476
|
-
//
|
|
3477
|
-
// agbcc corpus carry one.
|
|
4123
|
+
// `ptr - ptr` is C's ELEMENT difference, where the asm subtracted bytes: agbcc compiles `(q -
|
|
4124
|
+
// p) + m` on an `int *` to `sub; asr #2; add`, and spelled `(v0 - a0) >> 2` the lift divided
|
|
4125
|
+
// by 4 twice. Both sides go byte pointers, `(u8 *)v0 - (u8 *)a0`, the byte count in every
|
|
4126
|
+
// world — unless both already are.
|
|
3478
4127
|
//
|
|
3479
|
-
//
|
|
3480
|
-
//
|
|
3481
|
-
//
|
|
3482
|
-
// real predicate is the CONSUMER, and deciding it here at the producer is what these two
|
|
3483
|
-
// branches would have to stop doing.
|
|
4128
|
+
// The inexact-CONSTANT branch above casts its base and does not cast the sum back: a deref
|
|
4129
|
+
// supplies its own cast, and a temp of another pointer type takes the sum through
|
|
4130
|
+
// intoDeclaredTemp's cast.
|
|
3484
4131
|
const walkVar = (x: Expr): IrType | undefined => {
|
|
3485
4132
|
const t = ctype(x);
|
|
3486
4133
|
return t?.kind === 'ptr' && ptrElemBytes(t.to) !== 1 ? t : undefined;
|
|
@@ -3495,7 +4142,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3495
4142
|
// the shape from them.
|
|
3496
4143
|
const lp = ctype(l)?.kind === 'ptr';
|
|
3497
4144
|
const rp = d.operands.length === 2 && ctype(r)?.kind === 'ptr';
|
|
3498
|
-
if (lp &&
|
|
4145
|
+
if (lp && rp && d.opcode === 'sub') {
|
|
4146
|
+
if (walkVar(l) || walkVar(r)) {
|
|
4147
|
+
l = bytePtr(l);
|
|
4148
|
+
r = bytePtr(r);
|
|
4149
|
+
}
|
|
4150
|
+
} else if (lp && !rp) {
|
|
3499
4151
|
restoreTo = walkVar(l);
|
|
3500
4152
|
l = restoreTo ? bytePtr(l) : l;
|
|
3501
4153
|
} else if (rp && !lp && d.opcode === 'add') {
|
|
@@ -3509,14 +4161,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3509
4161
|
// DEFINITELY-pointer rendering is cast (same conservative direction as memAccess); the
|
|
3510
4162
|
// additive ops keep C's legal pointer arithmetic untouched.
|
|
3511
4163
|
const op = ARITH_TO_BIN[d.opcode];
|
|
3512
|
-
const intify = (x: Expr): Expr =>
|
|
4164
|
+
const intify = (x: Expr): Expr =>
|
|
4165
|
+
restoredIntSum(x, false) ?? (ctype(x)?.kind === 'ptr' ? { k: 'cast', to: T.s(32), e: x } : x);
|
|
3513
4166
|
if (!['+', '-', '&&', '||'].includes(op)) {
|
|
3514
4167
|
l = intify(l);
|
|
3515
4168
|
r = intify(r);
|
|
3516
4169
|
} else if (op === '+' && ctype(l)?.kind === 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
3517
|
-
|
|
4170
|
+
// ptr + ptr is not C; ptr + (s32)ptr is, and C scales it by the left pointee. So the left
|
|
4171
|
+
// side is the base, and the sum goes back to its type as the walk's does.
|
|
4172
|
+
r = intify(r);
|
|
4173
|
+
if (!restoreTo) {
|
|
4174
|
+
restoreTo = walkVar(l);
|
|
4175
|
+
l = restoreTo ? bytePtr(l) : l;
|
|
4176
|
+
}
|
|
3518
4177
|
} else if (op === '-' && ctype(l)?.kind !== 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
3519
|
-
|
|
4178
|
+
// int - ptr is not C. A byte sum this rule made integer leaves a global it is taken from a
|
|
4179
|
+
// pointer under a pointer declaration, which the integer would scale; a word, it is the
|
|
4180
|
+
// asm's under any integer or pointer one.
|
|
4181
|
+
if (restoredIntSum(r, false) !== undefined) {
|
|
4182
|
+
l = intWords(l);
|
|
4183
|
+
}
|
|
4184
|
+
r = intify(r);
|
|
3520
4185
|
}
|
|
3521
4186
|
// A bare global address `&gSym` under ANY of these operators is never emitted as-is: its C
|
|
3522
4187
|
// type comes from the PROJECT's own declaration (unknowable here — exprCType types `addr`
|
|
@@ -3530,10 +4195,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3530
4195
|
const intifyAddr = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: T.u(32), e: x } : x);
|
|
3531
4196
|
l = intifyAddr(l);
|
|
3532
4197
|
r = intifyAddr(r);
|
|
3533
|
-
// The SAME hazard one level down, for a
|
|
3534
|
-
//
|
|
3535
|
-
//
|
|
3536
|
-
//
|
|
4198
|
+
// The SAME hazard one level down, for a pointer VALUE (`gPtr`, `gSym.pBuf` — isPtrValue):
|
|
4199
|
+
// C scales `gPtr + K` by sizeof(*gPtr) — 1 under the map's synthesized `void *`, but
|
|
4200
|
+
// whatever the PROJECT's header declares (a `u16 *` member, a 0x5C-byte struct) in the
|
|
4201
|
+
// world a user actually recompiles in. The asm added BYTES, so the honest
|
|
3537
4202
|
// spelling makes the stride explicit: CAST-THEN-ADD, `(u8 *)gPtr + K`, the same address in
|
|
3538
4203
|
// EVERY world. Add-then-cast (`(u8 *)(gPtr + K)`, what the backend's deref legalization
|
|
3539
4204
|
// would otherwise produce) is byte-correct in exactly one of them — a silent wrongness, the
|
|
@@ -3544,17 +4209,97 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3544
4209
|
// ACCESS width, a different address whenever that width is not 1.
|
|
3545
4210
|
// Under the non-additive operators C rejects a pointer outright, so there the honest
|
|
3546
4211
|
// spelling is integer math on the cell — exactly intifyAddr's `(u32)&gSym` rule.
|
|
3547
|
-
const intifyPtrValue =
|
|
4212
|
+
const intifyPtrValue = globalWord;
|
|
3548
4213
|
if (op === '+' || op === '-') {
|
|
3549
4214
|
// `ptr ± int` and `ptr - ptr` are byte arithmetic once both sides are byte pointers;
|
|
3550
|
-
// `ptr + ptr` and `int - ptr` are not C at all, so the second pointer goes integer.
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
4215
|
+
// `ptr + ptr` and `int - ptr` are not C at all, so the second pointer goes integer. The
|
|
4216
|
+
// other side is a pointer when it is a pointer value too or when it already renders one
|
|
4217
|
+
// (a pointer temp, the walk's `(u8 *)v1`), and then the global's value is the side that
|
|
4218
|
+
// goes integer.
|
|
4219
|
+
//
|
|
4220
|
+
// A global's value no declaration types, added to an integer, is a pointer value too
|
|
4221
|
+
// when the IR types the SUM a pointer: `(u8 *)g + x` is the asm's address under every
|
|
4222
|
+
// declaration of `g` once `x` renders an integer, whichever operand the source held as
|
|
4223
|
+
// the pointer. An integer side that IS an address (`(u32)&gArr + gIdx`) says the address
|
|
4224
|
+
// is the base and the global its index, so that sum stays as it is. An address under a
|
|
4225
|
+
// load (`((u16 *)&gTbl)[a0]`) is a loaded value, and that side is an integer. A global
|
|
4226
|
+
// that is no pointer value renders an integer too, though `ctype` types no global.
|
|
4227
|
+
const isAddr = (x: Expr): boolean =>
|
|
4228
|
+
x.k === 'addr' ||
|
|
4229
|
+
(x.k === 'cast' && isAddr(x.e)) ||
|
|
4230
|
+
(x.k === 'bin' && (x.op === '+' || x.op === '-') && (isAddr(x.l) || isAddr(x.r)));
|
|
4231
|
+
const intSide = (x: Expr): boolean =>
|
|
4232
|
+
!isAddr(x) && (ctype(x)?.kind === 'int' || (x.k === 'var' && ctype(x) === undefined && !isPtrValue(x)));
|
|
4233
|
+
const sumBase =
|
|
4234
|
+
d.results[0]?.type.kind === 'ptr' && !isPtrValue(l) && !isPtrValue(r)
|
|
4235
|
+
? isUndeclaredGlobalValue(l) && intSide(r)
|
|
4236
|
+
? l
|
|
4237
|
+
: op === '+' && isUndeclaredGlobalValue(r) && intSide(l)
|
|
4238
|
+
? r
|
|
4239
|
+
: undefined
|
|
4240
|
+
: undefined;
|
|
4241
|
+
const ptrValue = (x: Expr): boolean => isPtrValue(x) || x === sumBase;
|
|
4242
|
+
// A global no declaration types in the base's partner is added as an integer, which it is
|
|
4243
|
+
// under an integer declaration and a pointer one alike.
|
|
4244
|
+
if (sumBase !== undefined) {
|
|
4245
|
+
l = l !== sumBase ? intWords(l) : l;
|
|
4246
|
+
r = r !== sumBase ? intWords(r) : r;
|
|
4247
|
+
}
|
|
4248
|
+
const rendersPtr = (x: Expr): boolean => ptrValue(x) || ctype(x)?.kind === 'ptr';
|
|
4249
|
+
const bothPtr = rendersPtr(l) && rendersPtr(r);
|
|
4250
|
+
// `x + (u8 *)g` is pointer arithmetic, and gcc makes the pointer the first operand of the
|
|
4251
|
+
// add, which swaps the asm's. So under an integer left side, the value of a global no
|
|
4252
|
+
// declaration types, or a byte sum this rule made of one, is added as an integer in the
|
|
4253
|
+
// asm's order, and the cast keeps the sum the byte pointer it would have been. A declared
|
|
4254
|
+
// pointer keeps `x + (u8 *)p`, the operand the element and field spellings read.
|
|
4255
|
+
const undeclaredPtr = (x: Expr): boolean => x.k === 'var' && mapUndeclared(x.name);
|
|
4256
|
+
// The same global LEFT of a runtime offset is added as an integer too: CodeWarrior at -O4
|
|
4257
|
+
// puts the index first in every pointer sum, where an integer sum keeps the source's
|
|
4258
|
+
// order, so `(u8 *)g + x` is the asm's order on agbcc, KMC gcc and IDO only. The partner
|
|
4259
|
+
// goes integer with it, or a pointer partner would scale the sum. A constant offset folds
|
|
4260
|
+
// into the access and keeps `(u8 *)g + K`.
|
|
4261
|
+
const constValued = (x: Expr): boolean =>
|
|
4262
|
+
x.k === 'const' ||
|
|
4263
|
+
(x.k === 'cast' && constValued(x.e)) ||
|
|
4264
|
+
(x.k === 'bin' && constValued(x.l) && constValued(x.r));
|
|
4265
|
+
const intSum = op === '+' && !constValued(r) && ptrValue(l) && undeclaredPtr(l);
|
|
4266
|
+
if (intSum) {
|
|
4267
|
+
l = intifyPtrValue(l);
|
|
4268
|
+
r = ptrValue(r)
|
|
4269
|
+
? intifyPtrValue(r)
|
|
4270
|
+
: ctype(r)?.kind === 'ptr'
|
|
4271
|
+
? isByteGlobalSum(r)
|
|
4272
|
+
? byteSumAsInt(r)
|
|
4273
|
+
: { k: 'cast', to: T.u(32), e: r }
|
|
4274
|
+
: intWords(r);
|
|
4275
|
+
restoreTo ??= T.ptr(T.u(8));
|
|
4276
|
+
} else if (ptrValue(l)) {
|
|
4277
|
+
l = op === '+' && bothPtr && !ptrValue(r) ? intifyPtrValue(l) : bytePtr(l);
|
|
3554
4278
|
}
|
|
3555
|
-
if (
|
|
4279
|
+
if (intSum) {
|
|
4280
|
+
// both operands are integers now
|
|
4281
|
+
} else if (
|
|
4282
|
+
op === '+' &&
|
|
4283
|
+
!bothPtr &&
|
|
4284
|
+
!restoreTo &&
|
|
4285
|
+
((ptrValue(r) && undeclaredPtr(r)) || (ctype(r)?.kind === 'ptr' && isByteGlobalSum(r, true)))
|
|
4286
|
+
) {
|
|
4287
|
+
r = ptrValue(r) ? intifyPtrValue(r) : byteSumAsInt(r);
|
|
4288
|
+
// A left global no declaration types may be a pointer in the project's header, which
|
|
4289
|
+
// would scale the integer sum; as an integer it is the asm's word under any declaration.
|
|
4290
|
+
l = intWords(l);
|
|
4291
|
+
restoreTo = T.ptr(T.u(8));
|
|
4292
|
+
} else if (ptrValue(r)) {
|
|
3556
4293
|
r = bothPtr && op === '-' ? bytePtr(r) : op === '+' && !bothPtr ? bytePtr(r) : intifyPtrValue(r);
|
|
3557
4294
|
}
|
|
4295
|
+
// A byte sum less an integer sum of globals no declaration types: bare, `gB2 - gB3` is an
|
|
4296
|
+
// element count under a wider pointer declaration of them, where the asm subtracted bytes.
|
|
4297
|
+
if (op === '-' && r.k === 'bin' && !rendersPtr(r) && isByteGlobalSum(l)) {
|
|
4298
|
+
r = intWords(r);
|
|
4299
|
+
}
|
|
4300
|
+
if (op === '-' && ctype(l)?.kind === 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
4301
|
+
restoreTo = undefined; // a byte pointer less a byte pointer is the byte count, an integer
|
|
4302
|
+
}
|
|
3558
4303
|
} else if (op !== '&&' && op !== '||') {
|
|
3559
4304
|
// (`&&`/`||` take a pointer operand legally — a truth test, no arithmetic.)
|
|
3560
4305
|
l = isPtrValue(l) ? intifyPtrValue(l) : l;
|
|
@@ -3563,10 +4308,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3563
4308
|
// (The signedness-carrying pairs stay DISTINCT ops here — `>>>`/`>>` and `/u` `%u`/`/` `%`.
|
|
3564
4309
|
// Which token a language spells each with, and what cast pins the choice, is a BACKEND
|
|
3565
4310
|
// decision; see l3/ast.ts BinOp and backend/cfamily.ts's C_SPELLING.)
|
|
3566
|
-
// SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer
|
|
3567
|
-
// COMPARISON (`gPtr < K`
|
|
3568
|
-
//
|
|
3569
|
-
// today, so closing it would churn spellings for a signedness case no row exercises.
|
|
4311
|
+
// SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer value under a
|
|
4312
|
+
// COMPARISON (`gPtr < K`, `(u8 *)gPtr + K < a0`) needs nothing here: an icmp_s* pins every
|
|
4313
|
+
// operand that does not provably render signed, a pointer included (pinSigned above).
|
|
3570
4314
|
//
|
|
3571
4315
|
// A `+`/`|` over two IR `const`s that both RENDER as literals is the literal it is. After
|
|
3572
4316
|
// pre-recovery there is only one way such an op still exists: `raise/const.ts` refuses to fold
|
|
@@ -3666,13 +4410,122 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3666
4410
|
? { k: 'cast', to: T.int(w, d.opcode === 'sext'), e: e(d.operands[0]) }
|
|
3667
4411
|
: mkGap(`no C type for a ${w}-bit '${d.opcode}'`, [e(d.operands[0])]);
|
|
3668
4412
|
}
|
|
4413
|
+
// The recovered integer type of a value, where it has one. `lo32`/`hi32`/`concat` all cast to
|
|
4414
|
+
// one, and a value recovery left as anything else has no cast to spell.
|
|
4415
|
+
const asInt = (t: IrType) => (t.kind === 'int' ? t : undefined);
|
|
4416
|
+
// THE TWO HALVES OF A 64-BIT VALUE, which C spells exactly: the low half is a truncating cast,
|
|
4417
|
+
// the high half a shift and then the same cast. The RESULT's own recovered type is what the
|
|
4418
|
+
// cast is to, and the shift's kind comes from the 64-bit operand's — `>>` over a signed value
|
|
4419
|
+
// and `>>>` over an unsigned one is what the machine did, and what the C backend's operand pin
|
|
4420
|
+
// is spelled in terms of.
|
|
4421
|
+
if (d.opcode === 'lo32' || d.opcode === 'hi32') {
|
|
4422
|
+
const src = e(d.operands[0]);
|
|
4423
|
+
const half = asInt(d.results[0].type) ?? T.u(32);
|
|
4424
|
+
// The LOW half is a truncating cast and needs nothing of its operand's rank: `(u32)x` and
|
|
4425
|
+
// `(u32)(s64)x` are the same value whatever `x` renders as.
|
|
4426
|
+
if (d.opcode === 'lo32') {
|
|
4427
|
+
return { k: 'cast', to: half, e: src };
|
|
4428
|
+
}
|
|
4429
|
+
// THE HIGH HALF SHIFTS BY 32, AND `x >> 32` IS UNDEFINED IN C WHERE `x` RENDERS 32 BITS
|
|
4430
|
+
// WIDE — the shift count must be below the promoted left operand's width. The 64-bit VALUE
|
|
4431
|
+
// is what this op projects, but the rendered EXPRESSION is a separate question and the two
|
|
4432
|
+
// can part, so the operand is made to carry the rank the shift reads by a cast to the
|
|
4433
|
+
// value's own recovered 64-bit type.
|
|
4434
|
+
//
|
|
4435
|
+
// THE SIGNEDNESS IS A SEPARATE PIN AND IS NOT MADE HERE. `>>` versus `>>>` is read off the
|
|
4436
|
+
// 64-bit operand's recovered type, and what the printed operand renders as is decided one
|
|
4437
|
+
// level down by backend/cfamily.ts `pinnedOperands` — which casts whenever the operand does
|
|
4438
|
+
// not PROVABLY render as the operator needs, and a call never does. So the cast this arm may
|
|
4439
|
+
// add carries the rank alone; claiming it carries both would be a claim about a decision
|
|
4440
|
+
// this file does not make.
|
|
4441
|
+
const whole = asInt(d.operands[0].type);
|
|
4442
|
+
const wide: Expr | undefined =
|
|
4443
|
+
exprIntWidth(src, vtEnv) === 64 ? src : whole === undefined ? undefined : { k: 'cast', to: whole, e: src };
|
|
4444
|
+
// A 64-BIT VALUE WHOSE RECOVERY IS NOT AN INTEGER, AND WHOSE RENDERING DOES NOT ALREADY
|
|
4445
|
+
// CARRY THE RANK, has no 64-bit type to cast to and no legal shift to spell, so it is a GAP
|
|
4446
|
+
// rather than the undefined C it would otherwise print — the same loud floor the un-nameable
|
|
4447
|
+
// `sext` width gets two arms above. The condition is the CONJUNCTION and the refusal states
|
|
4448
|
+
// it as one: neither half alone is a reason. Nothing in the corpus reaches it: every `hi32`
|
|
4449
|
+
// is built beside a `concat` or a pair return whose recovery is an integer, and a recovery
|
|
4450
|
+
// that is not one stops at the `concat` gap below before a projection of it is rendered.
|
|
4451
|
+
if (wide === undefined) {
|
|
4452
|
+
// "UPPER HALF", NOT "HIGH HALF", for the reason the split-pair refusal in thumb.ts gives at
|
|
4453
|
+
// its own throw: `apps/web`'s `reloc-halves` decline class holds `/high half/`, its list is
|
|
4454
|
+
// ORDERED, and a 64-bit gap published as a relocation gap because one entry comes first is
|
|
4455
|
+
// a classification nothing states. `wide-call-arg` claims this phrase BY NAME
|
|
4456
|
+
// (`/no upper half to shift out/`) — a 64-bit gap with no class at all renders as
|
|
4457
|
+
// "Other / unclassified", which is the same wrong answer arrived at from the other side.
|
|
4458
|
+
return mkGap(
|
|
4459
|
+
'a 64-bit value that neither renders 64 bits wide nor has an integer type has no upper half to shift out',
|
|
4460
|
+
[src],
|
|
4461
|
+
);
|
|
4462
|
+
}
|
|
4463
|
+
return {
|
|
4464
|
+
k: 'cast',
|
|
4465
|
+
to: half,
|
|
4466
|
+
e: { k: 'bin', op: whole?.signed ? '>>' : '>>>', l: wide, r: { k: 'const', value: 32 } },
|
|
4467
|
+
};
|
|
4468
|
+
}
|
|
4469
|
+
// …AND THE WIDEN THAT BUILDS ONE, which the machine spells as a PAIR rather than as a cast —
|
|
4470
|
+
// `asr rN,rM,#31` for the signed extension of a word, `mov rN,#0` for the unsigned one. So the
|
|
4471
|
+
// shape is what says HOW the half was widened, and the cast this renders is to whatever 64-bit
|
|
4472
|
+
// type recovery gave the value.
|
|
4473
|
+
//
|
|
4474
|
+
// THE WORD'S OWN SIGNEDNESS IS WHAT THE WIDEN MEANS, and it is pinned the way every other
|
|
4475
|
+
// signedness-carrying operand in this function is: cast only where the operand does not
|
|
4476
|
+
// already render that way. Leaving the signed arm to whatever `x` happens to render as reads
|
|
4477
|
+
// the widen off the DECLARATION rather than off the `asr` — under `/unsigned` the parameter is
|
|
4478
|
+
// declared `u32` and `(s64)x` then says zero-extend, against an instruction that says
|
|
4479
|
+
// otherwise.
|
|
4480
|
+
//
|
|
4481
|
+
// A `concat` of anything else is a 64-bit value this pipeline has no C spelling for, and it
|
|
4482
|
+
// falls through to the loud gap at the bottom — which is the whole safety story for the
|
|
4483
|
+
// representation. A pair that tried to cross a join matches nothing here.
|
|
4484
|
+
if (d.opcode === 'concat') {
|
|
4485
|
+
const [lo, hi] = d.operands;
|
|
4486
|
+
const hiDef = defs.get(hi);
|
|
4487
|
+
const whole = asInt(d.results[0].type);
|
|
4488
|
+
const signedWiden = hiDef?.opcode === 'shr_s' && hiDef.operands[0] === lo && hiDef.attrs.imm === 31;
|
|
4489
|
+
const unsignedWiden = hiDef?.opcode === 'const' && hiDef.attrs.value === 0;
|
|
4490
|
+
if (whole && (signedWiden || unsignedWiden)) {
|
|
4491
|
+
const word = e(lo);
|
|
4492
|
+
const half =
|
|
4493
|
+
renderedIntSignedness(word, vtEnv) === signedWiden
|
|
4494
|
+
? word
|
|
4495
|
+
: { k: 'cast' as const, to: T.int(32, signedWiden), e: word };
|
|
4496
|
+
return { k: 'cast', to: whole, e: half };
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
3669
4499
|
if (d.opcode === 'call') {
|
|
3670
|
-
|
|
4500
|
+
// THE RANK THE RETURN RENDERS WITH TRAVELS ON THE NODE, because no later reader can recover
|
|
4501
|
+
// it: the width is a fact the frontend was TOLD (a runtime helper's signature, or a project's
|
|
4502
|
+
// `FnProto.returns`) and it is gone from the tree by the time the C backend pins an operand.
|
|
4503
|
+
// Stamping it here — at the one place a call becomes an expression — is what keeps every
|
|
4504
|
+
// reader of `exprIntWidth` agreeing. `grep -rn exprIntWidth packages/core/src` is the
|
|
4505
|
+
// enumeration, and it lists SIX call sites in three files: the `hi32` shift and the two
|
|
4506
|
+
// compare pins in this file, the shift rank and the divide rank in backend/cfamily.ts
|
|
4507
|
+
// `pinnedOperands`, and `l3/initfirst.ts`'s `meaningPreserved`.
|
|
4508
|
+
//
|
|
4509
|
+
// THE LAST ONE IS A BEHAVIOUR CHANGE THE STAMP MAKES, and it is named because a reader who
|
|
4510
|
+
// trusts an enumeration is owed a complete one: a `wide64` call answered 32 before the stamp
|
|
4511
|
+
// and answers 64 now, so `meaningPreserved` refuses to fold such a result into a 32-bit
|
|
4512
|
+
// local where it used to admit it. That is what its own reason asks for — a 64-bit value
|
|
4513
|
+
// assigned to a 32-bit one truncates — but it is a pass this branch did not set out to move.
|
|
4514
|
+
const wide64 = d.results[0] !== undefined && intWidth(d.results[0].type) === 64;
|
|
4515
|
+
const through = calleeValue(d);
|
|
4516
|
+
return {
|
|
4517
|
+
k: 'call',
|
|
4518
|
+
fn: through === undefined ? (d.attrs.target as string) : e(through),
|
|
4519
|
+
args: callArgs(d).map(e),
|
|
4520
|
+
...(wide64 ? { wide64: true as const } : {}),
|
|
4521
|
+
...(d.attrs.sret === true ? { sret: d.results[0].type } : {}),
|
|
4522
|
+
};
|
|
3671
4523
|
}
|
|
3672
4524
|
if (d.opcode === 'laddr') {
|
|
3673
4525
|
// gaddr's local twin: the address of the frame-local object the Thumb frontend PROVED
|
|
3674
4526
|
// (frame-object audit — width/signed are stamped machine facts). The NAME is this layer's:
|
|
3675
|
-
// see laddrName. Renders `&sp0`; the object itself is declared in `locals
|
|
4527
|
+
// see laddrName. Renders `&sp0`; the object itself is declared in `locals`, and an ARRAY
|
|
4528
|
+
// one renders by decay, which the C printer reads off that declaration.
|
|
3676
4529
|
return { k: 'addr', name: laddrName.get(d)! };
|
|
3677
4530
|
}
|
|
3678
4531
|
if (d.opcode === 'undef') {
|
|
@@ -3691,7 +4544,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3691
4544
|
return { k: 'addr', name: d.attrs.sym as string };
|
|
3692
4545
|
}
|
|
3693
4546
|
if (d.opcode === 'load') {
|
|
3694
|
-
|
|
4547
|
+
const access = memAccess(
|
|
3695
4548
|
d.operands[0],
|
|
3696
4549
|
e(d.operands[0]),
|
|
3697
4550
|
d.attrs.off as number,
|
|
@@ -3703,11 +4556,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3703
4556
|
false,
|
|
3704
4557
|
advanceStepOf(d.operands[0]),
|
|
3705
4558
|
);
|
|
4559
|
+
return pinnedAccess(d, access);
|
|
3706
4560
|
}
|
|
3707
4561
|
// aload carries a runtime index operand (variable-index array access) — `base[index]`, or
|
|
3708
4562
|
// `base[index].field_K` when it carries a `fieldOff` (array-of-STRUCT element access).
|
|
3709
4563
|
if (d.opcode === 'aload') {
|
|
3710
|
-
|
|
4564
|
+
const access = arrayAccess(
|
|
3711
4565
|
d.operands[0],
|
|
3712
4566
|
e(d.operands[0]),
|
|
3713
4567
|
e(d.operands[1]),
|
|
@@ -3718,16 +4572,28 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3718
4572
|
ctype,
|
|
3719
4573
|
symCtx,
|
|
3720
4574
|
);
|
|
4575
|
+
return pinnedAccess(d, access);
|
|
3721
4576
|
}
|
|
3722
4577
|
return d.opcode === 'opaque'
|
|
3723
|
-
? mkGap(gapReasonFor(d.attrs
|
|
4578
|
+
? mkGap(gapReasonFor(d.attrs), d.operands.map(e))
|
|
3724
4579
|
: mkGap(`no lowering for op '${d.opcode}'`, d.operands.map(e));
|
|
3725
4580
|
};
|
|
3726
4581
|
|
|
4582
|
+
// A map EXTENDS the ambient post-loop substitution (`activeSub`, below) and wins where both map a
|
|
4583
|
+
// value: a loop's own reading is layered over the enclosing exit region's, which still holds
|
|
4584
|
+
// inside it. Replacing the ambient one would re-derive a value a previous loop left in its
|
|
4585
|
+
// variable's name from that name, which already holds it (`v1 + 1` for `v1`). `null` is no
|
|
4586
|
+
// substitution at all.
|
|
3727
4587
|
const exprWith = (sub: Map<Value, string> | null) => {
|
|
3728
4588
|
const e = (v: Value): Expr => {
|
|
3729
|
-
const subbed = sub?.get(v);
|
|
4589
|
+
const subbed = sub === null ? undefined : (sub.get(v) ?? activeSub?.get(v));
|
|
3730
4590
|
if (subbed) {
|
|
4591
|
+
if (zeroTripStale?.get(v) === subbed) {
|
|
4592
|
+
throw new StructureError(
|
|
4593
|
+
`cannot structure '${fn.name}': a read after the loop spells a value computed before it by a ` +
|
|
4594
|
+
`loop variable's name, which holds its initial value on a zero-trip run`,
|
|
4595
|
+
);
|
|
4596
|
+
}
|
|
3731
4597
|
return { k: 'var', name: subbed };
|
|
3732
4598
|
}
|
|
3733
4599
|
if (varName.has(v)) {
|
|
@@ -3748,7 +4614,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3748
4614
|
// naming walk and the coalescing that follows it, and these checks only read it — so today the
|
|
3749
4615
|
// two readings coincide; the reference is what keeps them coinciding if a write ever moves down
|
|
3750
4616
|
// here.
|
|
3751
|
-
const {
|
|
4617
|
+
const {
|
|
4618
|
+
readsClobbered,
|
|
4619
|
+
loopEscapeHazard,
|
|
4620
|
+
loopUpdateHazard,
|
|
4621
|
+
testSkipsAnEffect,
|
|
4622
|
+
preUpdateCondFold,
|
|
4623
|
+
sinkablePreUpdateSlots,
|
|
4624
|
+
sameAtEntry,
|
|
4625
|
+
loopWriteSet,
|
|
4626
|
+
loopWriteSetAhead,
|
|
4627
|
+
} = makeLoopHazards({
|
|
3752
4628
|
defs,
|
|
3753
4629
|
varName,
|
|
3754
4630
|
useSitesOf,
|
|
@@ -3756,6 +4632,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3756
4632
|
opBlock,
|
|
3757
4633
|
materialize,
|
|
3758
4634
|
respelledDefs: bitfieldSpelling,
|
|
4635
|
+
emitPos,
|
|
3759
4636
|
});
|
|
3760
4637
|
|
|
3761
4638
|
// A POST-LOOP substitution active while structuring a loop's exit region: a loop-carried value (a
|
|
@@ -3763,6 +4640,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3763
4640
|
// read the name, not re-inline the computation (which would double-count, e.g. `(u8)(v1+1)` instead
|
|
3764
4641
|
// of `v1`). `expr` consults it; `withSub` installs/merges it around the exit region. Null normally.
|
|
3765
4642
|
let activeSub: Map<Value, string> | null = null;
|
|
4643
|
+
// The entries of a guarded self-loop's substitution its exit region may not render (that site's
|
|
4644
|
+
// `stale`), merged over every enclosing guarded loop's like `activeSub` is: consulted where ANY
|
|
4645
|
+
// substitution hit renders in scope, so it judges what the region spells, dead copies included —
|
|
4646
|
+
// which over-declines, the refusing direction.
|
|
4647
|
+
let zeroTripStale: Map<Value, string> | null = null;
|
|
3766
4648
|
const expr = (v: Value): Expr => exprWith(activeSub)(v);
|
|
3767
4649
|
const withSub = <R>(sub: Map<Value, string>, run: () => R): R => {
|
|
3768
4650
|
const prev = activeSub;
|
|
@@ -3802,9 +4684,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3802
4684
|
// stores whatever the arm left — the same substitution the parameter case makes, one step over.
|
|
3803
4685
|
//
|
|
3804
4686
|
// THE SECOND RELOCATION goes the other way, and this test cannot see it at all. A SUNK pre-update
|
|
3805
|
-
// exit copy (preUpdateCopies) writes the loop EXIT's param
|
|
3806
|
-
//
|
|
3807
|
-
//
|
|
4687
|
+
// exit copy (preUpdateCopies) writes the loop EXIT's param INSIDE the loop BODY, so the home this
|
|
4688
|
+
// reads for it — `paramBlock`, the exit block — sits strictly LATER in the CFG than where the copy
|
|
4689
|
+
// lands. What keeps the two apart is not this test but `dest-free-inside-loop`
|
|
3808
4690
|
// (hazards.ts): a merge inside the body under the exit param's name is a block param
|
|
3809
4691
|
// `definedInBody` sees, so the slot is never sunk in the first place. That gate carries its own
|
|
3810
4692
|
// KNOWN GAP, so the pair is a conjecture rather than a proof — `sunkCopyOverDroppedUndef` re-checks
|
|
@@ -3818,10 +4700,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3818
4700
|
}
|
|
3819
4701
|
/** Every copy `undefCarriesNothing` dropped, with the edge it was dropped from. */
|
|
3820
4702
|
const droppedUndefCopies: { name: string; pred: Block }[] = [];
|
|
3821
|
-
/** Every sunk pre-update exit copy, homed where it LANDS (the loop header
|
|
3822
|
-
*
|
|
3823
|
-
* whole input. */
|
|
4703
|
+
/** Every sunk pre-update exit copy, homed where it LANDS (the loop header, which dominates every
|
|
4704
|
+
* point inside the body a copy is rebuilt at) rather than where its destination param lives.
|
|
4705
|
+
* These two lists are the postcondition's whole input. */
|
|
3824
4706
|
const sunkCopyHomes: { name: string; home: Block }[] = [];
|
|
4707
|
+
/** A loop's sunk pre-update exit copies: those rebuilt at the op that computed their value, and
|
|
4708
|
+
* those with no such op, which open the body. */
|
|
4709
|
+
type SunkCopies = { leading: Stmt[]; atDef: Map<Op, Stmt[]> };
|
|
4710
|
+
const NO_COPIES_AT_A_DEF: ReadonlyMap<Op, Stmt[]> = new Map();
|
|
4711
|
+
/** The `atDef` copies `sideEffects` has spelled — `assertSunkCopiesPlaced`'s input. */
|
|
4712
|
+
const sunkCopiesEmitted = new Set<Stmt>();
|
|
3825
4713
|
const undefCarriesNothing = (arg: Value, name: string, pred: Block): boolean => {
|
|
3826
4714
|
if (defs.get(arg)?.opcode !== 'undef') {
|
|
3827
4715
|
return false;
|
|
@@ -4064,24 +4952,28 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4064
4952
|
// EFFECTFUL op whose result nothing consumes (a void/discarded call, and an `opaque` standing for
|
|
4065
4953
|
// an instruction asmlift could not model); and MATERIALIZED defs — a call/load whose value cannot
|
|
4066
4954
|
// soundly render at its use is assigned to its named temp here, at its own program position.
|
|
4067
|
-
/** A pointer VALUE assigned into a
|
|
4068
|
-
* against ANY declaration of that cell. The byte-arithmetic guard renders such a
|
|
4069
|
-
* side `(u8 *)gS.pBuf + K` — the right ADDRESS in every world, which is the whole
|
|
4070
|
-
* and a `u8 *` where the declaration says `u16 *`. READING one is fine (C converts
|
|
4071
|
-
* pointer freely under a deref, a call argument or a compare); ASSIGNING one is
|
|
4072
|
-
* assignment from incompatible pointer type`, and this project's own `-Werror`
|
|
4073
|
-
* template makes that FATAL — so a source that scores clean here fails to build in the
|
|
4074
|
-
* user pastes it into, which no score gate can observe.
|
|
4955
|
+
/** A pointer VALUE assigned into a pointer CELL (isPtrValue), spelled so the assignment is legal
|
|
4956
|
+
* against ANY pointer declaration of that cell. The byte-arithmetic guard renders such a
|
|
4957
|
+
* right-hand side `(u8 *)gS.pBuf + K` — the right ADDRESS in every world, which is the whole
|
|
4958
|
+
* point of it, and a `u8 *` where the declaration says `u16 *`. READING one is fine (C converts
|
|
4959
|
+
* an object pointer freely under a deref, a call argument or a compare); ASSIGNING one is
|
|
4960
|
+
* `warning: assignment from incompatible pointer type`, and this project's own `-Werror`
|
|
4961
|
+
* compiler template makes that FATAL — so a source that scores clean here fails to build in the
|
|
4962
|
+
* tree a user pastes it into, which no score gate can observe.
|
|
4075
4963
|
*
|
|
4076
4964
|
* `void *`, NOT the map's declared pointee: it is the one target assignment-compatible with any
|
|
4077
4965
|
* object-pointer declaration, the same "same answer in every world" property the guard that
|
|
4078
4966
|
* made the `u8 *` exists for. Trusting the map's pointee would put the spelling back in one
|
|
4079
4967
|
* world. It costs nothing: agbcc compiles `p = (void *)((u8 *)p + 4)` and the warning-carrying
|
|
4080
4968
|
* `p = (u8 *)p + 4` to BYTE-IDENTICAL objects (`-mthumb-interwork -Wimplicit -O2 -fhex-asm
|
|
4081
|
-
* -fprologue-bugfix`), so the fix is invisible to the differ and visible to the compiler.
|
|
4969
|
+
* -fprologue-bugfix`), so the fix is invisible to the differ and visible to the compiler.
|
|
4970
|
+
*
|
|
4971
|
+
* A project may still declare a cell no map types an INTEGER (`u32 gSym;`), where `(void *)`
|
|
4972
|
+
* makes an integer from a pointer: agbcc warns, CodeWarrior rejects it. The candidate's own world
|
|
4973
|
+
* declares the cell `void *` (l3/symbol-refs.ts), and `/int-cell` (l3/intcell.ts) spells the
|
|
4974
|
+
* integer declaration's source, published only at a byte-exact score. */
|
|
4082
4975
|
const intoPtrCell = (lval: Expr, value: Expr): Expr => {
|
|
4083
|
-
|
|
4084
|
-
if (!cell) {
|
|
4976
|
+
if (!isPtrValue(lval)) {
|
|
4085
4977
|
return value;
|
|
4086
4978
|
}
|
|
4087
4979
|
const vt = ctype(value);
|
|
@@ -4093,80 +4985,49 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4093
4985
|
return isPtr ? { k: 'cast', to: T.ptr(T.void()), e: value } : value;
|
|
4094
4986
|
};
|
|
4095
4987
|
|
|
4096
|
-
/** THE REFUSAL for the read half of `unreadResult
|
|
4097
|
-
*
|
|
4988
|
+
/** THE REFUSAL for the read half of `unreadResult`: can a `volatile` reach this dead read's
|
|
4989
|
+
* spelling, here or in some candidate enumerated from this tree?
|
|
4990
|
+
*
|
|
4991
|
+
* A QUALIFIED read (ir/discipline.ts `qualified`) answers yes: `pinnedAccess` puts the qualifier
|
|
4992
|
+
* on its spelling, from the device pin or from the map's declaration of the object it reads. Every
|
|
4993
|
+
* other read is plain in this tree, and only a variation can qualify it, so the statement is
|
|
4994
|
+
* spelled only where both of these hold:
|
|
4098
4995
|
*
|
|
4099
4996
|
* 1. EVIDENCE — would a source plausibly have declared this access `volatile`? The answer has
|
|
4100
4997
|
* to come from DATA: the target's declared device-register window
|
|
4101
|
-
* (`capabilities.deviceRegisters`)
|
|
4102
|
-
*
|
|
4103
|
-
*
|
|
4104
|
-
* 2. REACHABILITY —
|
|
4105
|
-
*
|
|
4106
|
-
*
|
|
4107
|
-
*
|
|
4108
|
-
*
|
|
4109
|
-
*
|
|
4998
|
+
* (`capabilities.deviceRegisters`). `volatile` is a CORRECTNESS claim about an address, not a
|
|
4999
|
+
* spelling preference, and asserting one about ordinary RAM is a wrong answer rather than a
|
|
5000
|
+
* wrong spelling.
|
|
5001
|
+
* 2. REACHABILITY — the base value has a use OTHER than this read: l3/volatileptr.ts qualifies a
|
|
5002
|
+
* pointer LOCAL, and l3/basecse.ts only mints that local for a base something else also
|
|
5003
|
+
* touches. A single-access read — `*(s32 *)0x04000200;`, the `REG_IF` acknowledge idiom, and
|
|
5004
|
+
* the shape a WRONG `returnsVoid` on a register accessor produces — has no local to qualify
|
|
5005
|
+
* and never will, however plainly its address is a device register. "Some other use" is
|
|
5006
|
+
* NECESSARY for that local, not sufficient; the gate states the necessary half. Where no
|
|
5007
|
+
* qualifier can land, the statement would be a permanent bare deref in the DEFAULT source,
|
|
5008
|
+
* which is what the playground pins and what a decomp author copies.
|
|
4110
5009
|
*
|
|
4111
|
-
*
|
|
4112
|
-
*
|
|
4113
|
-
*
|
|
4114
|
-
* (`((s32 *)®_DMA3SAD)[2]`) has thrown the qualifier away in the spelling itself, whatever
|
|
4115
|
-
* the declaration says. The member arm asks the CONTAINER's qualifier and not the member's own
|
|
4116
|
-
* (`SymbolStructField.volatile`), which looks backwards and is not: `memberQualsAllow` above
|
|
4117
|
-
* refuses to NAME a volatile member at all, so a `vu16` member is spelled `((s32 *)&gSym)[k]`
|
|
4118
|
-
* with nothing in the spelling for a variation to hold, while `volatile struct S gSym;` qualifies
|
|
4119
|
-
* every member and `gSym.ctl;` really is an observable read. Every other map spelling refuses —
|
|
4120
|
-
* `gPtr->member`, a bare-name array element, a multidimensional subscript — because
|
|
4121
|
-
* over-refusing costs a SPELLING and admitting wrongly costs an ANSWER, this file's standing
|
|
4122
|
-
* asymmetry.
|
|
5010
|
+
* The address is a LITERAL, which inhabits only `/raw-globals`: with a symbol map the frontend
|
|
5011
|
+
* spells a pool word as `gaddr`, so `constAddressOf` returns null, and a read of a named object
|
|
5012
|
+
* is plain because the map does not declare it volatile.
|
|
4123
5013
|
*
|
|
4124
|
-
* THE
|
|
4125
|
-
*
|
|
4126
|
-
* also touches. A single-access read — `*(s32 *)0x04000200;`, the `REG_IF` acknowledge idiom,
|
|
4127
|
-
* and the shape a WRONG `returnsVoid` on a register accessor produces — has no local to qualify
|
|
4128
|
-
* and never will, however plainly its address is a device register. "Some other use" is
|
|
4129
|
-
* NECESSARY for that local, not sufficient; the gate states the necessary half.
|
|
4130
|
-
*
|
|
4131
|
-
* ONLY `load` REACHES EITHER ARM. `aload` carries its index in `operands[1]` and has no `off`
|
|
4132
|
-
* attr at all, so both address queries would answer for the BARE BASE — `globalCellOf` resolves
|
|
4133
|
-
* a base and discards the index by construction (ir/alias.ts), `constAddressOf` sees the literal
|
|
5014
|
+
* ONLY `load` REACHES THE ARM. `aload` carries its index in `operands[1]` and has no `off` attr at
|
|
5015
|
+
* all, so the address query would answer for the BARE BASE — `constAddressOf` sees the literal
|
|
4134
5016
|
* with `off` defaulted to 0 — which admits `volatile s32 *p0 = (s32 *)0x04000000; p0[a0];`, a
|
|
4135
5017
|
* qualified access at an address the declared window does not cover. The whitelist is by OPCODE
|
|
4136
|
-
* so a read op added later refuses until someone answers both questions for it.
|
|
4137
|
-
*
|
|
4138
|
-
* THE ARMS DO NOT SHARE A POPULATION. With a symbol map the frontend spells a pool word as
|
|
4139
|
-
* `gaddr`, so `constAddressOf` returns null and the literal arm inhabits only `/raw-globals`,
|
|
4140
|
-
* which re-structures with NO map. The map arm is the default-source one, and a map-fed DMA
|
|
4141
|
-
* function spells no read at all — its wait-read is cast-spelled (see `BASECSE_GATES`'
|
|
4142
|
-
* `repeated-const-offset`, whose base local this refusal hands back). */
|
|
5018
|
+
* so a read op added later refuses until someone answers both questions for it. */
|
|
4143
5019
|
const volatileQualifiable = (op: Op): boolean => {
|
|
5020
|
+
if (qualified(op)) {
|
|
5021
|
+
return true;
|
|
5022
|
+
}
|
|
4144
5023
|
if (op.opcode !== 'load') {
|
|
4145
5024
|
return false;
|
|
4146
5025
|
}
|
|
4147
|
-
const off = typeof op.attrs.off === 'number' ? op.attrs.off : 0;
|
|
4148
|
-
const width = op.attrs.width as number;
|
|
4149
|
-
const cell = globalCellOf(defs, op.operands[0], off);
|
|
4150
|
-
if (cell) {
|
|
4151
|
-
const si = symbols?.get(cell.name);
|
|
4152
|
-
if (si === undefined) {
|
|
4153
|
-
return false;
|
|
4154
|
-
}
|
|
4155
|
-
if (si.shape === 'struct') {
|
|
4156
|
-
// the SAME find memAccess's struct arm makes, so the two cannot disagree about which
|
|
4157
|
-
// accesses reach the `gSym.field` spelling this arm's qualifier rides on
|
|
4158
|
-
const fld = symCtx
|
|
4159
|
-
?.fieldsOf(cell.name)
|
|
4160
|
-
?.find((f) => f.offset === cell.byte && f.size === width && !isArrayField(f) && !isBitfieldField(f));
|
|
4161
|
-
return fld !== undefined && memberQualsAllow(fld, si.const, false) && si.volatile === true;
|
|
4162
|
-
}
|
|
4163
|
-
return cell.byte === 0 && scalarGlobals.has(cell.name) && si.volatile === true;
|
|
4164
|
-
}
|
|
4165
5026
|
const window = opts.deviceRegisters;
|
|
4166
5027
|
if (!window) {
|
|
4167
5028
|
return false;
|
|
4168
5029
|
}
|
|
4169
|
-
const addr = constAddressOf(defs, op.operands[0], off);
|
|
5030
|
+
const addr = constAddressOf(defs, op.operands[0], typeof op.attrs.off === 'number' ? op.attrs.off : 0);
|
|
4170
5031
|
if (addr === null || addr < window[0] || addr >= window[1]) {
|
|
4171
5032
|
return false;
|
|
4172
5033
|
}
|
|
@@ -4174,11 +5035,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4174
5035
|
};
|
|
4175
5036
|
|
|
4176
5037
|
/** Ops the `sideEffects` walk must SPELL even though nothing consumes their result — the
|
|
4177
|
-
*
|
|
4178
|
-
* needs no edit here, plus the address refusal above for the memory-read half.
|
|
5038
|
+
* question ir/discipline.ts asks of every op (`spelledWhenDead`), so the next op to acquire the
|
|
5039
|
+
* property needs no edit here, plus the address refusal above for the memory-read half.
|
|
4179
5040
|
*
|
|
4180
|
-
* A memory READ is not
|
|
4181
|
-
*
|
|
5041
|
+
* A memory READ is not `effectful`, deliberately: ir/discipline.ts calls a load deletable when
|
|
5042
|
+
* dead, because nothing observes a read nobody reads. That is the C claim. The COMPILER
|
|
4182
5043
|
* claim points the other way — an optimizing compiler deletes every dead read it is allowed to
|
|
4183
5044
|
* delete, so one still in the target is evidence the source's access was `volatile`, and
|
|
4184
5045
|
* dropping it deletes an instruction the machine executed.
|
|
@@ -4203,9 +5064,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4203
5064
|
* the other preimage is a function whose `returnsVoid` fact is WRONG, so its return value
|
|
4204
5065
|
* arrived here as a dead read. The second-use clause removes the common shape of that (a bare
|
|
4205
5066
|
* register accessor), leaving a function that both STORES to a device register and reads one
|
|
4206
|
-
* back — narrow, but not proven empty.
|
|
4207
|
-
* `extern volatile u32 gStatus;` compiles differently from its own absence
|
|
4208
|
-
* referee it. The clean fix — spell the read only in candidates that also qualify it, paired
|
|
5067
|
+
* back — narrow, but not proven empty. A qualified read has no such problem: `gStatus;` under
|
|
5068
|
+
* `extern volatile u32 gStatus;` compiles differently from its own absence. The clean fix — spell the read only in candidates that also qualify it, paired
|
|
4209
5069
|
* the way `/livebase/volatile` already pairs — moves every device row's fan shape and wants
|
|
4210
5070
|
* its own round and zero-flip gate over BOTH tiers.
|
|
4211
5071
|
* 2. THE REFUSAL IS SILENT. When this returns false for a `load`, the machine performed a read
|
|
@@ -4268,7 +5128,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4268
5128
|
const hasSpelledUse = (v: Value): boolean => (useSitesOf.get(v) ?? []).some((s) => isSpelled(s.op));
|
|
4269
5129
|
|
|
4270
5130
|
const unreadResult = (op: Op): boolean =>
|
|
4271
|
-
|
|
5131
|
+
spelledWhenDead(op) &&
|
|
4272
5132
|
op.results.length > 0 &&
|
|
4273
5133
|
// NO EXEMPTION FOR A MATERIALIZED DEF HERE, and it is not missing. Such a def already renders
|
|
4274
5134
|
// at its own position, as `v = f(…)`, which spells the effect as surely as a bare statement
|
|
@@ -4281,7 +5141,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4281
5141
|
!hasSpelledUse(op.results[0]) &&
|
|
4282
5142
|
(opSig(op.opcode)?.reads !== true || volatileQualifiable(op));
|
|
4283
5143
|
|
|
4284
|
-
const sideEffects = (b: Block): Stmt[] => {
|
|
5144
|
+
const sideEffects = (b: Block, atDef: ReadonlyMap<Op, Stmt[]> = NO_COPIES_AT_A_DEF): Stmt[] => {
|
|
4285
5145
|
const out: Stmt[] = [];
|
|
4286
5146
|
for (const op of b.ops) {
|
|
4287
5147
|
if (op.opcode === 'store') {
|
|
@@ -4320,24 +5180,26 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4320
5180
|
}
|
|
4321
5181
|
// signedness mirrors recoverTypes' store seed (word ⇒ signed, narrow ⇒ unsigned), so an
|
|
4322
5182
|
// inserted cast declares the same scalar the recovered pointee would have.
|
|
4323
|
-
|
|
5183
|
+
const lval = pinnedAccess(op, lval0);
|
|
5184
|
+
out.push({ k: 'store', lval, value: intoPtrCell(lval, expr(op.operands[1])) });
|
|
4324
5185
|
} else if (op.opcode === 'astore') {
|
|
4325
5186
|
const elemSize = op.attrs.elemSize as number;
|
|
5187
|
+
const lval = arrayAccess(
|
|
5188
|
+
op.operands[0],
|
|
5189
|
+
expr(op.operands[0]),
|
|
5190
|
+
expr(op.operands[1]),
|
|
5191
|
+
op.attrs.fieldOff as number | undefined,
|
|
5192
|
+
op.attrs.memberOff as number | undefined,
|
|
5193
|
+
elemSize,
|
|
5194
|
+
// a member array's store spells through the member's OWN declaration, which the
|
|
5195
|
+
// recognizer records; every other astore keeps the width===4 convention
|
|
5196
|
+
(op.attrs.signed as boolean | undefined) ?? elemSize === 4,
|
|
5197
|
+
ctype,
|
|
5198
|
+
symCtx,
|
|
5199
|
+
);
|
|
4326
5200
|
out.push({
|
|
4327
5201
|
k: 'store',
|
|
4328
|
-
lval:
|
|
4329
|
-
op.operands[0],
|
|
4330
|
-
expr(op.operands[0]),
|
|
4331
|
-
expr(op.operands[1]),
|
|
4332
|
-
op.attrs.fieldOff as number | undefined,
|
|
4333
|
-
op.attrs.memberOff as number | undefined,
|
|
4334
|
-
elemSize,
|
|
4335
|
-
// a member array's store spells through the member's OWN declaration, which the
|
|
4336
|
-
// recognizer records; every other astore keeps the width===4 convention
|
|
4337
|
-
(op.attrs.signed as boolean | undefined) ?? elemSize === 4,
|
|
4338
|
-
ctype,
|
|
4339
|
-
symCtx,
|
|
4340
|
-
),
|
|
5202
|
+
lval: pinnedAccess(op, lval),
|
|
4341
5203
|
value: expr(op.operands[2]),
|
|
4342
5204
|
});
|
|
4343
5205
|
} else if (rendersAtOwnPosition(op)) {
|
|
@@ -4364,6 +5226,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4364
5226
|
out.push({ k: 'assign', name: a.name, value: expr(a.arg) });
|
|
4365
5227
|
anchorsEmitted.add(a);
|
|
4366
5228
|
}
|
|
5229
|
+
// a sunk pre-update exit copy, rebuilt at the op that computed its value (preUpdateCopies).
|
|
5230
|
+
// Reached for the same reason the anchored copies are: every key is an op with a RESULT, so
|
|
5231
|
+
// none of them is the `store`/`astore` this walk `continue`s out of above. A PARAMETER, where
|
|
5232
|
+
// the anchored copies read a module-level map: a sunk copy belongs to ONE loop's body, and a
|
|
5233
|
+
// map would let the generic `sideEffects(b)` calls emit it in an unrelated context — silently,
|
|
5234
|
+
// because `assertSunkCopiesPlaced` would see it placed.
|
|
5235
|
+
for (const st of atDef.get(op) ?? []) {
|
|
5236
|
+
out.push(st);
|
|
5237
|
+
sunkCopiesEmitted.add(st);
|
|
5238
|
+
}
|
|
4367
5239
|
}
|
|
4368
5240
|
return out;
|
|
4369
5241
|
};
|
|
@@ -4380,7 +5252,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4380
5252
|
// to `header` is a conditional continue; its other edge, when it leaves the loop, is an early exit
|
|
4381
5253
|
// (a `break` to `exit`, or an early `return` through a trampoline). Null outside any loop body —
|
|
4382
5254
|
// the early-exit branch in structureBlock is inert there.
|
|
4383
|
-
type LoopFrame = { header: Block; exit: Block; body: Set<Block>; arms: LoopArm[] };
|
|
5255
|
+
type LoopFrame = { header: Block; exit: Block; body: Set<Block>; arms: LoopArm[]; breaks: ReadonlySet<Block> };
|
|
4384
5256
|
let loopCtx: LoopFrame | null = null;
|
|
4385
5257
|
const withLoop = <R>(frame: LoopFrame, run: () => R): R => {
|
|
4386
5258
|
const prev = loopCtx;
|
|
@@ -4392,6 +5264,88 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4392
5264
|
}
|
|
4393
5265
|
};
|
|
4394
5266
|
|
|
5267
|
+
// THE IMPLICIT CONTINUE. A branch with one edge back to the loop header spells that edge as
|
|
5268
|
+
// nothing at all: control falls to the bottom of the body and the loop goes round. That holds only
|
|
5269
|
+
// where the region being structured ends at the loop bottom (`stop === frame.header`). An `if` in
|
|
5270
|
+
// the body whose join lies outside it, at the loop's exit, hands its arms that exit as `stop` and
|
|
5271
|
+
// renders the exit region after itself, still inside the body: an implicit continue there falls
|
|
5272
|
+
// into that region and the loop runs at most once. Both spellings of a branch that leaves the loop
|
|
5273
|
+
// beside such an edge consult this.
|
|
5274
|
+
const continueFallsToBottom = (frame: LoopFrame, stop: Block | null): boolean => stop === frame.header;
|
|
5275
|
+
|
|
5276
|
+
// THE BREAK RULE, for a `break` from block `from` out of `frame`'s body wherever the edge sits. A
|
|
5277
|
+
// `break` lands after the loop, where the header→exit copies render and then the exit region, raw.
|
|
5278
|
+
// Refused:
|
|
5279
|
+
// • a `do-while`: its exit copies live post-loop too, but are judged differently;
|
|
5280
|
+
// • the header→exit edge has copies the break cannot share: the break path runs them too, and
|
|
5281
|
+
// they would overwrite what the break carried;
|
|
5282
|
+
// • the exit region reads a loop value under a loop variable's name that `writes` holds —
|
|
5283
|
+
// the names this iteration has rewritten by the time the break leaves.
|
|
5284
|
+
//
|
|
5285
|
+
// A BREAK THAT CARRIES WHAT THE HEADER CARRIES shares them. When every argument the break edge
|
|
5286
|
+
// hands the exit is the very value the header's exit edge hands it, the break needs no copies of
|
|
5287
|
+
// its own: it leaves `bare` and falls into the header's copies after the loop, which compute the
|
|
5288
|
+
// same values. They compute them from the names they read, at the loop's end instead of at the
|
|
5289
|
+
// header, so those names must still hold what the header read: none may be one `writes` holds.
|
|
5290
|
+
// Without that clause a latch break spelled after its update runs the update twice —
|
|
5291
|
+
// `v = v + 1; break;`, then the header's copy `x = v + 1` after the loop. The clause is judged
|
|
5292
|
+
// over every argument the header hands the exit, at the value level, and not only over the copies
|
|
5293
|
+
// that render: an exit parameter that takes its argument's own name renders no copy, yet the exit
|
|
5294
|
+
// still reads that name, and a latch update that wrote it hands the exit the updated value.
|
|
5295
|
+
const breakRule = (frame: LoopFrame, from: Block, writes: Set<string>): { refusal: string } | { bare: boolean } => {
|
|
5296
|
+
// A `while (1)` has no copies of its own after the loop, so a break carries its own, computed
|
|
5297
|
+
// where it leaves; what is left is the exit region's re-derivations. An inner loop's value the
|
|
5298
|
+
// region reads under its name (`emitForever`) is no re-derivation.
|
|
5299
|
+
const fl = foreverLoops.get(frame.header);
|
|
5300
|
+
if (fl !== undefined) {
|
|
5301
|
+
const exitRegion = new Set([frame.exit, ...reachFrom(frame.exit)].filter((x) => !frame.body.has(x)));
|
|
5302
|
+
const { sub } = latchInnerSub(fl.header, fl.body, [...fl.breaks], exitRegion);
|
|
5303
|
+
return loopEscapeHazard(frame.body, sub, writes, exitRegion)
|
|
5304
|
+
? { refusal: 'reaches an exit region that reads a loop value under a name this iteration already rewrote' }
|
|
5305
|
+
: { bare: false };
|
|
5306
|
+
}
|
|
5307
|
+
if (!whileLoops.has(frame.header)) {
|
|
5308
|
+
return { refusal: 'leaves a do-while' };
|
|
5309
|
+
}
|
|
5310
|
+
const headerCopies = argAssigns(frame.header, frame.exit);
|
|
5311
|
+
let bare = false;
|
|
5312
|
+
if (headerCopies.length !== 0) {
|
|
5313
|
+
const headerArgs = successorTo(frame.header, frame.exit)!.args;
|
|
5314
|
+
const breakArgs = successorTo(from, frame.exit)!.args;
|
|
5315
|
+
const reads = new Set<string>();
|
|
5316
|
+
for (const s of headerCopies) {
|
|
5317
|
+
stmtExprs(s).forEach((e) => exprVars(e, reads));
|
|
5318
|
+
}
|
|
5319
|
+
bare =
|
|
5320
|
+
breakArgs.every((v, k) => v === headerArgs[k]) &&
|
|
5321
|
+
![...reads].some((n) => writes.has(n)) &&
|
|
5322
|
+
!headerArgs.some((a) => readsClobbered(a, new Map(), writes));
|
|
5323
|
+
if (!bare) {
|
|
5324
|
+
return { refusal: 'would run the copies the loop header hands its exit' };
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
const exitRegion = new Set([frame.exit, ...reachFrom(frame.exit)].filter((x) => !frame.body.has(x)));
|
|
5328
|
+
if (loopEscapeHazard(frame.body, new Map(), writes, exitRegion)) {
|
|
5329
|
+
return { refusal: 'reaches an exit region that reads a loop value under a name this iteration already rewrote' };
|
|
5330
|
+
}
|
|
5331
|
+
return { bare };
|
|
5332
|
+
};
|
|
5333
|
+
// The loop-variable names an iteration has written on its way from the header to `b`, before any
|
|
5334
|
+
// update copy (`loopWriteSetAhead`).
|
|
5335
|
+
const writtenAhead = (frame: LoopFrame, b: Block): Set<string> => {
|
|
5336
|
+
const ahead = new Set<Block>([b]);
|
|
5337
|
+
for (const stack = [b]; stack.length;) {
|
|
5338
|
+
for (const p of preds.get(stack.pop()!) ?? []) {
|
|
5339
|
+
if (p !== frame.header && frame.body.has(p) && !ahead.has(p)) {
|
|
5340
|
+
ahead.add(p);
|
|
5341
|
+
stack.push(p);
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
const incoming = (x: Block) => [...inEdgeRecords(preds, x)].map(({ succ }) => succ.args);
|
|
5346
|
+
return loopWriteSetAhead(ahead, incoming, frame.header);
|
|
5347
|
+
};
|
|
5348
|
+
|
|
4395
5349
|
// Branch-sense sites, numbered as the walk below first reaches them (`branchSenseFlipSites`).
|
|
4396
5350
|
const senseOrdinal = new Map<number, number>();
|
|
4397
5351
|
|
|
@@ -4403,11 +5357,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4403
5357
|
if (b === stop) {
|
|
4404
5358
|
return [];
|
|
4405
5359
|
}
|
|
5360
|
+
// Back to a `while (1)` header from a region that ends short of the loop bottom: the edge's
|
|
5361
|
+
// copies are already out, and `continue` is the jump.
|
|
5362
|
+
if (loopCtx !== null && b === loopCtx.header && foreverLoops.has(b)) {
|
|
5363
|
+
return [{ k: 'continue' }];
|
|
5364
|
+
}
|
|
4406
5365
|
if (onStack.has(b)) {
|
|
4407
5366
|
throw new StructureError(
|
|
4408
5367
|
`cannot structure '${fn.name}': unrecovered back-edge into block #${fn.blocks.indexOf(b)} ` +
|
|
4409
|
-
`(loop-recovery declined this shape:
|
|
4410
|
-
`a
|
|
5368
|
+
`(loop-recovery declined this shape: irreducible/overlapping loops, exits to more than one ` +
|
|
5369
|
+
`live merge, a break out of a nested loop, or an unsafe break or continue)`,
|
|
4411
5370
|
);
|
|
4412
5371
|
}
|
|
4413
5372
|
onStack.add(b);
|
|
@@ -4437,7 +5396,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4437
5396
|
isNamed: (v) => varName.has(v),
|
|
4438
5397
|
isCmpOpcode: (opcode) => !!CMP_TO_BIN[opcode],
|
|
4439
5398
|
switchAllowsNeqCase,
|
|
4440
|
-
|
|
5399
|
+
switchBoundCase: switchBoundCase ?? null,
|
|
4441
5400
|
switchArmsFollowLayout,
|
|
4442
5401
|
switchRequiresFrontLoadedTests,
|
|
4443
5402
|
spellSwitchFallthrough,
|
|
@@ -4456,6 +5415,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4456
5415
|
if (dw && !dwActive.has(b)) {
|
|
4457
5416
|
return emitDoWhile(dw, stop);
|
|
4458
5417
|
}
|
|
5418
|
+
const forever = foreverLoops.get(b);
|
|
5419
|
+
if (forever && !dwActive.has(b)) {
|
|
5420
|
+
return emitForever(forever, stop);
|
|
5421
|
+
}
|
|
4459
5422
|
|
|
4460
5423
|
const out: Stmt[] = [...sideEffects(b)];
|
|
4461
5424
|
const term = b.ops[b.ops.length - 1];
|
|
@@ -4480,7 +5443,34 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4480
5443
|
// region flows into the NEXT arm is C fall-through (no `break`). Any other shape needs a `goto` and
|
|
4481
5444
|
// fails LOUD rather than being duplicated or silently closed.
|
|
4482
5445
|
if (term.opcode === 'switch_br') {
|
|
4483
|
-
|
|
5446
|
+
// WHERE THE SWITCH ENDS. Post-dominance answers it whenever the arms reconverge. When they
|
|
5447
|
+
// do not — some arm `return`s, so EXIT is the only block on every path — the answer is the
|
|
5448
|
+
// one `followOverReturns` derives for a divergent `if`: keep the blocks that reach a `ret`
|
|
5449
|
+
// EVERY successor reaches, and post-dominate on what is left.
|
|
5450
|
+
//
|
|
5451
|
+
// UNCONDITIONAL, where the `if` asks it behind `followEarlyReturns` — and NOT because a jump
|
|
5452
|
+
// table has only one reading. It has the same two an `if` has, and `ipdom.get(b) ?? stop` is
|
|
5453
|
+
// the other one: for a table whose arms reach a shared `ret` and also a shared tail, that
|
|
5454
|
+
// reading structures each arm to its own end and emits legal, complete C with the tail
|
|
5455
|
+
// duplicated into the arms. Both are legal C for one CFG, so the asm underdetermines the
|
|
5456
|
+
// source — which is the definition of a ranked variation (docs/level-tower.md).
|
|
5457
|
+
//
|
|
5458
|
+
// WHAT MAKES THIS ONE THE DEFAULT is which of the two can decline. With a null merge the
|
|
5459
|
+
// default edge's target joins `siblings`, so an arm's plain `break` classifies as a
|
|
5460
|
+
// fall-through into it, and Regime B has no fallback recovery — `synthetic:sw_jtret` is that
|
|
5461
|
+
// shape and declines outright. A variation could not rescue it, and the reason is an
|
|
5462
|
+
// authoring rule rather than a blanket one: `assertDefaultAccepts` re-runs the default
|
|
5463
|
+
// structuring, and lets its refusal stand, for the options it RESETS — which is every
|
|
5464
|
+
// structure variation but the four named as non-members beside it. `followEarlyReturns`, the
|
|
5465
|
+
// `if`-side twin of this very mechanism, is one it resets, because moving a merge point
|
|
5466
|
+
// changes which edge copies elide as identities; a switch-side twin moves the same merge
|
|
5467
|
+
// point and would be reset for the same reason. So the divergent reading cannot come back as
|
|
5468
|
+
// a candidate that rescues `sw_jtret`. The reading that lifts strictly
|
|
5469
|
+
// more functions therefore has to BE the default, and it is the DIVERGENT reading that a
|
|
5470
|
+
// variation would have to offer back — a candidate nothing mints today, and the one thing
|
|
5471
|
+
// this line costs. `?? stop` keeps the shape where no `ret` is reachable from every
|
|
5472
|
+
// successor.
|
|
5473
|
+
const merge = ipdom.get(b) ?? followOverReturns(b, stop) ?? stop;
|
|
4484
5474
|
const succ = term.successors;
|
|
4485
5475
|
const caseVals = term.attrs.cases as number[];
|
|
4486
5476
|
// Switch edges CARRY phi args (frontend/ssa.ts appends them terminator-generically) — each
|
|
@@ -4724,14 +5714,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4724
5714
|
// BEFORE the body ever ran — reading a temp uninitialized on the first test. The kept-guard
|
|
4725
5715
|
// `do-while` tests after the body, so it carries no such restriction.
|
|
4726
5716
|
const fused = guardProven && !li.header.ops.some((o) => materialize.has(o));
|
|
4727
|
-
// A pre-update exit copy is repairable rather than fatal: emitted
|
|
4728
|
-
// captures the value
|
|
5717
|
+
// A pre-update exit copy is repairable rather than fatal: emitted INSIDE the body, ahead of
|
|
5718
|
+
// the update, it captures the value the exit edge carries rather than the updated one.
|
|
4729
5719
|
// Its post-loop copy is then dropped, so the ZERO-TRIP path needs the guard→exit edge as a
|
|
4730
5720
|
// seed — the only edge holding the never-entered value. That is why the sink demands the
|
|
4731
5721
|
// proof above: it makes the fused zero-trip path load-bearing.
|
|
4732
5722
|
const sunk = guardProven
|
|
4733
|
-
? sinkablePreUpdateSlots(
|
|
4734
|
-
|
|
5723
|
+
? sinkablePreUpdateSlots(
|
|
5724
|
+
li.header,
|
|
5725
|
+
li.exit,
|
|
5726
|
+
hexitArgs,
|
|
5727
|
+
new Set([li.header]),
|
|
5728
|
+
li.header,
|
|
5729
|
+
sub,
|
|
5730
|
+
updateWrites,
|
|
5731
|
+
hooks.preUpdateSinkGates ?? PREUPDATE_SINK_GATES,
|
|
5732
|
+
(j) => !sameAtEntry(hexitArgs[j], guardExit.args[j] as Value, entryVals),
|
|
5733
|
+
)
|
|
5734
|
+
: new Map<number, Op | null>();
|
|
4735
5735
|
// (2) Every exit copy the fused form KEEPS renders after the loop, on the zero-trip path
|
|
4736
5736
|
// too — where the loop variables still hold their init values. It must therefore produce
|
|
4737
5737
|
// what the guard→exit edge carries, since that edge is dropped. A sunk slot is exempt: its
|
|
@@ -4757,6 +5757,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4757
5757
|
null,
|
|
4758
5758
|
)
|
|
4759
5759
|
) {
|
|
5760
|
+
// NO `n++` REPAIR HERE, and the SHAPE is why rather than an oversight. The fold
|
|
5761
|
+
// (`preUpdateCondFold`) trades the update copy at the foot of the body for a `++` at the
|
|
5762
|
+
// leaf, which is the same program only for a BOTTOM test: it runs once per iteration, in
|
|
5763
|
+
// the position the copy stood. This test renders at the TOP, ahead of the body, and runs
|
|
5764
|
+
// one more time than the body does — `while (n++ < k)` over a body with no update is a
|
|
5765
|
+
// different loop. Reaching this site is a rotation, not a wider gate.
|
|
4760
5766
|
throw new StructureError(
|
|
4761
5767
|
`cannot structure '${fn.name}': loop condition or a post-loop value reads a pre-update loop variable`,
|
|
4762
5768
|
);
|
|
@@ -4867,13 +5873,68 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4867
5873
|
} // entering the loop must be `taken`
|
|
4868
5874
|
out.push(mkIf(gcond, [loopStmt], []));
|
|
4869
5875
|
}
|
|
5876
|
+
// A POST-LOOP READ THAT HOLDS THE INIT INSTEAD. The exit copies and the exit region render
|
|
5877
|
+
// under the un-rotation substitution, which spells a back-edge arg as its loop variable's
|
|
5878
|
+
// name. Both run on the zero-trip path too, where that name still holds its init — so an arg
|
|
5879
|
+
// computed BEFORE the loop reads right there only if the init is that very value (`sub` is
|
|
5880
|
+
// last-wins over params sharing an arg, so the init is read off the last one). `staleExit`
|
|
5881
|
+
// proves a copy through `sameAtEntry`, which answers true for one value on both exit edges
|
|
5882
|
+
// without looking inside it — so such a copy is judged here through its inlined expression,
|
|
5883
|
+
// and the region wherever its render hits such an entry (`zeroTripStale`).
|
|
5884
|
+
//
|
|
5885
|
+
// KNOWN GAP, wrong before this check existed too: two DIFFERENT exit values sharing a subtree
|
|
5886
|
+
// that reaches a stale entry pass `staleExit` for the same reason, and so do two calls whose
|
|
5887
|
+
// operands map to each other through the entry values.
|
|
5888
|
+
const stale = new Map(
|
|
5889
|
+
[...sub].filter(([x]) => {
|
|
5890
|
+
const d = defs.get(x);
|
|
5891
|
+
return (
|
|
5892
|
+
!li.header.params.includes(x) &&
|
|
5893
|
+
(d === undefined || opBlock.get(d) !== li.header) &&
|
|
5894
|
+
initArgs[li.backArgOfParam.lastIndexOf(x)] !== x
|
|
5895
|
+
);
|
|
5896
|
+
}),
|
|
5897
|
+
);
|
|
5898
|
+
const holdsInitInstead = (root: Value): boolean => {
|
|
5899
|
+
const seen = new Set<Value>();
|
|
5900
|
+
const walk = (x: Value): boolean => {
|
|
5901
|
+
if (seen.has(x)) {
|
|
5902
|
+
return false;
|
|
5903
|
+
}
|
|
5904
|
+
seen.add(x);
|
|
5905
|
+
if (sub.has(x)) {
|
|
5906
|
+
return stale.has(x);
|
|
5907
|
+
}
|
|
5908
|
+
if (varName.has(x)) {
|
|
5909
|
+
return false;
|
|
5910
|
+
}
|
|
5911
|
+
const d = defs.get(x);
|
|
5912
|
+
return d !== undefined && d.operands.some(walk);
|
|
5913
|
+
};
|
|
5914
|
+
return walk(root);
|
|
5915
|
+
};
|
|
5916
|
+
if (hexitArgs.some((a, j) => keptSlot(j) && a === guardExit.args[j] && holdsInitInstead(a))) {
|
|
5917
|
+
throw new StructureError(
|
|
5918
|
+
`cannot structure '${fn.name}': a read after the loop spells a value computed before it by a ` +
|
|
5919
|
+
`loop variable's name, which holds its initial value on a zero-trip run`,
|
|
5920
|
+
);
|
|
5921
|
+
}
|
|
4870
5922
|
// The header→exit edge may carry non-identity phi args (the exit param merges the guard-false
|
|
4871
5923
|
// value with the loop's final value). Emit those copies after the loop — dropping them returns
|
|
4872
5924
|
// a stale value. Read under the un-rotation substitution (post-loop the params hold their
|
|
4873
5925
|
// updated values), and structure the exit region under the same substitution so a post-loop
|
|
4874
5926
|
// use of a loop value reads its name.
|
|
4875
5927
|
out.push(
|
|
4876
|
-
...withSub(sub, () =>
|
|
5928
|
+
...withSub(sub, () => {
|
|
5929
|
+
const copies = argAssigns(li.header, li.exit, sub, keptSlot);
|
|
5930
|
+
const prevStale = zeroTripStale;
|
|
5931
|
+
zeroTripStale = prevStale ? new Map([...prevStale, ...stale]) : stale;
|
|
5932
|
+
try {
|
|
5933
|
+
return [...copies, ...structureRegion(li.exit, stop)];
|
|
5934
|
+
} finally {
|
|
5935
|
+
zeroTripStale = prevStale;
|
|
5936
|
+
}
|
|
5937
|
+
}),
|
|
4877
5938
|
);
|
|
4878
5939
|
return out;
|
|
4879
5940
|
}
|
|
@@ -4892,19 +5953,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4892
5953
|
// back-edge arm is the implicit continue (control falls to the loop bottom). Guarded to leaving
|
|
4893
5954
|
// edges only (`!body.has(exitB)` AND a break/return target); an in-body conditional continue still
|
|
4894
5955
|
// declines (falls through → the header re-entry trips `onStack`, an honest loud fail).
|
|
4895
|
-
|
|
5956
|
+
//
|
|
5957
|
+
// Taken only where the implicit continue holds (`continueFallsToBottom`). Where it does not, a
|
|
5958
|
+
// break edge is refused by name below, and an arm re-enters the header and declines loud.
|
|
5959
|
+
if (loopCtx && continueFallsToBottom(loopCtx, stop) && (takenB === loopCtx.header || fallB === loopCtx.header)) {
|
|
4896
5960
|
const contIsTaken = takenB === loopCtx.header;
|
|
4897
5961
|
const exitB = contIsTaken ? fallB : takenB;
|
|
4898
5962
|
const isBreak = exitB === loopCtx.exit;
|
|
4899
|
-
// SOUNDNESS (break-clobber): a structured `break` jumps to AFTER the loop, where the header→exit
|
|
4900
|
-
// phi copies are emitted (emitTestAtTopWhile / emitDoWhile). If those copies are NON-identity, the
|
|
4901
|
-
// break path would fall through them and CLOBBER the value the break carried into the exit param.
|
|
4902
|
-
// Only emit `break` for a WHILE header whose header→exit copy is empty (the exit param already
|
|
4903
|
-
// coalesces across both edges); otherwise decline (fall through → honest loud fail). A do-while
|
|
4904
|
-
// `break` declines here (its exit copies live post-loop too, but the check differs) — the
|
|
4905
|
-
// return-trampoline path below still serves both. Trampolines are immune (the `return` terminates
|
|
4906
|
-
// the arm, so nothing falls through).
|
|
4907
|
-
const breakSafe = whileLoops.has(loopCtx.header) && argAssigns(loopCtx.header, loopCtx.exit).length === 0;
|
|
4908
5963
|
// The back-edge substitution: each header param's back-edge arg → the param's name, so a test that
|
|
4909
5964
|
// reads a POST-update value (the value carried to the header) shows the header var name.
|
|
4910
5965
|
const sub = subFor(loopCtx.header.params, successorTo(b, loopCtx.header)!.args);
|
|
@@ -4928,21 +5983,75 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4928
5983
|
// `if (found) { *out = i; return; }` would store `i + 1`. Handing the arm's own region makes
|
|
4929
5984
|
// the escape check judge exactly those reads.
|
|
4930
5985
|
const exitRegion = new Set([exitB, ...reachFrom(exitB)].filter((x) => !loopCtx!.body.has(x)));
|
|
5986
|
+
// A `break` is judged by the break rule, with the update among the writes ahead of it: the
|
|
5987
|
+
// exit region renders raw after the loop, so a header value re-derived there from a name the
|
|
5988
|
+
// update wrote is computed once more — `do { b = t / (b + t); if (c > 2) break; } while
|
|
5989
|
+
// (--d > 0); return b;` renders `return t / (v + t)` after `v = t / (v + t)` already ran. An
|
|
5990
|
+
// early `return` needs none of it: its `return` ends the arm, so nothing after the loop runs.
|
|
4931
5991
|
const hazard = loopUpdateHazard(term.operands[0], exitArgs, loopCtx.body, sub, updateWrites, exitRegion);
|
|
4932
|
-
|
|
5992
|
+
const rule = isBreak ? breakRule(loopCtx, b, new Set([...updateWrites, ...writtenAhead(loopCtx, b)])) : null;
|
|
5993
|
+
if (!hazard && !loopCtx.body.has(exitB) && (rule ? 'bare' in rule : isArm(exitB))) {
|
|
4933
5994
|
out.push(...updateCopies); // the loop update, RAW (i++, p>>=1, …)
|
|
4934
5995
|
let leaveCond = exprWith(sub)(term.operands[0]);
|
|
4935
5996
|
if (contIsTaken) {
|
|
4936
5997
|
leaveCond = negateCond(leaveCond);
|
|
4937
5998
|
} // continue is `taken` → leave when NOT it
|
|
4938
|
-
const exitArm =
|
|
4939
|
-
|
|
4940
|
-
|
|
5999
|
+
const exitArm =
|
|
6000
|
+
rule && 'bare' in rule
|
|
6001
|
+
? [...(rule.bare ? [] : argAssigns(b, loopCtx.exit, sub)), { k: 'break' } as Stmt] // break to the loop exit
|
|
6002
|
+
: withSub(sub, () => [...argAssigns(b, exitB, sub), ...structureRegion(exitB, stop)]); // early return
|
|
4941
6003
|
out.push(mkIf(leaveCond, exitArm, []));
|
|
4942
6004
|
return out;
|
|
4943
6005
|
}
|
|
4944
6006
|
}
|
|
4945
6007
|
|
|
6008
|
+
// A `break` from INSIDE the body: one edge of this cond_br is an admitted break edge (it lands on
|
|
6009
|
+
// the `while`'s own exit). Spelled `if (c) { <edge copies>; break; }` and then what the other edge
|
|
6010
|
+
// runs, which is what the asm runs: the exit region renders once, after the loop, for the
|
|
6011
|
+
// header's exit and this edge alike. The break leaves before any update copy on this path, so
|
|
6012
|
+
// the loop variables still hold what the header read, unless a block between the header and here
|
|
6013
|
+
// wrote one of their names.
|
|
6014
|
+
//
|
|
6015
|
+
// The other edge may be the BACK edge, where the latch path above refused (its update-first
|
|
6016
|
+
// spelling would let the test or the exit read a value the update overwrites). The update copies
|
|
6017
|
+
// then follow the `if`, where the break has already left — the same C as when the update sits in
|
|
6018
|
+
// a block of its own, which is a layout accident.
|
|
6019
|
+
//
|
|
6020
|
+
// Refused: by the break rule over the names written ahead of this edge; where the other edge
|
|
6021
|
+
// leaves the body too; and where the other edge is the back edge but the implicit continue does
|
|
6022
|
+
// not hold (`continueFallsToBottom`) — every route to the header re-enters it, so that refusal is
|
|
6023
|
+
// always loud. The others are loud, since the loop was admitted on the promise of this spelling,
|
|
6024
|
+
// unless an arm owns the edge or the exit ends in a `ret`: then if-recovery below copies the
|
|
6025
|
+
// exit's tail into the arm and returns.
|
|
6026
|
+
// A `switch` case body never holds one: the exit is a block no switch in the body dominates,
|
|
6027
|
+
// and both switch regimes refuse an arm that reaches such a block (`analyzeArmExit`).
|
|
6028
|
+
if (loopCtx && loopCtx.breaks.has(b)) {
|
|
6029
|
+
const frame = loopCtx;
|
|
6030
|
+
const breakIsTaken = takenB === frame.exit;
|
|
6031
|
+
const stayB = breakIsTaken ? fallB : takenB;
|
|
6032
|
+
const continueRefused = stayB === frame.header && !continueFallsToBottom(frame, stop);
|
|
6033
|
+
const rule = continueRefused
|
|
6034
|
+
? { refusal: 'would continue the loop from a region that ends past the loop bottom' }
|
|
6035
|
+
: frame.body.has(stayB)
|
|
6036
|
+
? breakRule(frame, b, writtenAhead(frame, b))
|
|
6037
|
+
: { refusal: 'has no edge beside it that stays in the loop' };
|
|
6038
|
+
if ('bare' in rule) {
|
|
6039
|
+
let leaveCond = expr(term.operands[0]);
|
|
6040
|
+
if (!breakIsTaken) {
|
|
6041
|
+
leaveCond = negateCond(leaveCond);
|
|
6042
|
+
}
|
|
6043
|
+
const [breakEdge, stayEdge] = breakIsTaken ? term.successors : [term.successors[1], term.successors[0]];
|
|
6044
|
+
out.push(mkIf(leaveCond, [...(rule.bare ? [] : argAssignsFor(b, breakEdge)), { k: 'break' }], []));
|
|
6045
|
+
out.push(...argAssignsFor(b, stayEdge), ...structureRegion(stayB, stop));
|
|
6046
|
+
return out;
|
|
6047
|
+
}
|
|
6048
|
+
if (continueRefused || (!isArm(frame.exit) && !isRet(frame.exit))) {
|
|
6049
|
+
throw new StructureError(
|
|
6050
|
+
`cannot structure '${fn.name}': a break out of block #${fn.blocks.indexOf(b)} ${rule.refusal}`,
|
|
6051
|
+
);
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
6054
|
+
|
|
4946
6055
|
// Regime-A switch: if this cond_br roots a comparison tree over a single scrutinee, emit a
|
|
4947
6056
|
// `switch`. A pre-check here — mirroring the guard-fused-loop check above — so it sees the raw
|
|
4948
6057
|
// tree before if-recovery claims the diamonds. Declines (null) fall through to plain if-recovery.
|
|
@@ -4979,7 +6088,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4979
6088
|
!loopCtx.body.has(ipd) &&
|
|
4980
6089
|
term.successors.some((sc) => isArm(sc.block) || sc.block === stop) &&
|
|
4981
6090
|
term.successors.every((sc) => loopCtx!.body.has(sc.block) || isArm(sc.block));
|
|
4982
|
-
|
|
6091
|
+
// In a `while (1)` body the join is the body's own, which the CFG's post-dominance cannot see
|
|
6092
|
+
// (`foreverJoin`): a branch whose one arm continues is post-dominated, in the CFG, by wherever
|
|
6093
|
+
// the other arm leaves the loop from.
|
|
6094
|
+
const inForever = loopCtx === null ? undefined : foreverLoops.get(loopCtx.header);
|
|
6095
|
+
const bodyJoin = inForever === undefined ? undefined : foreverJoin(inForever, b);
|
|
6096
|
+
const merge = bodyJoin !== undefined ? (bodyJoin ?? stop) : clampToLoop ? stop : (ipd ?? stop);
|
|
4983
6097
|
// Per-successor records, NOT successorTo(b, block): a cond_br whose two edges reach the SAME
|
|
4984
6098
|
// block with different args would otherwise give both arms the first edge's copies.
|
|
4985
6099
|
const thenS = [...argAssignsFor(b, term.successors[0]), ...structureRegion(takenB, merge)];
|
|
@@ -5088,7 +6202,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5088
6202
|
negateHere = branchSenseFlipSites?.has(ord) ? !siteDefault : siteDefault;
|
|
5089
6203
|
hooks.onBranchSenseSite?.({ block: bi, ordinal: ord, joined: ipd !== null, negated: negateHere });
|
|
5090
6204
|
}
|
|
5091
|
-
|
|
6205
|
+
if (
|
|
6206
|
+
stmtsEqual(thenS, elseS) &&
|
|
6207
|
+
testRereadsOnly(term, b, { defs, useSitesOf, dom, ipdom, loops: forest.byHeader.values() })
|
|
6208
|
+
) {
|
|
6209
|
+
out.push(...thenS);
|
|
6210
|
+
} else {
|
|
6211
|
+
out.push(negateHere ? { k: 'if', cond: negateCond(cond), then: elseS, else: thenS } : mkIf(cond, thenS, elseS));
|
|
6212
|
+
}
|
|
5092
6213
|
if (merge && merge !== stop) {
|
|
5093
6214
|
out.push(...structureRegion(merge, stop));
|
|
5094
6215
|
}
|
|
@@ -5105,10 +6226,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5105
6226
|
};
|
|
5106
6227
|
const loopSub = (li: LoopInfo): Map<Value, string> => subFor(li.header.params, li.backArgOfParam);
|
|
5107
6228
|
|
|
5108
|
-
// The sunk exit copies, as body statements: `dest = <the arg, rebuilt here
|
|
5109
|
-
//
|
|
5110
|
-
//
|
|
5111
|
-
//
|
|
6229
|
+
// The sunk exit copies, as body statements: `dest = <the arg, rebuilt here>`, SPLIT by where in
|
|
6230
|
+
// the body each one belongs. WHERE is not recomputed here: `sinkablePreUpdateSlots` cleared each
|
|
6231
|
+
// slot at one position and hands it back, so `atDef` keys the ones homed at a latch op by that op,
|
|
6232
|
+
// for that block's `sideEffects` walk, and `leading` opens the body with the rest. The sink's gates
|
|
6233
|
+
// are stated against `exprWith(null)` — every name the walk stops at holds, anywhere in the body
|
|
6234
|
+
// ahead of the update, what it held where the edge read it — so the arg is spelled with no
|
|
6235
|
+
// substitution. Slot order keeps it deterministic.
|
|
5112
6236
|
//
|
|
5113
6237
|
// NOT `expr`: an ambient `activeSub` is an ENCLOSING loop's post-loop naming, which the sink's
|
|
5114
6238
|
// walk does not model, so a REBUILT tree here would be spelled under names that substitution has
|
|
@@ -5119,27 +6243,63 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5119
6243
|
// The destination name is read as an invariant, not checked: every block param carries one.
|
|
5120
6244
|
// `assertResolved` is what catches a widening that breaks that.
|
|
5121
6245
|
//
|
|
5122
|
-
// `home` is the loop HEADER
|
|
5123
|
-
// the
|
|
5124
|
-
// `sunkCopyHomes`).
|
|
5125
|
-
const preUpdateCopies = (
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
6246
|
+
// `home` is the loop HEADER, which dominates every point inside the body a copy is rebuilt at, and
|
|
6247
|
+
// so the block from which every one of these writes is reachable. Recorded rather than inferred
|
|
6248
|
+
// because `exit` says the opposite (see `sunkCopyHomes`).
|
|
6249
|
+
const preUpdateCopies = (
|
|
6250
|
+
exit: Block,
|
|
6251
|
+
exitArgs: readonly Value[],
|
|
6252
|
+
sunk: ReadonlyMap<number, Op | null>,
|
|
6253
|
+
home: Block,
|
|
6254
|
+
): SunkCopies => {
|
|
6255
|
+
const out: SunkCopies = { leading: [], atDef: new Map() };
|
|
6256
|
+
for (const [j, at] of [...sunk].sort(([x], [y]) => x - y)) {
|
|
6257
|
+
if (activeSub !== null && !varName.has(exitArgs[j])) {
|
|
6258
|
+
throw new StructureError(
|
|
6259
|
+
`cannot structure '${fn.name}': a pre-update exit copy would rebuild a computed value ` +
|
|
6260
|
+
`inside a loop nested in another loop's post-loop naming`,
|
|
6261
|
+
);
|
|
6262
|
+
}
|
|
6263
|
+
const name = varName.get(exit.params[j])!;
|
|
6264
|
+
sunkCopyHomes.push({ name, home });
|
|
6265
|
+
// A value the body already names under the destination writes it where it is computed, so
|
|
6266
|
+
// its copy would be `dest = dest`: the slot needs only the seed and the dropped post-loop copy.
|
|
6267
|
+
if (varName.get(exitArgs[j]) === name) {
|
|
6268
|
+
continue;
|
|
6269
|
+
}
|
|
6270
|
+
const st = { k: 'assign' as const, name, value: exprWith(null)(exitArgs[j]) };
|
|
6271
|
+
if (at === null) {
|
|
6272
|
+
out.leading.push(st);
|
|
6273
|
+
} else {
|
|
6274
|
+
(out.atDef.get(at) ?? out.atDef.set(at, []).get(at)!).push(st);
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
return out;
|
|
6278
|
+
};
|
|
6279
|
+
|
|
6280
|
+
/** Every copy `preUpdateCopies` placed at a def, once the body that was meant to hold it is
|
|
6281
|
+
* assembled. FORWARD DEFENCE WITH NO REACH TODAY: both emitters hand `atDef` to `sideEffects` on
|
|
6282
|
+
* the very block the gate homed those copies in, and that walk visits every op the block holds,
|
|
6283
|
+
* so no key can go unvisited. It is stated rather than assumed because what it names is silent —
|
|
6284
|
+
* a body assembled without that block's side effects, or handing the copies to another block's
|
|
6285
|
+
* walk, drops a copy, and a dropped copy is a wrong value rather than a gap.
|
|
6286
|
+
*
|
|
6287
|
+
* STRICT, where the anchored-copy obligation below tests OBSERVABILITY instead, and the
|
|
6288
|
+
* difference is which position is guaranteed. An anchor's position is a const's def block, which
|
|
6289
|
+
* may legitimately render no statement at all — a loop preheader the guarded-loop emitter never
|
|
6290
|
+
* structures is one — so a copy that vanishes there can still leave a consistent program. A sunk
|
|
6291
|
+
* copy's position is in a block both emitters render whatever else they do, because that block's
|
|
6292
|
+
* side effects carry the loop's stores; a copy missing from it is a mistake in assembling the
|
|
6293
|
+
* body and has no second reading. */
|
|
6294
|
+
const assertSunkCopiesPlaced = (copies: SunkCopies): void => {
|
|
6295
|
+
for (const st of [...copies.atDef.values()].flat()) {
|
|
6296
|
+
if (!sunkCopiesEmitted.has(st)) {
|
|
6297
|
+
throw new StructureError(
|
|
6298
|
+
`cannot structure '${fn.name}': a pre-update exit copy has no position in the loop body it belongs to`,
|
|
6299
|
+
);
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
};
|
|
5143
6303
|
|
|
5144
6304
|
// A guarded self-loop's body and latch test. The test reads the header's own params (back-edge
|
|
5145
6305
|
// args substituted back), and the body is any sunk trailing copies, then the header's SIDE
|
|
@@ -5154,16 +6314,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5154
6314
|
// source spelling means.
|
|
5155
6315
|
const emitWhile = (
|
|
5156
6316
|
li: LoopInfo,
|
|
5157
|
-
updates
|
|
5158
|
-
sunkCopies:
|
|
5159
|
-
kind: 'while' | 'dowhile'
|
|
6317
|
+
updates: Stmt[] | undefined,
|
|
6318
|
+
sunkCopies: SunkCopies,
|
|
6319
|
+
kind: 'while' | 'dowhile',
|
|
5160
6320
|
): Stmt => {
|
|
5161
6321
|
const term = li.header.ops[li.header.ops.length - 1];
|
|
5162
6322
|
let cond = exprWith(loopSub(li))(term.operands[0]);
|
|
5163
6323
|
if (term.successors[0].block !== li.header) {
|
|
5164
6324
|
cond = negateCond(cond);
|
|
5165
6325
|
} // loop-continue must be `taken`
|
|
5166
|
-
const body = [
|
|
6326
|
+
const body = [
|
|
6327
|
+
...sunkCopies.leading,
|
|
6328
|
+
...sideEffects(li.header, sunkCopies.atDef),
|
|
6329
|
+
...(updates ?? argAssigns(li.header, li.header)),
|
|
6330
|
+
];
|
|
6331
|
+
assertSunkCopiesPlaced(sunkCopies);
|
|
5167
6332
|
return kind === 'while' ? { k: 'while', cond, body } : { k: 'dowhile', cond, body };
|
|
5168
6333
|
};
|
|
5169
6334
|
|
|
@@ -5182,7 +6347,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5182
6347
|
// Mirror the br/cond_br cases: argAssigns then structureRegion. Structure the body under this
|
|
5183
6348
|
// loop's frame so an in-body conditional exit (break / early return) is recognised instead of
|
|
5184
6349
|
// tripping the header-re-entry `onStack` guard.
|
|
5185
|
-
const body = withLoop({ header: wl.header, exit: wl.exit, body: wl.body, arms: wl.arms }, () => [
|
|
6350
|
+
const body = withLoop({ header: wl.header, exit: wl.exit, body: wl.body, arms: wl.arms, breaks: wl.breaks }, () => [
|
|
5186
6351
|
...argAssigns(wl.header, wl.bodyEntry),
|
|
5187
6352
|
...structureRegion(wl.bodyEntry, wl.header),
|
|
5188
6353
|
]);
|
|
@@ -5192,6 +6357,105 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5192
6357
|
return out;
|
|
5193
6358
|
};
|
|
5194
6359
|
|
|
6360
|
+
// THE JOIN OF A BRANCH IN A `while (1)` BODY, over the body with its back edges and exits deleted.
|
|
6361
|
+
// A block that only jumps back to the header or out of the loop, rendering nothing but that edge's
|
|
6362
|
+
// copies, ENDS its path — a `continue`, a `break` — so it constrains no join; the join of `b` is
|
|
6363
|
+
// where the paths that go on meet: its nearest post-dominator over the rest. Null when none goes
|
|
6364
|
+
// on, and the region's own `stop` ends it. Without the deletion, a branch whose one arm continues
|
|
6365
|
+
// joins at the loop bottom, and whatever the other arm runs before its own continue is copied into
|
|
6366
|
+
// both arms. A block that ends every path but TESTS first is still where its predecessors meet,
|
|
6367
|
+
// and so is a latch two body paths reach: the foot of `for (…) { if (c) {…} }`, where deleting it
|
|
6368
|
+
// moves the `if`'s join into one arm, which then copies the latch and adds a `continue` the
|
|
6369
|
+
// source never wrote. A break block two paths share stays an end, each path its own `break`.
|
|
6370
|
+
const foreverJoins = new Map<Block, Map<Block, Block | null>>();
|
|
6371
|
+
const foreverJoin = (fl: ForeverLoopInfo, b: Block): Block | null => {
|
|
6372
|
+
let ipdom = foreverJoins.get(fl.header);
|
|
6373
|
+
if (ipdom === undefined) {
|
|
6374
|
+
const inBody = (n: Block): Block[] =>
|
|
6375
|
+
[...new Set(successorsOf(n))].filter((x) => fl.body.has(x) && x !== fl.header);
|
|
6376
|
+
const ends = (n: Block): boolean =>
|
|
6377
|
+
inBody(n).length === 0 &&
|
|
6378
|
+
n.ops[n.ops.length - 1].opcode === 'br' &&
|
|
6379
|
+
n.ops.every((op) => !effectful(op) && !materialize.has(op)) &&
|
|
6380
|
+
!(successorsOf(n)[0] === fl.header && new Set(preds.get(n)?.filter((q) => fl.body.has(q))).size > 1);
|
|
6381
|
+
ipdom = postDominators(fn, fl.body, (n) => inBody(n).filter((x) => !ends(x)));
|
|
6382
|
+
foreverJoins.set(fl.header, ipdom);
|
|
6383
|
+
}
|
|
6384
|
+
return ipdom.get(b) ?? null;
|
|
6385
|
+
};
|
|
6386
|
+
|
|
6387
|
+
// Does rendering `root` under `map` re-derive one of `targets`: reach it through the def tree
|
|
6388
|
+
// without passing a mapped or a named value, either of which renders as a name?
|
|
6389
|
+
const rendersThrough = (
|
|
6390
|
+
root: Value,
|
|
6391
|
+
map: ReadonlyMap<Value, string> | null,
|
|
6392
|
+
targets: ReadonlySet<Value>,
|
|
6393
|
+
): boolean => {
|
|
6394
|
+
const seen = new Set<Value>();
|
|
6395
|
+
const walk = (x: Value): boolean => {
|
|
6396
|
+
if (seen.has(x) || map?.has(x) === true || varName.has(x)) {
|
|
6397
|
+
return false;
|
|
6398
|
+
}
|
|
6399
|
+
if (targets.has(x)) {
|
|
6400
|
+
return true;
|
|
6401
|
+
}
|
|
6402
|
+
seen.add(x);
|
|
6403
|
+
return defs.get(x)?.operands.some(walk) ?? false;
|
|
6404
|
+
};
|
|
6405
|
+
return walk(root);
|
|
6406
|
+
};
|
|
6407
|
+
// What a loop's exit region renders after the loop, before `stop`, and the values it reads.
|
|
6408
|
+
const exitRegion = (exit: Block, loopBody: ReadonlySet<Block>, stop: Block | null): Set<Block> => {
|
|
6409
|
+
const region = new Set<Block>();
|
|
6410
|
+
for (const work = [exit]; work.length > 0;) {
|
|
6411
|
+
const x = work.pop()!;
|
|
6412
|
+
if (x !== stop && !loopBody.has(x) && !region.has(x)) {
|
|
6413
|
+
region.add(x);
|
|
6414
|
+
work.push(...successorsOf(x));
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
return region;
|
|
6418
|
+
};
|
|
6419
|
+
const rootsOf = (region: Iterable<Block>): Value[] =>
|
|
6420
|
+
[...region].flatMap((x) => x.ops.flatMap((op) => [...op.operands, ...op.successors.flatMap((sc) => sc.args)]));
|
|
6421
|
+
|
|
6422
|
+
// A `break` of the loop whose body `ss` is — not of a loop or `switch` inside it.
|
|
6423
|
+
const breaksOut = (ss: Stmt[]): boolean =>
|
|
6424
|
+
ss.some((s) => s.k === 'break' || (!isLoop(s) && s.k !== 'switch' && breaksOut(stmtChildren(s))));
|
|
6425
|
+
// `while (1) { body }`: the header structured as an ordinary block up to itself (its do-while-style
|
|
6426
|
+
// hook masked via dwActive), under a frame whose breaks leave for `exit`, which renders after it.
|
|
6427
|
+
// Where no edge out rendered as a `break` — each one an early `return`, the exit's tail copied
|
|
6428
|
+
// into it — nothing reaches the end of the loop, and the exit rendered after it would be a dead
|
|
6429
|
+
// second copy of that tail.
|
|
6430
|
+
//
|
|
6431
|
+
// The exit region runs after every break, so an inner loop that runs before each of them has left
|
|
6432
|
+
// its last value in its variable's name, and the region reads it there (`latchInnerSub`, with the
|
|
6433
|
+
// breaks as its readers). Read raw it would re-derive that value from the name that holds it. Where
|
|
6434
|
+
// neither reading is the value, decline LOUD, as the do-while latch does.
|
|
6435
|
+
const emitForever = (fl: ForeverLoopInfo, stop: Block | null): Stmt[] => {
|
|
6436
|
+
dwActive.add(fl.header);
|
|
6437
|
+
const body = withLoop({ header: fl.header, exit: fl.exit, body: fl.body, arms: fl.arms, breaks: fl.breaks }, () =>
|
|
6438
|
+
structureBlock(fl.header, fl.header),
|
|
6439
|
+
);
|
|
6440
|
+
dwActive.delete(fl.header);
|
|
6441
|
+
const loop: Stmt = { k: 'while', cond: { k: 'const', value: 1 }, body };
|
|
6442
|
+
if (!breaksOut(body)) {
|
|
6443
|
+
return [loop];
|
|
6444
|
+
}
|
|
6445
|
+
const region = exitRegion(fl.exit, fl.body, stop);
|
|
6446
|
+
const { sub: innerSub, unreadable } = latchInnerSub(fl.header, fl.body, [...fl.breaks], region);
|
|
6447
|
+
if (unreadable.size > 0 && rootsOf(region).some((r) => rendersThrough(r, innerSub, unreadable))) {
|
|
6448
|
+
throw new StructureError(
|
|
6449
|
+
`cannot structure '${fn.name}': the code after a while (1) reads an inner loop's value whose name was ` +
|
|
6450
|
+
`rewritten after the inner loop, and re-deriving it reads a name the inner loop wrote`,
|
|
6451
|
+
);
|
|
6452
|
+
}
|
|
6453
|
+
return [
|
|
6454
|
+
loop,
|
|
6455
|
+
...(innerSub.size > 0 ? withSub(innerSub, () => structureRegion(fl.exit, stop)) : structureRegion(fl.exit, stop)),
|
|
6456
|
+
];
|
|
6457
|
+
};
|
|
6458
|
+
|
|
5195
6459
|
// The latch back-edge substitution (do-while) — subFor over the latch's back-edge args.
|
|
5196
6460
|
const latchSub = (dw: DoWhileInfo): Map<Value, string> =>
|
|
5197
6461
|
subFor(dw.header.params, successorTo(dw.latch, dw.header)!.args);
|
|
@@ -5262,16 +6526,24 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5262
6526
|
const ins = [...inEdgeRecords(preds, b)];
|
|
5263
6527
|
return ins.length > 0 && ins.every(({ succ }) => aliasOf(succ.args[k], v, seen));
|
|
5264
6528
|
};
|
|
6529
|
+
// `readers` are the blocks the substitution is read after: the latch, or every block a `while (1)`
|
|
6530
|
+
// breaks from, whose exit region renders after the loop. `after` adds the blocks of that region,
|
|
6531
|
+
// whose writes land before some of its reads.
|
|
5265
6532
|
const latchInnerSub = (
|
|
5266
|
-
|
|
6533
|
+
header: Block,
|
|
6534
|
+
loopBody: ReadonlySet<Block>,
|
|
6535
|
+
readers: Block[],
|
|
6536
|
+
after: ReadonlySet<Block> = new Set(),
|
|
5267
6537
|
): { sub: Map<Value, string>; unreadable: Set<Value>; writtenAfter: Map<Value, Set<string>> } => {
|
|
5268
6538
|
const out = new Map<Value, string>();
|
|
5269
6539
|
const refused = new Set<Value>();
|
|
5270
6540
|
const writtenAfter = new Map<Value, Set<string>>();
|
|
5271
|
-
const latchDoms = dom.get(dw.latch)!;
|
|
5272
6541
|
const kids = [...forest.byHeader.values()]
|
|
5273
6542
|
.filter(
|
|
5274
|
-
(l) =>
|
|
6543
|
+
(l) =>
|
|
6544
|
+
l.header !== header &&
|
|
6545
|
+
loopBody.has(l.header) &&
|
|
6546
|
+
readers.every((r) => !l.body.has(r) && dom.get(r)!.has(l.header)),
|
|
5275
6547
|
)
|
|
5276
6548
|
.sort((x, y) => dom.get(x.header)!.size - dom.get(y.header)!.size);
|
|
5277
6549
|
for (const l of kids) {
|
|
@@ -5286,7 +6558,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5286
6558
|
for (const [v, n] of varName) {
|
|
5287
6559
|
const d = defs.get(v);
|
|
5288
6560
|
const home = paramBlock.get(v) ?? (d !== undefined && materialize.has(d) ? opBlock.get(d) : undefined);
|
|
5289
|
-
if (home === undefined || !
|
|
6561
|
+
if (home === undefined || !(loopBody.has(home) || after.has(home)) || home === header) {
|
|
5290
6562
|
continue;
|
|
5291
6563
|
}
|
|
5292
6564
|
written.add(n);
|
|
@@ -5332,30 +6604,28 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5332
6604
|
// then every line below spells what it did before the substitution existed — unless an unnamed
|
|
5333
6605
|
// value an inner loop computed could reach the latch.
|
|
5334
6606
|
//
|
|
5335
|
-
// The update copies take it MERGED with `activeSub
|
|
5336
|
-
//
|
|
5337
|
-
//
|
|
5338
|
-
//
|
|
5339
|
-
|
|
5340
|
-
// two programs are the same, and it is left conditional so an empty `innerSub` keeps the line
|
|
5341
|
-
// exactly as it was (the corpus census is byte-identical either side of this commit's parent).
|
|
5342
|
-
const { sub: innerSub, unreadable, writtenAfter } = latchInnerSub(dw);
|
|
6607
|
+
// The update copies take it MERGED with `activeSub`. A map extends the ambient naming where it
|
|
6608
|
+
// renders (`exprWith`), but identity elision consults the map alone, so the merge is what drops
|
|
6609
|
+
// a copy that `activeSub` spells `n = n` rather than writing it. The two programs are the same,
|
|
6610
|
+
// and it is left conditional so an empty `innerSub` takes the map-less path.
|
|
6611
|
+
const { sub: innerSub, unreadable, writtenAfter } = latchInnerSub(dw.header, dw.body, [dw.latch]);
|
|
5343
6612
|
const latchMap = innerSub.size > 0 ? new Map([...(activeSub ?? []), ...innerSub]) : null;
|
|
5344
6613
|
const updates = argAssigns(dw.latch, dw.header, latchMap);
|
|
5345
6614
|
const updateWrites = loopWriteSet(updates, dw.body, dw.header);
|
|
5346
6615
|
const lterm = dw.latch.ops[dw.latch.ops.length - 1];
|
|
5347
|
-
//
|
|
5348
|
-
// block's param
|
|
5349
|
-
//
|
|
5350
|
-
//
|
|
5351
|
-
//
|
|
5352
|
-
// header
|
|
6616
|
+
// A DEFENSIVE GUARD, and the reason the sink stands down rather than repairing anything. A body
|
|
6617
|
+
// block's param that holds a LOOP VARIABLE's name makes the arm's copy into it a real write
|
|
6618
|
+
// partway through the body, and everything rendered after it reads the name RAW: the update,
|
|
6619
|
+
// the bottom test, the header's own ops, the latch's side effects. The pure-alias route to such
|
|
6620
|
+
// a name is closed (`alias-under-its-own-name`); a param may still take it through `varName`
|
|
6621
|
+
// once the header param is dead, and no fixture reaches that today, so this guard is defensive.
|
|
5353
6622
|
//
|
|
5354
6623
|
// The refusal is on the NAME, not on who reads it: the readers are every statement the loop
|
|
5355
6624
|
// emits, which is not a set worth enumerating when the name alone is the whole signal.
|
|
5356
6625
|
//
|
|
5357
|
-
// The emitted C
|
|
5358
|
-
//
|
|
6626
|
+
// The emitted C can be wrong wherever this shape occurs, sink or no sink (the predicate is on
|
|
6627
|
+
// names, so it also holds on loops that emit correctly) — a naming-pipeline defect, not this
|
|
6628
|
+
// pass's, and repairing the exit copy does not touch it. What IS this pass's
|
|
5359
6629
|
// is not to UNLOCK such a loop: with no sink these functions decline on the pre-update hazard,
|
|
5360
6630
|
// so standing down keeps them loud rather than trading a decline for a silent wrong answer.
|
|
5361
6631
|
const headerNames = new Set(dw.header.params.map((p) => varName.get(p)));
|
|
@@ -5380,12 +6650,22 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5380
6650
|
}
|
|
5381
6651
|
const rebindHazard = [...bodyRebinds].some((n) => headerNames.has(n));
|
|
5382
6652
|
const exitArgs = (successorTo(dw.latch, dw.exit)?.args ?? []) as Value[];
|
|
5383
|
-
// A pre-update exit copy moves
|
|
5384
|
-
// their top-of-iteration values. No zero-trip seed here: a `do-while` always runs
|
|
5385
|
-
// any other predecessor of the exit is an ordinary edge some enclosing `if`
|
|
6653
|
+
// A pre-update exit copy moves INSIDE the body, ahead of the update, where the loop variables
|
|
6654
|
+
// still hold their top-of-iteration values. No zero-trip seed here: a `do-while` always runs
|
|
6655
|
+
// its body, so any other predecessor of the exit is an ordinary edge some enclosing `if`
|
|
6656
|
+
// already emits.
|
|
5386
6657
|
const sunk = rebindHazard
|
|
5387
|
-
? new
|
|
5388
|
-
: sinkablePreUpdateSlots(
|
|
6658
|
+
? new Map<number, Op | null>()
|
|
6659
|
+
: sinkablePreUpdateSlots(
|
|
6660
|
+
dw.header,
|
|
6661
|
+
dw.exit,
|
|
6662
|
+
exitArgs,
|
|
6663
|
+
dw.body,
|
|
6664
|
+
dw.latch,
|
|
6665
|
+
sub,
|
|
6666
|
+
updateWrites,
|
|
6667
|
+
hooks.preUpdateSinkGates ?? PREUPDATE_SINK_GATES,
|
|
6668
|
+
);
|
|
5389
6669
|
// The post-loop region the escaped-value check judges: everything the loop does not emit itself.
|
|
5390
6670
|
// An early-`return` arm the loop OWNS renders inside the body, ahead of the update, so a read of
|
|
5391
6671
|
// a loop variable there is the pre-update value it wants — counting it as post-loop would decline
|
|
@@ -5393,7 +6673,48 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5393
6673
|
// post-loop where the other path really does read them after the update.
|
|
5394
6674
|
const owned = new Set(dw.arms.flatMap((a) => [...a.owned]));
|
|
5395
6675
|
const postLoop = new Set(fn.blocks.filter((bb) => !dw.body.has(bb) && !owned.has(bb)));
|
|
6676
|
+
// AN EFFECT THE RENDERED TEST MAY SKIP. `movedEffect` below says this about the exit copies; the
|
|
6677
|
+
// bottom test is the other place an effect can change how often it runs, and here it is the
|
|
6678
|
+
// `&&`/`||` that moves it rather than a copy: the right operand is evaluated only where the left
|
|
6679
|
+
// one let it be, while the asm's branch did not guard the op at all. Nothing can re-place it —
|
|
6680
|
+
// the connective is what the test IS — so decline LOUD, the same answer for the same reason.
|
|
6681
|
+
//
|
|
6682
|
+
// Asked of every `do-while`, not only of a folded one: the arm holds the same call whichever way
|
|
6683
|
+
// the counter is spelled.
|
|
6684
|
+
if (testSkipsAnEffect(lterm.operands[0], sub)) {
|
|
6685
|
+
throw new StructureError(
|
|
6686
|
+
`cannot structure '${fn.name}': the bottom test may evaluate an effect behind a '&&'/'||' ` +
|
|
6687
|
+
`that the asm ran on every iteration`,
|
|
6688
|
+
);
|
|
6689
|
+
}
|
|
6690
|
+
// The test's OWN pre-update read has a spelling the other two disjuncts do not: `n++` at the
|
|
6691
|
+
// leaf, with the update copy dropped from the foot of the body. Asked only where the test is
|
|
6692
|
+
// already a hazard, so a loop that structures today enumerates exactly the candidates it did.
|
|
6693
|
+
// The polarity is the one applied to `cond` below — the continue edge must be the taken one.
|
|
6694
|
+
const condAnswer = readsClobbered(lterm.operands[0], sub, updateWrites)
|
|
6695
|
+
? preUpdateCondFold(
|
|
6696
|
+
lterm.operands[0],
|
|
6697
|
+
lterm.successors[1].block === dw.header,
|
|
6698
|
+
dw.header,
|
|
6699
|
+
successorTo(dw.latch, dw.header)?.args ?? [],
|
|
6700
|
+
exitArgs,
|
|
6701
|
+
dw.body,
|
|
6702
|
+
sub,
|
|
6703
|
+
updates,
|
|
6704
|
+
updateWrites,
|
|
6705
|
+
)
|
|
6706
|
+
: null;
|
|
6707
|
+
const condFold = condAnswer === null || 'refused' in condAnswer ? null : condAnswer;
|
|
6708
|
+
// A PRE-UPDATE HOME DOES NOT UNLOCK A LOOP whose variable's NAME a block after it also holds.
|
|
6709
|
+
// The exit region reads the loop's back-edge values under those names (`sub`), and such a
|
|
6710
|
+
// param's copies write the same name first — the merge naming's `carrier-live` sees live NAMED
|
|
6711
|
+
// values, not ones the substitution spells, so it does not refuse them. The spelling is wrong
|
|
6712
|
+
// wherever it occurs; it is the home's to keep from unlocking one, which without the name would
|
|
6713
|
+
// decline on this hazard.
|
|
6714
|
+
const homed = [...dw.body].some((bb) => bb.ops.some((o) => preUpdateHomes.has(o)));
|
|
6715
|
+
const sharesALoopName = homed && [...postLoop].some((bb) => bb.params.some((p) => headerNames.has(varName.get(p))));
|
|
5396
6716
|
if (
|
|
6717
|
+
sharesALoopName ||
|
|
5397
6718
|
loopUpdateHazard(
|
|
5398
6719
|
lterm.operands[0],
|
|
5399
6720
|
exitArgs.filter((_, j) => !sunk.has(j)),
|
|
@@ -5401,10 +6722,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5401
6722
|
sub,
|
|
5402
6723
|
updateWrites,
|
|
5403
6724
|
postLoop,
|
|
6725
|
+
condFold !== null,
|
|
5404
6726
|
)
|
|
5405
6727
|
) {
|
|
6728
|
+
// Three reasons share this refusal — the test, an exit slot, an escaped body value — and the
|
|
6729
|
+
// one that was asked in detail names the gate that answered.
|
|
5406
6730
|
throw new StructureError(
|
|
5407
|
-
`cannot structure '${fn.name}': do-while condition or a post-loop value reads a pre-update loop variable
|
|
6731
|
+
`cannot structure '${fn.name}': do-while condition or a post-loop value reads a pre-update loop variable` +
|
|
6732
|
+
(condAnswer !== null && 'refused' in condAnswer
|
|
6733
|
+
? ` (no '++' for the test's own read: ${condAnswer.refused})`
|
|
6734
|
+
: ''),
|
|
5408
6735
|
);
|
|
5409
6736
|
}
|
|
5410
6737
|
// The exit copies render AFTER the `dowhile` statement, but the analysis judged where each
|
|
@@ -5437,7 +6764,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5437
6764
|
const inner =
|
|
5438
6765
|
dw.header === dw.latch
|
|
5439
6766
|
? [] // single-block self-loop: the header IS the latch — its ops render via sideEffects below
|
|
5440
|
-
: withLoop({ header: dw.header, exit: dw.exit, body: dw.body, arms: dw.arms }, () =>
|
|
6767
|
+
: withLoop({ header: dw.header, exit: dw.exit, body: dw.body, arms: dw.arms, breaks: new Set() }, () =>
|
|
5441
6768
|
structureBlock(dw.header, dw.latch),
|
|
5442
6769
|
); // header..latch (exclusive of latch)
|
|
5443
6770
|
dwActive.delete(dw.header);
|
|
@@ -5446,17 +6773,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5446
6773
|
// already holds the next value, so the latch-computed test reads `v`, not `v - 1`). `updates`
|
|
5447
6774
|
// reuses the hazard check's computation — a second argAssigns call would burn a spurious
|
|
5448
6775
|
// swap-cycle temp number.
|
|
6776
|
+
const sunkCopies = preUpdateCopies(dw.exit, exitArgs, sunk, dw.header);
|
|
6777
|
+
const latchEffects = () => sideEffects(dw.latch, sunkCopies.atDef);
|
|
6778
|
+
// The folded update is spelled by the test's own `n++`, so it leaves the foot of the body.
|
|
6779
|
+
// `updates` itself is untouched: every hazard above, and `writtenByUpdate` below, ask what one
|
|
6780
|
+
// iteration WRITES, and the fold moves that write without removing it.
|
|
6781
|
+
const bodyUpdates =
|
|
6782
|
+
condFold === null ? updates : updates.filter((st) => !(st.k === 'assign' && st.name === condFold.name));
|
|
5449
6783
|
const body = [
|
|
5450
|
-
...
|
|
6784
|
+
...sunkCopies.leading,
|
|
5451
6785
|
...inner,
|
|
5452
|
-
...(innerSub.size > 0 ? withSub(innerSub,
|
|
5453
|
-
...
|
|
6786
|
+
...(innerSub.size > 0 ? withSub(innerSub, latchEffects) : latchEffects()),
|
|
6787
|
+
...bodyUpdates,
|
|
5454
6788
|
];
|
|
6789
|
+
assertSunkCopiesPlaced(sunkCopies);
|
|
5455
6790
|
// The test reads this loop's own update under `sub`, and anything else an inner loop left under
|
|
5456
6791
|
// `innerSub` — the outer loop's own reading wins where a value is both. The test runs AFTER the
|
|
5457
6792
|
// update copies, so an inner name one of them really writes no longer holds the inner value, and
|
|
5458
6793
|
// that entry keeps the raw reading — the same refusal `latchInnerSub` makes for a name written
|
|
5459
6794
|
// before the latch, with the update's writes added to what the re-derivation must not read.
|
|
6795
|
+
// The exit copies and the exit region run after the test and read under the same map: an inner
|
|
6796
|
+
// loop's value is still in its name there.
|
|
5460
6797
|
const writtenByUpdate = innerSub.size > 0 ? updateWriteSet(updates) : new Set<string>();
|
|
5461
6798
|
const condInner = [...innerSub].filter(([, n]) => !writtenByUpdate.has(n));
|
|
5462
6799
|
const condMap = condInner.length > 0 ? new Map([...condInner, ...sub]) : sub;
|
|
@@ -5474,31 +6811,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5474
6811
|
}
|
|
5475
6812
|
}
|
|
5476
6813
|
if (condUnreadable.size > 0) {
|
|
5477
|
-
const needs = (root: Value, stop: ReadonlyMap<Value, string> | null, targets: ReadonlySet<Value>): boolean => {
|
|
5478
|
-
const seen = new Set<Value>();
|
|
5479
|
-
const walk = (x: Value): boolean => {
|
|
5480
|
-
if (seen.has(x) || stop?.has(x) === true || varName.has(x)) {
|
|
5481
|
-
return false;
|
|
5482
|
-
}
|
|
5483
|
-
if (targets.has(x)) {
|
|
5484
|
-
return true;
|
|
5485
|
-
}
|
|
5486
|
-
seen.add(x);
|
|
5487
|
-
return defs.get(x)?.operands.some(walk) ?? false;
|
|
5488
|
-
};
|
|
5489
|
-
return walk(root);
|
|
5490
|
-
};
|
|
5491
6814
|
const bodyMap = latchMap ?? activeSub;
|
|
5492
6815
|
const effectRoots = dw.latch.ops
|
|
5493
6816
|
.slice(0, -1)
|
|
5494
|
-
.filter(
|
|
5495
|
-
(op) => op.results.length === 0 || materialize.has(op) || EFFECTFUL_OPS.has(op.opcode) || unreadResult(op),
|
|
5496
|
-
)
|
|
6817
|
+
.filter((op) => op.results.length === 0 || materialize.has(op) || effectful(op) || unreadResult(op))
|
|
5497
6818
|
.flatMap((op) => op.operands);
|
|
5498
6819
|
const updateRoots = successorTo(dw.latch, dw.header)!.args;
|
|
6820
|
+
const regionRoots = rootsOf(exitRegion(dw.exit, dw.body, stop));
|
|
5499
6821
|
if (
|
|
5500
|
-
[...effectRoots, ...updateRoots].some((r) =>
|
|
5501
|
-
|
|
6822
|
+
[...effectRoots, ...updateRoots].some((r) => rendersThrough(r, bodyMap, unreadable)) ||
|
|
6823
|
+
[lterm.operands[0], ...exitArgs, ...regionRoots].some((r) => rendersThrough(r, condMap, condUnreadable))
|
|
5502
6824
|
) {
|
|
5503
6825
|
throw new StructureError(
|
|
5504
6826
|
`cannot structure '${fn.name}': a loop latch reads an inner loop's value whose name was rewritten ` +
|
|
@@ -5510,11 +6832,25 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5510
6832
|
if (lterm.successors[1].block === dw.header) {
|
|
5511
6833
|
cond = negateCond(cond);
|
|
5512
6834
|
} // continue edge must be `taken`
|
|
6835
|
+
if (condFold !== null) {
|
|
6836
|
+
// AFTER the negation, so the leaf is placed in the tree that is emitted. `negateCond` rewrites
|
|
6837
|
+
// the connectives by De Morgan, which moves the leaf between arms without duplicating it — and
|
|
6838
|
+
// a rewrite that did duplicate it is exactly what `spellUpdateInCond` refuses.
|
|
6839
|
+
const folded = spellUpdateInCond(cond, condFold.name, condFold.by);
|
|
6840
|
+
if (folded === null) {
|
|
6841
|
+
throw new StructureError(
|
|
6842
|
+
`cannot structure '${fn.name}': the do-while test does not spell '${condFold.name}' exactly once, ` +
|
|
6843
|
+
`so its update has no place inside the test`,
|
|
6844
|
+
);
|
|
6845
|
+
}
|
|
6846
|
+
cond = folded;
|
|
6847
|
+
}
|
|
5513
6848
|
const out: Stmt[] = [{ k: 'dowhile', cond, body }];
|
|
5514
|
-
// The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars)
|
|
6849
|
+
// The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars),
|
|
6850
|
+
// and inner loops' values as the test does.
|
|
5515
6851
|
out.push(
|
|
5516
|
-
...withSub(
|
|
5517
|
-
...argAssigns(dw.latch, dw.exit,
|
|
6852
|
+
...withSub(condMap, () => [
|
|
6853
|
+
...argAssigns(dw.latch, dw.exit, condMap, (j) => !sunk.has(j)),
|
|
5518
6854
|
...structureRegion(dw.exit, stop),
|
|
5519
6855
|
]),
|
|
5520
6856
|
);
|
|
@@ -5544,8 +6880,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5544
6880
|
if (owed.length > 0) {
|
|
5545
6881
|
const mentioned = new Set<string>(globalNames);
|
|
5546
6882
|
for (const e of walkExprs(body)) {
|
|
5547
|
-
|
|
5548
|
-
|
|
6883
|
+
const n = mentionedName(e);
|
|
6884
|
+
if (n !== undefined) {
|
|
6885
|
+
mentioned.add(n);
|
|
5549
6886
|
}
|
|
5550
6887
|
}
|
|
5551
6888
|
const assignTargets = (ss: Stmt[]): void => {
|
|
@@ -5673,7 +7010,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5673
7010
|
lastLaddrOf.set(laddrName.get(op)!, op);
|
|
5674
7011
|
}
|
|
5675
7012
|
const structs = collectStructs(fn);
|
|
5676
|
-
|
|
7013
|
+
const tree: SFn = {
|
|
5677
7014
|
name: fn.name,
|
|
5678
7015
|
params: entry.params.map((p, i) => ({ name: `a${i}`, type: p.type })),
|
|
5679
7016
|
locals: [
|
|
@@ -5697,7 +7034,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5697
7034
|
// spill, is the referee — stamp them then, and `l3/slotorder.ts` needs no change to use it.
|
|
5698
7035
|
...[...lastLaddrOf].map(([name, op]) => ({
|
|
5699
7036
|
name,
|
|
5700
|
-
type:
|
|
7037
|
+
type: laddrType(op),
|
|
5701
7038
|
// the asm materialized this slot's address, and this is how many times it
|
|
5702
7039
|
// loaded and stored through it — both asm facts, and the gate the
|
|
5703
7040
|
// l3/volatileval.ts variation reads (see the SFn.locals doc)
|
|
@@ -5752,13 +7089,48 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5752
7089
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
5753
7090
|
}
|
|
5754
7091
|
: {}),
|
|
7092
|
+
...(localStatics.statics.length ? { statics: localStatics.statics } : {}),
|
|
5755
7093
|
retType: returnsVoid ? T.void() : returnType(fn),
|
|
5756
7094
|
body,
|
|
5757
7095
|
...(structs.length ? { structs } : {}),
|
|
5758
7096
|
// absent stays absent: the backend asks "is a direction known?", and there is no third state
|
|
5759
7097
|
// at this boundary — `structureOptionsFor` dropped the target's `'unknown'` already.
|
|
5760
7098
|
...(spillSlotOrder !== undefined ? { slotOrder: spillSlotOrder } : {}),
|
|
7099
|
+
...calledOnly({ declaredArgs, declaredReturns }, body),
|
|
7100
|
+
...(dialect !== undefined ? { dialect } : {}),
|
|
5761
7101
|
};
|
|
7102
|
+
if (fn.localObjects === undefined) {
|
|
7103
|
+
return tree;
|
|
7104
|
+
}
|
|
7105
|
+
const named = nameLocalStatics(tree, fn.localObjects);
|
|
7106
|
+
if ('refused' in named) {
|
|
7107
|
+
throw new StructureError(
|
|
7108
|
+
`cannot structure '${fn.name}': names a function-scope static ('${named.symbol}') ${named.refused}`,
|
|
7109
|
+
);
|
|
7110
|
+
}
|
|
7111
|
+
return named;
|
|
7112
|
+
}
|
|
7113
|
+
|
|
7114
|
+
/** Of each table of declared callees, the entries this body calls — every tree the ranked pass
|
|
7115
|
+
* structures carries its own, and is keyed by its whole text. A table this body calls nothing in is
|
|
7116
|
+
* absent. */
|
|
7117
|
+
function calledOnly(
|
|
7118
|
+
tables: Pick<StructureOptions, 'declaredArgs' | 'declaredReturns'>,
|
|
7119
|
+
body: Stmt[],
|
|
7120
|
+
): Pick<StructureOptions, 'declaredArgs' | 'declaredReturns'> {
|
|
7121
|
+
const callees = new Set<string>();
|
|
7122
|
+
for (const e of walkExprs(body)) {
|
|
7123
|
+
if (e.k === 'call' && typeof e.fn === 'string') {
|
|
7124
|
+
callees.add(e.fn);
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
const of = <T>(table: Readonly<Record<string, T>> | undefined): Record<string, T> | undefined => {
|
|
7128
|
+
const called = Object.fromEntries(Object.entries(table ?? {}).filter(([n]) => callees.has(n)));
|
|
7129
|
+
return Object.keys(called).length > 0 ? called : undefined;
|
|
7130
|
+
};
|
|
7131
|
+
const declaredArgs = of(tables.declaredArgs);
|
|
7132
|
+
const declaredReturns = of(tables.declaredReturns);
|
|
7133
|
+
return { ...(declaredArgs ? { declaredArgs } : {}), ...(declaredReturns ? { declaredReturns } : {}) };
|
|
5762
7134
|
}
|
|
5763
7135
|
|
|
5764
7136
|
// Does any statement CONTINUE this loop (vs. a nested one)? A `continue` inside a nested while/dowhile/
|
|
@@ -5984,11 +7356,16 @@ function* inEdgeRecords(preds: Map<Block, Block[]>, b: Block): Generator<{ pred:
|
|
|
5984
7356
|
}
|
|
5985
7357
|
|
|
5986
7358
|
// Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it. Over the
|
|
5987
|
-
// subgraph `keep` induces when given, every member of which must still reach a `ret` inside it
|
|
5988
|
-
|
|
7359
|
+
// subgraph `keep` induces when given, every member of which must still reach a `ret` inside it — or,
|
|
7360
|
+
// with `onward`, over only the edges it names between members of `keep`: a block it names no edge
|
|
7361
|
+
// from ends its paths, post-dominated by nothing but itself.
|
|
7362
|
+
function postDominators(fn: Fn, keep?: ReadonlySet<Block>, onward?: (b: Block) => Block[]): Map<Block, Block | null> {
|
|
5989
7363
|
const blocks = keep ? fn.blocks.filter((b) => keep.has(b)) : fn.blocks;
|
|
5990
7364
|
const nodes: (Block | null)[] = [null, ...blocks];
|
|
5991
7365
|
const succ = (b: Block): (Block | null)[] => {
|
|
7366
|
+
if (onward !== undefined) {
|
|
7367
|
+
return onward(b);
|
|
7368
|
+
}
|
|
5992
7369
|
const term = b.ops[b.ops.length - 1];
|
|
5993
7370
|
return term.opcode === 'ret' ? [null] : successorsOf(b).filter((s) => keep === undefined || keep.has(s));
|
|
5994
7371
|
};
|