@asmlift/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
@@ -0,0 +1,40 @@
1
+ // L3 respell variation: sink each leading pointer-base INIT to the statement that first uses it.
2
+ //
3
+ // `l3/basecse.ts`'s COMMITTED call emits every base hoist at the head of `sfn.body`, so a base
4
+ // first touched halfway down the function is live across everything above it — a live range the
5
+ // original never had, and agbcc pays for it with a callee-saved register. (Its roster's hoists
6
+ // ask for this placement directly, through the same `l3/hoist.ts` mechanism this variation uses; what
7
+ // the variation adds is reaching the run on a tree the roster did not build — one NOTHING re-hoisted,
8
+ // or one `l3/nearbase.ts` prepended into, which is the `/nearbase/sinkinit` pairing.)
9
+ // Compiled pair on `synthetic:basehome`:
10
+ // assigning at the top adds `push {r4, lr}` / `pop {r4}` / `pop {r0}` / `bx r0` where assigning at
11
+ // the first use keeps a plain `bx lr`. The ladder that row records is the argument for the variation
12
+ // being a PLACEMENT rather than a wider hoist — top-placed 11, not hoisted at all 9, placed at
13
+ // first use 0. A hoist at the wrong place is worse than no hoist.
14
+ //
15
+ // A VARIATION, NOT THE DEFAULT. Which placement the source used is per-function knowledge the asm does
16
+ // not carry: moving basecse's own head placement to first use moves 8 benchmark rows, 4 better and
17
+ // 4 worse, two of the losses being matches. The UNSUNK spelling always rides beside this one —
18
+ // head-placed wherever basecse built the run, prepend-placed under `/nearbase` — and the differ
19
+ // referees, so ADDING this candidate can never cost a match. Withholding one is a different
20
+ // question and not a free one (l3/basecse.ts's fold-evidence note).
21
+ //
22
+ // SCOPE (decline over approximate). Only the LEADING run of base inits moves — the run basecse.ts
23
+ // and nearbase.ts mint into, placed by the same `l3/hoist.ts` mechanism, so no two of the three can
24
+ // disagree about where the run ends or how a body carrying it is rebuilt. Each init sinks to
25
+ // immediately before the first TOP-LEVEL statement mentioning its name and never INTO a nested
26
+ // scope, so it still dominates every use — planning a hoist inside a scope is `l3/scopebase.ts`'s
27
+ // job and it does the domination work. `placeBaseLocals` carries the refusals; a run where none of
28
+ // them moves is this variation declining.
29
+ //
30
+ // SEMANTICS BY CONSTRUCTION: the moved value is a pure address leaf — it reads nothing, writes its
31
+ // own plain cell and cannot fault — and every statement it crosses mentions the name nowhere, an
32
+ // `&p` escape included (mentions.ts counts `addr` as a mention). All later evaluation can change
33
+ // is where the allocator puts it.
34
+ import type { SFn } from './ast';
35
+ import { placeBaseLocals } from './hoist';
36
+
37
+ export function sinkInitsToFirstUse(sfn: SFn): SFn | null {
38
+ const { body, moved } = placeBaseLocals(sfn, [], 'first-use');
39
+ return moved.length === 0 ? null : { ...sfn, body };
40
+ }
@@ -0,0 +1,123 @@
1
+ // asmlift — declare slot-homed locals in the TARGET'S OWN FRAME ORDER.
2
+ //
3
+ // THE LAW. gcc 2.9 hands a spilled user local its frame slot by DECLARATION RANK: reload walks
4
+ // pseudos in ascending number handing each global-allocation loser a fresh stack slot, a user
5
+ // local's pseudo number is fixed at its `expand_decl` — i.e. its position in the declaration list
6
+ // — and the Thumb frame grows upward. So under agbcc the earlier-declared spilled local takes the
7
+ // LOWER `[sp,#k]`, and a source whose two spilled locals are declared the other way round compiles
8
+ // to the same object with those two operands swapped and nothing else moved.
9
+ //
10
+ // A COMPILER BEHAVIOR, NOT A RANKED VARIATION. The asm does not underdetermine the answer: a
11
+ // `[sp,#k]` operand NAMES the slot, and slot → declaration rank is a FUNCTION once the compiler is
12
+ // fixed. A variation exists where two source spellings collapse to the same object and only the differ
13
+ // can choose between them; here the object chooses. The fan cost is zero — this rewrites the one
14
+ // tree every candidate already carries, adding no candidate, no `structure()` call and no compile.
15
+ //
16
+ // WHY `emit` OWNS IT. Two reasons, and they point the same way. It must run AFTER every L3 respell variation,
17
+ // because each of those rebuilds the declaration list (appends: basecse, scopebase, argbase,
18
+ // nearbase, regspell, reindex; filters: unmerge, coalesce, dce, inlinebase, pollguard, unreduce),
19
+ // and `emit` is last by construction. And it must NOT run at a `.emit(` call site: there are seven
20
+ // of those, the web Playground reaches two of them for one function (the headline source and the
21
+ // Pipeline tab), and the score probe reaches a third — a call-site ordering would print an ordered
22
+ // headline beside an unordered pipeline dump and measure the probe's scoreDelta on a source the
23
+ // ranked path never compiles.
24
+ //
25
+ // WHAT DEPENDS ON THE ORDER THIS DOES NOT CHANGE. `l3/coalesce.ts` picks the arm-disjoint survivor
26
+ // by `declIdx` — the earlier declaration, matching how a shared source local reads. It runs before
27
+ // this and therefore reads the UNSORTED list, which is exactly right and is what the emit-time
28
+ // placement preserves. Sorting any earlier would silently change which local survives every
29
+ // arm-disjoint merge.
30
+ import type { SFn } from './ast';
31
+
32
+ /** Refill the positions held by slot-carrying locals with those same locals in frame order.
33
+ *
34
+ * PURE: the structurer's own list is never mutated, so `emit` cannot leak an ordered list back to
35
+ * a caller that expects the structurer's order.
36
+ *
37
+ * REFUSAL CONDITIONS, all of them — 1, 2 and 6 are checked below; 3, 4 and 5 hold by construction
38
+ * (see each):
39
+ * 1. `fn.slotOrder` absent — the target's direction is unknown, or it ships `'unknown'`: the
40
+ * ordering is the IDENTITY. There is no default direction.
41
+ * 2. Fewer than two sortable locals: nothing to order.
42
+ * 3. A local with no `slots`, or an empty list, keeps its position exactly. Only the positions
43
+ * the sortable locals already occupy are refilled, so an unslotted local never moves and no
44
+ * local is ever inserted or removed.
45
+ * 4. `frame` and `uninit` locals are never sortable, because the structurer never stamps
46
+ * `slots` on them — the refusal, and the measurement behind it, live at that stamp site.
47
+ * 5. Parameters are never touched: their storage is the caller's question.
48
+ * 6. TWO DECLARED LOCALS SHARING ONE OFFSET REFUSE THE WHOLE FUNCTION. Reload hands each
49
+ * spilled pseudo a FRESH slot (`reload1.c:769-770`, the premise this file's law rests on),
50
+ * so a frame in which the slot -> local map is not injective is not a frame this law
51
+ * describes at all: something upstream — a decomposed stack aggregate, a coalesce, a naming
52
+ * walk that put two spilled values under two names at one address — produced the evidence,
53
+ * and none of those is a declaration rank. It is NOT enough to leave that PAIR alone: the
54
+ * shared offset is still used to rank both of them against every other sortable local, so
55
+ * the whole ordering is unlicensed and the identity is the only sound answer.
56
+ *
57
+ * MEASURED, both sides. It costs nothing: `spillorder` (v6@4 v12@0), `uninit_spill` (v4@0
58
+ * v5@4 v6@8), `dma_fill_uninit` (v2@16 v4@4 v6@8 v8@12) and the `agbcc-u8spill.s` fixture
59
+ * (v0@4 v10@8 v11@12) are all injective, so every shipped inhabitant keeps its ordering. And
60
+ * it is what the wild corpus asks for: of 2,463 real agbcc functions (158 sa3 + klonoa
61
+ * listings) exactly ONE carries two slot-carrying locals at all, and it is non-injective —
62
+ * `sa3 enemies/hariisen_proj.s` `sub_80617E0`, `v8@12 v12@8 v13@12`, which without this
63
+ * refusal emitted `… v7 v12 v9 v10 v11 v8 v13 …` against the declaration order `… v7 v8 v9
64
+ * v10 v11 v12 v13 …`. Its slots are four words of one declared stack array (see the stamp
65
+ * site's aggregate note), which is exactly the class that mints a duplicate. So the one real
66
+ * function this default reached was one whose evidence contradicts its own premise, and the
67
+ * wild reach is zero functions changed rather than one changed wrongly.
68
+ *
69
+ * A consequence, so nothing downstream relies on the sort's stability: two sortable locals
70
+ * can no longer TIE. Equal ranks means an equal minimum offset under `ascending` and an
71
+ * equal maximum under `descending`, and either is a shared offset, which this refuses first.
72
+ *
73
+ * AND THIS IS THE ONE PLACE A SET OF HOMES IS REDUCED TO A RANK. A local can carry several
74
+ * offsets, and every site that merged them took the union and chose nothing, because the choice
75
+ * is direction-dependent and none of them holds a target (ir/core.ts `SlotHomes`). The earliest
76
+ * declaration rank is the LOWEST offset under an ascending frame and the HIGHEST under a
77
+ * descending one: one comparator, applied where `slotOrder` is in hand.
78
+ *
79
+ * The `descending` half of that comparator has NO SHIPPED INHABITANT: agbcc is the only target
80
+ * that ships a direction and it is `ascending`. It is reached only through the public
81
+ * `StructureOptions.spillSlotOrder` and by the tests, and it exists because ido7.1's
82
+ * `TargetDescription` carries a MEASURED `descending` with a written flip condition (target.ts).
83
+ *
84
+ * REACH. The two inhabitants are `spillorder` (6 → MATCH) and `dma_fill_uninit` (12 → MATCH, a
85
+ * row this capability did not author). On the REAL agbcc tier it is ZERO, and the cause is named
86
+ * rather than assumed: of 126 cases, four carry any L1 slot home, two carry a slot-carrying local
87
+ * at L3, and NONE carries two. So the blocker is neither the frame order nor the census — the
88
+ * structurer's naming walk INLINES spilled values instead of declaring them (`PackSaveSector`
89
+ * spills to 18 distinct slots, 108 stamped values, and reaches L3 with zero slot-carrying
90
+ * locals), which is also why counting stores is a bad proxy for reach. */
91
+ export function orderSlotLocals(fn: SFn): SFn {
92
+ const dir = fn.slotOrder;
93
+ if (dir === undefined) {
94
+ return fn;
95
+ }
96
+ // the earliest declaration rank among a local's homes
97
+ const rank = (l: SFn['locals'][number]): number =>
98
+ dir === 'ascending' ? Math.min(...l.slots!) : Math.max(...l.slots!);
99
+ const at: number[] = [];
100
+ fn.locals.forEach((l, i) => {
101
+ if (l.slots !== undefined && l.slots.length > 0) {
102
+ at.push(i);
103
+ }
104
+ });
105
+ if (at.length < 2) {
106
+ return fn;
107
+ }
108
+ // refusal 6: the slot -> local map must be INJECTIVE, or this frame is not one the law
109
+ // describes and no local in it can be ranked. Over every offset of every sortable local, not
110
+ // just their ranks: a duplicate anywhere means some slot was not a fresh reload assignment.
111
+ const offsets = at.flatMap((i) => fn.locals[i].slots!);
112
+ if (new Set(offsets).size !== offsets.length) {
113
+ return fn;
114
+ }
115
+ const inFrameOrder = at
116
+ .map((i) => fn.locals[i])
117
+ .sort((a, b) => (dir === 'ascending' ? rank(a) - rank(b) : rank(b) - rank(a)));
118
+ const locals = [...fn.locals];
119
+ at.forEach((i, k) => {
120
+ locals[i] = inFrameOrder[k];
121
+ });
122
+ return { ...fn, locals };
123
+ }
@@ -0,0 +1,48 @@
1
+ // Which STORAGE CLASS an L3 name denotes.
2
+ //
3
+ // `assign` names its target with a bare string, and structure.ts spells a store to a local, to a
4
+ // param and to a bare scalar global identically — so every pass that must tell them apart has to
5
+ // re-derive the distinction from `SFn`'s three declaration lists. Four did, privately, and the one
6
+ // that got it wrong (l3/reindex.ts) read a plausible type off a global and re-spelled away a write
7
+ // another translation unit observes.
8
+ //
9
+ // SHADOWING is what the copies disagreed on, and the disagreement is REAL rather than drift: a
10
+ // name can be declared both a global and a local, because `SFn.globals` records what the code
11
+ // REFERENCES (structure.ts `noteGlobal`) independently of what it declares. Which answer is right
12
+ // depends on how the asking pass will SPELL the name, so the two questions are two functions
13
+ // here instead of one set each caller filters its own way:
14
+ // • it keeps the reference verbatim (`(u8 *)g`) — the shadow is harmless, the spelling denotes
15
+ // whatever the original access denoted: `declaredGlobals`;
16
+ // • it re-spells the reference as an ADDRESS (`&g`) — a shadow then names the LOCAL's storage,
17
+ // silently a different object: `addressableGlobals`;
18
+ // • it asks which of the three classes a bare `assign` name denotes at all: `nameStorage`, the
19
+ // primitive the other two are the shadow-sensitive projections over.
20
+ import type { SFn } from './ast';
21
+
22
+ export type Storage = 'local' | 'param' | 'global';
23
+
24
+ /** Every declared name, classified. A local or a param SHADOWS a same-named global — that is what
25
+ * the name binds to. A name absent from the map is declared nowhere. */
26
+ export function nameStorage(sfn: SFn): Map<string, Storage> {
27
+ const t = new Map<string, Storage>();
28
+ for (const g of sfn.globals ?? []) {
29
+ t.set(g.name, 'global');
30
+ }
31
+ for (const p of sfn.params) {
32
+ t.set(p.name, 'param');
33
+ }
34
+ for (const l of sfn.locals) {
35
+ t.set(l.name, 'local');
36
+ }
37
+ return t;
38
+ }
39
+
40
+ /** Names declared as globals, INCLUDING one a local or param shadows (see the header). */
41
+ export function declaredGlobals(sfn: SFn): Set<string> {
42
+ return new Set((sfn.globals ?? []).map((g) => g.name));
43
+ }
44
+
45
+ /** Names whose ADDRESS is the global's — a shadowed one excluded (see the header). */
46
+ export function addressableGlobals(sfn: SFn): Set<string> {
47
+ return new Set([...nameStorage(sfn)].filter(([, s]) => s === 'global').map(([n]) => n));
48
+ }
@@ -1,9 +1,15 @@
1
1
  // asmlift — SELF-DECLARING CANDIDATES: the pure map-reference query
2
2
  // (research/self-declaring-candidates-2026-07-26.md).
3
3
  //
4
- // `collectSymbolRefs` derives, from a FINAL structured tree, every map-derived symbol the body
4
+ // `collectSymbolRefs` derives, from a FINAL structured tree, every DECLARABLE symbol the body
5
5
  // references in a VALUE context — the input to the scoring layer's declaration synthesis
6
- // (@asmlift/cli declare.ts). It is a pure tree query with no pipeline state: the enumeration
6
+ // (core declare.ts). "Declarable" and not "map-derived": the dictionary this is called with is
7
+ // the caller's, and rank.ts hands it a symbol map UNIONED with the names read straight out of
8
+ // the asm's own literal pool / relocations (`bareGlobalSymbols`), so a candidate compiled
9
+ // outside project headers declares what it spells even with no map at all. The map's facts win
10
+ // per NAME where it has them; the rest come back marked `synthesized`.
11
+ //
12
+ // It is a pure tree query with no pipeline state: the enumeration
7
13
  // layer (rank.ts) calls it exactly once per candidate, on the tree the candidate's source was
8
14
  // emitted from, at the moment the candidate is finalized. There is deliberately NO cached
9
15
  // `symbolRefs` field on `SFn` — a carried field would oblige every future l3 pass to remember
@@ -13,7 +19,7 @@
13
19
  import type { SymbolInfo } from '../symbols';
14
20
  import { Expr, Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
15
21
 
16
- /** One recorded map-symbol VALUE reference — a name the tree references plus its map facts. */
22
+ /** One recorded VALUE reference — a name the tree references plus the facts to declare it. */
17
23
  export interface SymbolRef {
18
24
  name: string;
19
25
  info: SymbolInfo;
@@ -21,19 +27,36 @@ export interface SymbolRef {
21
27
  * own IR — attached by the enumeration (rank.ts bareGlobalAccessFacts), consumed by the
22
28
  * declaration synthesis (declare.ts) as the width/signedness authority for `extern T name;`. */
23
29
  access?: { width: number; signed: boolean };
30
+ /** NO SYMBOL MAP KNOWS THIS NAME — the declaration for it is a HYPOTHESIS, not a fact. Both the
31
+ * name and (through `access`) its width and signedness were read out of the candidate's own
32
+ * asm: the very bytes the candidate is then scored against. A fitted declaration cannot LOSE
33
+ * score, so it is not covered by declare.ts's only-loses-score argument — it can manufacture
34
+ * agreement instead. Which is legitimate (the artifact is decls + source, and it does compile
35
+ * to those bytes) exactly as long as the consumer SHOWS the declarations with the verdict.
36
+ * Marked here so a consumer can tell the two provenances apart inside one block. */
37
+ synthesized?: true;
24
38
  }
25
39
 
26
- /** The map-derived symbols a structured body references in a VALUE context — the input to the
40
+ /** The declarable symbols a structured body references in a VALUE context — the input to the
27
41
  * scoring layer's declaration synthesis. A name counts when it appears as a `var`/`addr` leaf
28
- * and the map knows it (bare `gSym`, `&gSym`, `(u32)Func`, a `field` base — all reduce to
42
+ * and the caller's dictionary knows it (bare `gSym`, `&gSym`, `(u32)Func`, a `field` base — all reduce to
29
43
  * those leaves). A name that is ANY call's target is excluded entirely, even if also
30
44
  * value-referenced: prototyping a called symbol `void F(void);` hard-errors under gcc-2.9
31
45
  * when the call passes args, while leaving it undeclared keeps today's implicit-declaration
32
46
  * behavior (the one honest option without arity knowledge). The function's OWN name
33
47
  * (`selfName`) is excluded too — the candidate's definition IS its declaration, and a
34
48
  * synthesized `void F(void);` above `s32 F(...)` is a conflicting-types hard error (a
35
- * self-address reference resolves against the definition itself). */
36
- export function collectSymbolRefs(body: Stmt[], symbols: Map<string, SymbolInfo>, selfName: string): SymbolRef[] {
49
+ * self-address reference resolves against the definition itself).
50
+ *
51
+ * Both exclusions are REPORTED through `onRefused`, on the caller's own refusal channel: they
52
+ * leave a name undeclared for asmlift's own reason, and a consumer's list of those reasons is
53
+ * incomplete without them. */
54
+ export function collectSymbolRefs(
55
+ body: Stmt[],
56
+ symbols: Map<string, SymbolInfo>,
57
+ selfName: string,
58
+ onRefused?: (name: string, reason: 'call-target' | 'self-name') => void,
59
+ ): SymbolRef[] {
37
60
  const called = new Set<string>();
38
61
  const valueRefs = new Set<string>();
39
62
  const visitExpr = (e: Expr): void => {
@@ -55,7 +78,17 @@ export function collectSymbolRefs(body: Stmt[], symbols: Map<string, SymbolInfo>
55
78
  };
56
79
  body.forEach(visitStmt);
57
80
  return [...valueRefs]
58
- .filter((n) => !called.has(n) && n !== selfName)
59
81
  .sort()
82
+ .filter((n) => {
83
+ if (called.has(n)) {
84
+ onRefused?.(n, 'call-target');
85
+ return false;
86
+ }
87
+ if (n === selfName) {
88
+ onRefused?.(n, 'self-name');
89
+ return false;
90
+ }
91
+ return true;
92
+ })
60
93
  .map((n) => ({ name: n, info: symbols.get(n)! }));
61
94
  }
@@ -25,7 +25,7 @@
25
25
  // above covers only below-vs-in-arms.
26
26
  //
27
27
  // KNOWN INTERACTIONS, both byte-level rather than soundness. This pass is unconditional like
28
- // `dce.ts` and `basecse.ts` rather than a differ-refereed lever, and the argument those files each
28
+ // `dce.ts` and `basecse.ts` rather than a differ-refereed variation, and the argument those files each
29
29
  // state for themselves applies here too and was missing: a wrong merge changes recompiled bytes and
30
30
  // surfaces as a LOST match under the zero-lost gate, never as wrong C.
31
31
  //
@@ -37,6 +37,21 @@
37
37
  // of the arms hides the common tail, and DCE only removes it afterwards, so the shape this pass
38
38
  // exists for is missed. The fix is a fixpoint of the pair, not one extra call — a lone second
39
39
  // pass leaves an empty `if` behind.
40
+ // - THE SAME HIDING HAPPENS WITHOUT DCE, and the edge-copy sort decides when. An arm ends in the
41
+ // copies of one CFG edge, ordered by the predecessor's write record or by the def-position
42
+ // proxy (`structure.ts`); an arm-varying copy ordered LAST hides an agreeing one behind it.
43
+ // Measured on klonoa `CountCollectedGems` (map-less, agbcc): the record's order leaves six
44
+ // copies of `v22 = (s32 *)50345232;` where the proxy's order merges them into one, and with
45
+ // THIS PASS disabled both orders emit all six — so the duplication is this peel not firing,
46
+ // not something the sort creates. Reaching the hidden statement is not a matter of peeling
47
+ // further: it would have to move ACROSS the differing one, which needs the independence
48
+ // argument this pass deliberately does not have (see the soundness note above). Pinned both
49
+ // ways in `test/tailmerge.test.ts`.
50
+ // A ranked row is not stuck with either order: `/copy-defpos` (rank.ts) enumerates the
51
+ // def-position spelling beside the record's, so the merged form is a candidate the differ can
52
+ // pick on bytes — on `CountCollectedGems` its gate admits the variation on the map-ful and map-less
53
+ // lifts alike. What no arm of the fan spells is the third form, the hidden statement merged
54
+ // while KEEPING the record's order.
40
55
  //
41
56
  // SCOPE. Only `assign`/`store`/`exprstmt` merge, compared structurally through `exprEquals`.
42
57
  // Control flow (`break`/`continue`/`return`) is excluded: moving one out of an arm changes which
package/src/l3/typing.ts CHANGED
@@ -18,10 +18,10 @@
18
18
  // provably a pointer" (adds a cast — valid C either way); the deref contract treats `undefined`
19
19
  // as "not provably wrong" (no error).
20
20
  import { IrType, T, scalarTypeForAccess } from '../ir/types';
21
- import type { Expr, SFn } from './ast';
21
+ import { type Expr, type SFn, exprChildren } from './ast';
22
22
 
23
23
  /** The declared type of a printed variable — the env `exprCType` judges rendered C against.
24
- * THE one copy of the SFn→env derivation (C printer, Pascal printer, deref contract): each
24
+ * THE one copy of the SFn→env derivation printers, contracts and L3 respell variations alike: each
25
25
  * consumer judging against anything but the declarations it emits would let them disagree. */
26
26
  export type VarTypes = (name: string) => IrType | undefined;
27
27
 
@@ -48,22 +48,211 @@ export function ptrElemBytes(to: IrType): number {
48
48
  return to.kind === 'int' ? to.width / 8 : to.kind === 'ptr' ? 4 : 0;
49
49
  }
50
50
 
51
- /** May a `width`-byte access dereference a base of rendered C type `rt` AS SPELLED i.e. is `rt`
52
- * a pointer/array whose element size equals the access width? THE one copy of the stride rule:
51
+ /** The env a C-family printer judges rendered C against: the declared types, plus which printed
52
+ * vars render as a pointer to VOLATILE. IrType models no cv-qualifier (see the `cast` node doc in
53
+ * l3/ast.ts), so a printer that RE-TYPES an access — the deref legalization — has to read the
54
+ * qualifier from here or the re-typing silently drops it. */
55
+ export interface PrintEnv {
56
+ readonly type: VarTypes;
57
+ readonly volatilePointee: (name: string) => boolean;
58
+ }
59
+
60
+ export function printEnv(fn: SFn): PrintEnv {
61
+ // both flags, because the C declarator prints ONE `volatile ` prefix for a pointer local and it
62
+ // binds to the pointee either way (the declaration loop in backend/cfamily.ts)
63
+ const vol = new Set(
64
+ fn.locals.filter((l) => l.type.kind === 'ptr' && (l.pointeeVolatile || l.volatile)).map((l) => l.name),
65
+ );
66
+ return { type: declaredTypes(fn), volatilePointee: (n) => vol.has(n) };
67
+ }
68
+
69
+ /** Does anything under `e` assert a VOLATILE POINTEE? The two spellings that carry the qualifier
70
+ * into printed C are a `volatile` cast node and a volatile-pointee declaration. The whole subtree
71
+ * is searched, not the pointer spine: a cast to an integer type between the assertion and the
72
+ * access — `(u32)p + 4` — renders the qualifier no less dropped, and over-qualifying an access
73
+ * only restricts the compiler further, where under-qualifying is a lie about MMIO. */
74
+ export function assertsVolatile(e: Expr, env: PrintEnv): boolean {
75
+ return (
76
+ (e.k === 'cast' && e.volatile === true) ||
77
+ (e.k === 'var' && env.volatilePointee(e.name)) ||
78
+ exprChildren(e).some((c) => assertsVolatile(c, env))
79
+ );
80
+ }
81
+
82
+ /** May a `width`-byte access of the given signedness dereference a base of rendered C type `rt` AS
83
+ * SPELLED — i.e. is `rt` a pointer/array whose element size equals the access width, and whose
84
+ * element extends the way the access does? THE one copy of the stride rule:
53
85
  * the C-family printer decides cast insertion from it, the Pascal backend decides declining from
54
86
  * it, and exprCType types the access result from it. `false` for a non-pointer, an unknowable
55
87
  * base (undefined), or a pointer of the WRONG stride — a wrong-stride deref would make C read
56
88
  * the wrong width and scale the index by the wrong element size. */
57
- export function derefStrideOk(rt: IrType | undefined, width: number): boolean {
89
+ export function derefStrideOk(rt: IrType | undefined, width: number, signed: boolean): boolean {
90
+ // SIGNEDNESS COUNTS WHEREVER THE ACCESS EXTENDS. A sub-word load fills the top bits from either
91
+ // the sign bit or zero, and in the emitted C the pointee type is the ONLY thing that says which —
92
+ // so a base of the wrong signedness has to take the reinterpret cast exactly as a base of the
93
+ // wrong stride does. Read `*(u8 *)p` where the machine did `ldrsb` and `p[9] < 0` is not merely a
94
+ // different value: an `unsigned char` promotes to a non-negative `int`, so the arm goes dead.
95
+ // A word access extends nothing, and a POINTER pointee is a word.
96
+ const extendsOk = (to: IrType) => width === 4 || (to.kind === 'int' && to.signed === signed);
58
97
  if (rt?.kind === 'ptr') {
59
- return rt.to.kind !== 'struct' && ptrElemBytes(rt.to) === width;
98
+ return rt.to.kind !== 'struct' && ptrElemBytes(rt.to) === width && extendsOk(rt.to);
60
99
  }
61
100
  if (rt?.kind === 'array') {
62
- return rt.elem.kind === 'int' && rt.elem.width === width * 8;
101
+ return rt.elem.kind === 'int' && rt.elem.width === width * 8 && extendsOk(rt.elem);
63
102
  }
64
103
  return false;
65
104
  }
66
105
 
106
+ /** The USUAL ARITHMETIC CONVERSIONS over two rendered operands: at equal rank, unsigned wins.
107
+ * Either side unknown leaves the result unknown — EXCEPT when the known side is unsigned, which
108
+ * already decides it.
109
+ *
110
+ * That exception is the one place this returns a DEFINITE answer from an unknown operand, and it
111
+ * is sound only because every integer here is rank `int`: at UNEQUAL rank C converts to the wider
112
+ * type first, so `unsigned int & long long` is SIGNED. Core has no 64-bit integer type at all
113
+ * (the decomp typedef vocabulary stops at 32 — see contracts.ts SCALAR_WIDTHS), so the
114
+ * unequal-rank case cannot arise. Adding one would invalidate this.
115
+ *
116
+ * Exported because two consumers ask it about an operator whose own rendering they are deciding,
117
+ * so they cannot ask `renderedIntSignedness` about the node: the C-family backend's operand pin,
118
+ * and initfirst's compare-meaning gate over a substitution not yet made. */
119
+ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes): boolean | undefined {
120
+ const ls = renderedIntSignedness(l, varType);
121
+ const rs = renderedIntSignedness(r, varType);
122
+ if (ls === false || rs === false) {
123
+ return false;
124
+ }
125
+ return ls === true && rs === true ? true : undefined;
126
+ }
127
+
128
+ /**
129
+ * The C SIGNEDNESS a rendered integer expression actually has — `true`/`false`, or `undefined`
130
+ * when it is not determinable here. The deliberate complement to `exprCType`, which is
131
+ * pointer-ness-accurate and reports every integer as `s32` by contract; this models the two C
132
+ * rules that contract omits, integer PROMOTION and the usual arithmetic CONVERSIONS.
133
+ *
134
+ * THE one rendered-signedness judgment, and it lives beside the declarations it judges against
135
+ * because every consumer shares it: the C-family backend's operand pin, structure.ts's
136
+ * unsigned-compare gate (the /uns-cmp variation) and its signed-compare pin, and — through
137
+ * `arithConversionSignedness` above — initfirst's compare-meaning gate. Two of those models
138
+ * disagreeing about one expression is the drift this placement prevents.
139
+ *
140
+ * The question is byte-load-bearing: C spells both `>>>` and `>>` as `>>` and chooses between them
141
+ * from the left operand's type, so a logical shift rendered over a signed expression recompiles to
142
+ * `asr` where the target has `lsr` and evaluates to a different value.
143
+ *
144
+ * `undefined` means the model did not reach, never "possibly signed": what a `call` renders as is
145
+ * decided by the prototype in scope, and the prototypes in scope are the PROJECT's — the scoring
146
+ * context prepends its headers, and those do declare unsigned-returning callees. So every consumer
147
+ * whose operator needs a definite signedness takes a cast on `undefined` exactly as on the wrong
148
+ * definite answer. A redundant cast is codegen-identical (`(s32)f() / a` and `f() / a` assemble
149
+ * instruction-for-instruction the same where `f` really returns `int`); a missing one is a
150
+ * miscompile — with `u32 f(void);` in scope `f() / a` calls `__udivsi3` where the machine called
151
+ * `__divsi3`, and `if (f() >= 0)` compiles to `bl f; mov r0, #0`, comparison and both arms gone.
152
+ * The one consumer that reads `undefined` the other way is the /uns-cmp variation's own gate, which is
153
+ * asking whether an operand is ALREADY unsigned.
154
+ *
155
+ * Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was
156
+ * declared. Pointers, calls and markers are `undefined`.
157
+ */
158
+ export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | undefined {
159
+ const rec = (x: Expr): boolean | undefined => renderedIntSignedness(x, varType);
160
+ // an lvalue-ish leaf: its C type is a declaration / an explicit cast / a carried access width
161
+ const promoted = (t: IrType | undefined): boolean | undefined =>
162
+ t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 ? t.signed : undefined;
163
+ switch (e.k) {
164
+ case 'var':
165
+ case 'cast':
166
+ case 'index':
167
+ case 'field':
168
+ return promoted(exprCType(e, varType));
169
+ // A decimal literal is `int` when it fits in one, and C89 gives a larger one an UNSIGNED type
170
+ // — agbcc says so itself ("decimal constant is so large that it is unsigned"). INT_MIN is in
171
+ // that class despite fitting: the backend prints it as `-2147483648`, which C lexes as unary
172
+ // minus applied to `2147483648`. Definite, not undetermined, and it drags the whole
173
+ // expression with it: `a / -2147483648` compiles to `lsr r0, r0, #0x1f` — an unsigned divide
174
+ // folded to a shift — where `a / (s32)-2147483648` calls `__divsi3`.
175
+ case 'const':
176
+ return e.value > -2147483648 && e.value <= 2147483647;
177
+ // `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`.
178
+ case 'un':
179
+ return e.op === '!' ? true : rec(e.e);
180
+ case 'bin': {
181
+ // The SIGNEDNESS-CARRYING pairs — the ops the tower keeps apart because C spells each pair
182
+ // with one token and picks between them from the operand types (l3/ast.ts BinOp). The
183
+ // C-family backend PINS their operands, so what gets printed renders as the op says
184
+ // whatever the operands would have rendered as on their own. Reporting the operands here
185
+ // instead would answer about an expression nobody prints: a `>>` over a u32 var prints
186
+ // `(s32)v >> 3` and is signed, and a consumer told otherwise omits its own cast.
187
+ if (e.op === '>>' || e.op === '/' || e.op === '%') {
188
+ return true;
189
+ }
190
+ if (e.op === '>>>' || e.op === '/u' || e.op === '%u') {
191
+ return false;
192
+ }
193
+ // `<<` is sign-blind, so it keeps its left operand's type — nothing pins it.
194
+ if (e.op === '<<') {
195
+ return rec(e.l);
196
+ }
197
+ // Comparisons and the logical connectives yield `int`.
198
+ if (['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op)) {
199
+ return true;
200
+ }
201
+ return arithConversionSignedness(e.l, e.r, varType);
202
+ }
203
+ case 'call':
204
+ case 'marker':
205
+ case 'addr':
206
+ return undefined;
207
+ }
208
+ }
209
+
210
+ /** Is the rendered expression provably in [0, 2^31) — the range where a signed and an unsigned
211
+ * compare agree on every input (and where gcc itself emits the unsigned branch for the signed
212
+ * spelling)? Narrow unsigned values are the everyday case: a `(u8)x` cast or a `u8`/`u16`
213
+ * deref promotes to a non-negative `int`. Conservative false elsewhere. */
214
+ export function provablyNonNegative(e: Expr, varType: VarTypes): boolean {
215
+ switch (e.k) {
216
+ case 'const':
217
+ return e.value >= 0 && e.value < 0x80000000;
218
+ case 'un':
219
+ return e.op === '!';
220
+ case 'bin':
221
+ return ['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op);
222
+ case 'cast':
223
+ return e.to.kind === 'int' && e.to.width < 32 && !e.to.signed;
224
+ default: {
225
+ const t = exprCType(e, varType);
226
+ return t?.kind === 'int' && t.width < 32 && !t.signed;
227
+ }
228
+ }
229
+ }
230
+
231
+ /** Does this write put a value that renders as a DEFINITELY-NON-POINTER C expression into a
232
+ * pointer-declared slot? — `v2 = a1 + v0` with `v2: u8 *`, a `return a0 + v0` from a
233
+ * pointer-returning function, a store through a pointer-element lvalue. The machine's register
234
+ * move says nothing about types; C does, and mwcc makes it an ERROR (gcc merely warns).
235
+ *
236
+ * ONE question, TWO answers: the C family legalizes it with the reinterpret cast to the declared
237
+ * type (backend/cfamily.ts), Pascal has no reinterpret cast and declines LOUD (backend/pascal.ts).
238
+ * The question is shared so the two backends cannot come to disagree about WHICH writes are the
239
+ * hazard while still disagreeing about what to do with one.
240
+ *
241
+ * A rendering the model cannot reach (a call, whose C type comes from a prototype outside the
242
+ * emitted function) is NOT the hazard: the answer has to be definite in the non-pointer
243
+ * direction. A type predicate so the cast site keeps its narrowing on the declared type. */
244
+ export function writesNonPointerIntoPointer(
245
+ declared: IrType | undefined,
246
+ value: Expr,
247
+ varType: VarTypes,
248
+ ): declared is Extract<IrType, { kind: 'ptr' }> {
249
+ if (declared?.kind !== 'ptr') {
250
+ return false;
251
+ }
252
+ const ct = exprCType(value, varType);
253
+ return ct !== undefined && ct.kind !== 'ptr' && ct.kind !== 'array';
254
+ }
255
+
67
256
  export function exprCType(e: Expr, varType: (name: string) => IrType | undefined): IrType | undefined {
68
257
  const rec = (x: Expr): IrType | undefined => exprCType(x, varType);
69
258
  switch (e.k) {
@@ -121,10 +310,10 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
121
310
  if (bt?.kind === 'ptr' && bt.to.kind === 'struct' && (bt.to.size === undefined || bt.to.size === e.width)) {
122
311
  return bt.to;
123
312
  }
124
- if (bt?.kind === 'ptr' && derefStrideOk(bt, e.width)) {
313
+ if (bt?.kind === 'ptr' && derefStrideOk(bt, e.width, e.signed)) {
125
314
  return bt.to;
126
315
  }
127
- if (bt?.kind === 'array' && derefStrideOk(bt, e.width)) {
316
+ if (bt?.kind === 'array' && derefStrideOk(bt, e.width, e.signed)) {
128
317
  return bt.elem;
129
318
  }
130
319
  return scalarTypeForAccess(e.width, e.signed);