@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
package/src/l3/storage.ts CHANGED
@@ -17,7 +17,10 @@
17
17
  // silently a different object: `addressableGlobals`;
18
18
  // • it asks which of the three classes a bare `assign` name denotes at all: `nameStorage`, the
19
19
  // primitive the other two are the shadow-sensitive projections over.
20
- import type { SFn } from './ast';
20
+ // One question about LOCAL storage sits beside them, because it is asked from the same lists:
21
+ // which expressions are a local object's ADDRESS (`localAddresses`), the frame bytes a respelled
22
+ // tree must keep reading as integers (contracts.ts `assertLocalStorageStaysInteger`).
23
+ import { type Expr, type SFn, type Stmt, assignsIn, dotBase, stmtChildren, taintOverAssigns, walkExprs } from './ast';
21
24
 
22
25
  export type Storage = 'local' | 'param' | 'global';
23
26
 
@@ -46,3 +49,97 @@ export function declaredGlobals(sfn: SFn): Set<string> {
46
49
  export function addressableGlobals(sfn: SFn): Set<string> {
47
50
  return new Set([...nameStorage(sfn)].filter(([, s]) => s === 'global').map(([n]) => n));
48
51
  }
52
+
53
+ /** The ADDRESSES of a tree's local objects — the frame bytes, which nothing but the function's own
54
+ * accesses and the callees it hands an address to can reach.
55
+ *
56
+ * `isAddress(e)`: `e` evaluates to an address inside a local object — a local array (`sp0`,
57
+ * which decays), a local's address (`&v`), a local ever assigned one (`p0 = (s32 *)sp0;`, a
58
+ * flow-insensitive taint), under a cast or moved by `+`/`-`. PRECISE in the direction that
59
+ * matters to its readers: a value READ OUT of the bytes (`((s32 *)sp0)[i]`, a pointer the frame
60
+ * holds) is not an address of them, so an access through it is not an access to the frame.
61
+ * Known gap, on the side of answering no: an array MEMBER spelled as a `field` (`s.arr`) is an
62
+ * address C decays, and this does not follow it.
63
+ *
64
+ * `accessAddress(e)`: the operand an access node reads its address from — an `index`'s base, an
65
+ * arrow `field`'s base, a dot `field`'s array element's base, or a struct VALUE's own address
66
+ * (`s.f` reads at `&s`). A param counts as a local throughout: its home is the frame once its
67
+ * address is taken, and a param assigned an address holds it as a local would. */
68
+ export function localAddresses(sfn: SFn): {
69
+ isAddress: (e: Expr) => boolean;
70
+ accessAddress: (e: Extract<Expr, { k: 'index' } | { k: 'field' }>) => Expr;
71
+ } {
72
+ const storage = nameStorage(sfn);
73
+ // a param is a local variable of the callee: its home is the frame once its address is taken
74
+ const isLocal = (n: string): boolean => storage.get(n) === 'local' || storage.get(n) === 'param';
75
+ const addressIn = (e: Expr, tainted: ReadonlySet<string>): boolean => {
76
+ switch (e.k) {
77
+ case 'var':
78
+ case 'postincr':
79
+ return tainted.has(e.name);
80
+ case 'addr':
81
+ return isLocal(e.name);
82
+ case 'cast':
83
+ return addressIn(e.e, tainted);
84
+ case 'bin':
85
+ return (
86
+ (e.op === '+' && (addressIn(e.l, tainted) || addressIn(e.r, tainted))) ||
87
+ (e.op === '-' && addressIn(e.l, tainted))
88
+ );
89
+ default:
90
+ return false;
91
+ }
92
+ };
93
+ const tainted = taintOverAssigns(
94
+ assignsIn(sfn.body),
95
+ sfn.locals.filter((l) => l.type.kind === 'array').map((l) => l.name),
96
+ (a, t) => isLocal(a.name) && addressIn(a.value, t),
97
+ );
98
+ return {
99
+ isAddress: (e) => addressIn(e, tainted),
100
+ accessAddress: (e) =>
101
+ e.k === 'index'
102
+ ? e.base
103
+ : (dotBase(e)?.base ?? (e.dot === true && e.base.k === 'var' ? { k: 'addr', name: e.base.name } : e.base)),
104
+ };
105
+ }
106
+
107
+ /** Whether a local object's address LEAVES the function — passed to a call, stored to memory,
108
+ * assigned to a non-local, or returned — AND the function itself writes local storage. Both
109
+ * halves are what let a pointer the function obtains LATER (a call's result, a load) name bytes
110
+ * the function also stores to: a callee that keeps the address can hand it back.
111
+ *
112
+ * OVER-WIDE ON PURPOSE, and what it costs is measured: a store to a DEVICE register counts as an
113
+ * escape, since the tree cannot tell a DMA source register (`REG_DMA3SAD = sp0;`, the GBA's fill
114
+ * idiom) from memory a later load reads back. On kleod:PauseMenuScreenHandler that refuses every
115
+ * `/ptr-field` candidate, none of which was its winner. */
116
+ export function localStorageEscapesWritten(sfn: SFn): boolean {
117
+ const { isAddress, accessAddress } = localAddresses(sfn);
118
+ const storage = nameStorage(sfn);
119
+ const held = (n: string): boolean => storage.get(n) === 'local' || storage.get(n) === 'param';
120
+ let escapes = false;
121
+ let written = false;
122
+ const addressTaken = new Set<string>();
123
+ for (const e of walkExprs(sfn.body)) {
124
+ if (e.k === 'call' && e.args.some(isAddress)) {
125
+ escapes = true;
126
+ }
127
+ if (e.k === 'addr' && held(e.name)) {
128
+ addressTaken.add(e.name);
129
+ }
130
+ }
131
+ const visit = (s: Stmt): void => {
132
+ if (s.k === 'store') {
133
+ escapes ||= isAddress(s.value);
134
+ written ||= (s.lval.k === 'index' || s.lval.k === 'field') && isAddress(accessAddress(s.lval));
135
+ } else if (s.k === 'assign') {
136
+ escapes ||= !held(s.name) && isAddress(s.value);
137
+ written ||= addressTaken.has(s.name);
138
+ } else if (s.k === 'return' && s.value !== undefined) {
139
+ escapes ||= isAddress(s.value);
140
+ }
141
+ stmtChildren(s).forEach(visit);
142
+ };
143
+ sfn.body.forEach(visit);
144
+ return escapes && written;
145
+ }
@@ -16,8 +16,12 @@
16
16
  // to recompute it (a dead-store DCE that drops a tree's only reference would otherwise leave a
17
17
  // stale ref, transitively reintroducing the hazards the collector excludes). Deriving at the
18
18
  // consumption point makes staleness impossible by construction.
19
+ import type { IrType } from '../ir/types';
20
+ import { type AggregateLayout, type FnProto, type ParamType, type Prototypes, spellableProto } from '../proto';
19
21
  import type { SymbolInfo } from '../symbols';
20
- import { Expr, Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
22
+ import type { TargetDescription } from '../target';
23
+ import { Expr, SFn, Stmt, exprChildren, mentionedName, stmtChildren, stmtExprs } from './ast';
24
+ import { pointerCellsOf, pointerPartners } from './ptrcell';
21
25
 
22
26
  /** One recorded VALUE reference — a name the tree references plus the facts to declare it. */
23
27
  export interface SymbolRef {
@@ -27,43 +31,137 @@ export interface SymbolRef {
27
31
  * own IR — attached by the enumeration (rank.ts bareGlobalAccessFacts), consumed by the
28
32
  * declaration synthesis (declare.ts) as the width/signedness authority for `extern T name;`. */
29
33
  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. */
34
+ /** NO SYMBOL MAP ACCOUNTS FOR THIS NAME — that is the whole predicate, and it is one fact with
35
+ * two provenances the consumer must be able to tell apart.
36
+ *
37
+ * WITHOUT `proto`, the declaration is a HYPOTHESIS: the name and (through `access`) its width
38
+ * and signedness were read out of the candidate's own asm — the very bytes the candidate is
39
+ * then scored against. A fitted declaration cannot LOSE score, so it is not covered by
40
+ * declare.ts's only-loses-score argument; it can manufacture agreement instead. Which is
41
+ * legitimate (the artifact is decls + source, and it does compile to those bytes) exactly as
42
+ * long as the consumer SHOWS the declarations with the verdict.
43
+ *
44
+ * WITH `proto`, the declaration's TEXT is the project's own — a call target asmlift has a
45
+ * prototype for — and asmlift invented nothing about it. It is still marked, because the
46
+ * predicate this flag states is about the symbol MAP and not about who wrote the type, and
47
+ * because the score rests on that line being in the translation unit either way. A consumer
48
+ * wording the block reads `proto` to say which it is looking at; dropping the mark instead
49
+ * would under-report a declaration the verdict depends on, which is the direction
50
+ * `synthesizedRefs` exists to avoid. */
37
51
  synthesized?: true;
52
+ /** A CALL TARGET's own declaration, re-spelled from the prototype the project supplied — the
53
+ * parameter type texts and the return type text, verbatim as the user wrote them. Present only
54
+ * where {@link spellableProto} could read a COMPLETE C prototype out of the `FnProto`, and it
55
+ * is what `declare.ts` prints instead of an `extern`: a call target is a function, and
56
+ * `extern u32 llsrc;` is not a declaration of it. Absent where the symbol map calls the name
57
+ * something other than code — two facts that disagree are not one fact — and that subtraction
58
+ * happens at the table (`proto.ts` `prototypesFromSymbols`), so the frontend loses the same
59
+ * fact in the same place rather than acting on one this withheld. */
60
+ proto?: { readonly params: readonly ParamType[]; readonly returns: ParamType };
61
+ /** The struct a call target returns through memory — a type the project's headers own
62
+ * (ir/types.ts `declared`), so `declare.ts` defines it beside `proto`, whose `returns` spells it:
63
+ * by its tag (`name`), from the members the declaration lists (`layout`, which the lift laid out
64
+ * to type the call). */
65
+ returned?: { readonly name: string; readonly declared: string; readonly layout: AggregateLayout };
66
+ /** The structs a call target is passed BY VALUE (`byValue` on the call), defined the same way
67
+ * from the declaration's `byValueLayouts`. */
68
+ passed?: readonly { readonly name: string; readonly declared: string; readonly layout: AggregateLayout }[];
69
+ /** A global the body passes BARE as a struct by value (`take(gS)`), as `struct <tag>`: a
70
+ * name-only declaration of it is that struct, which the call needs and an integer cell rejects. */
71
+ passedAs?: string;
72
+ /** The bare name holds a pointer (l3/ptrcell.ts): the body stores one into it, or it meets such a
73
+ * cell bare and nothing reads it as an integer (`pointerPartners`). A name-only declaration of it
74
+ * is a pointer, which the store needs and an integer cell rejects on CodeWarrior. */
75
+ holdsPointer?: true;
76
+ /** A call target with NO spellable prototype whose address a function-scope static's (or an
77
+ * anonymous image's) initializer holds. The static's `(u32)&F` precedes every call, so the
78
+ * implicit declaration a call makes comes too late for it; `declare.ts` prints that same
79
+ * declaration first, `int F();`, which states no parameter and so binds the call as before. */
80
+ implicitCall?: true;
81
+ /** THE STRUCT THE FUNCTION ITSELF RETURNS ({@link ownReturnDefinition}): a ref under the
82
+ * function's own name that declares nothing of it — its definition is its declaration — and only
83
+ * defines `returned`, the type its own signature spells. */
84
+ definesReturn?: true;
85
+ }
86
+
87
+ /** The ref that defines the struct `tree` itself returns by value — a type the project's headers
88
+ * own (ir/types.ts `declared`), so the candidate's declarations define it as they define a
89
+ * callee's (`SymbolRef.returned`) — from the members its own declaration `own` lists. Undefined
90
+ * where it returns no such struct, or its declaration lists no members. */
91
+ export function ownReturnDefinition(tree: SFn, own: FnProto | undefined): SymbolRef | undefined {
92
+ const t = tree.retType;
93
+ const layout = own?.returnLayout;
94
+ if (t.kind !== 'struct' || t.declared === undefined || layout?.members === undefined) {
95
+ return undefined;
96
+ }
97
+ return {
98
+ name: tree.name,
99
+ info: { name: tree.name, kind: 'code' },
100
+ returned: { name: t.name, declared: t.declared, layout },
101
+ definesReturn: true,
102
+ };
38
103
  }
39
104
 
40
105
  /** The declarable symbols a structured body references in a VALUE context — the input to the
41
106
  * scoring layer's declaration synthesis. A name counts when it appears as a `var`/`addr` leaf
42
107
  * and the caller's dictionary knows it (bare `gSym`, `&gSym`, `(u32)Func`, a `field` base — all reduce to
43
- * those leaves). A name that is ANY call's target is excluded entirely, even if also
44
- * value-referenced: prototyping a called symbol `void F(void);` hard-errors under gcc-2.9
45
- * when the call passes args, while leaving it undeclared keeps today's implicit-declaration
46
- * behavior (the one honest option without arity knowledge). The function's OWN name
47
- * (`selfName`) is excluded too — the candidate's definition IS its declaration, and a
108
+ * those leaves). The function's OWN name
109
+ * (`tree.name`) is excluded too — the candidate's definition IS its declaration, and a
48
110
  * synthesized `void F(void);` above `s32 F(...)` is a conflicting-types hard error (a
49
111
  * self-address reference resolves against the definition itself).
50
112
  *
113
+ * A CALL TARGET IS DECLARED WHEN, AND ONLY WHEN, THE PROJECT DECLARED IT COMPLETELY. `void
114
+ * F(void);` hard-errors under gcc-2.9 the moment the call passes an argument, so with no arity
115
+ * knowledge the honest option is to leave the name undeclared and keep C's implicit-declaration
116
+ * behaviour — which is what this did for every call target and still does for every one the
117
+ * prototype table cannot spell. Where it CAN (`spellableProto`), that knowledge is exactly what
118
+ * the refusal said it lacked, and the declaration is the project's own text rather than a guess.
119
+ * It is needed: an implicit declaration is `int`, so `(s64)llsrc()` sign-extends a word and the
120
+ * candidate carries an `asr` the target does not have.
121
+ *
51
122
  * Both exclusions are REPORTED through `onRefused`, on the caller's own refusal channel: they
52
123
  * leave a name undeclared for asmlift's own reason, and a consumer's list of those reasons is
53
124
  * incomplete without them. */
54
125
  export function collectSymbolRefs(
55
- body: Stmt[],
126
+ tree: SFn,
56
127
  symbols: Map<string, SymbolInfo>,
57
- selfName: string,
128
+ /** the project's prototype table — the only thing that can turn a call target into a
129
+ * declaration. REQUIRED, not optional: a caller that forgot it would silently get the old
130
+ * blanket refusal back, which is the shape of an optional refusal a second caller switches off. */
131
+ prototypes: Prototypes,
132
+ /** which declared types the frontend lays out, so which a printed prototype may carry */
133
+ target: Pick<TargetDescription, 'doubleArgWords'>,
58
134
  onRefused?: (name: string, reason: 'call-target' | 'self-name') => void,
59
135
  ): SymbolRef[] {
60
136
  const called = new Set<string>();
137
+ // the struct a callee returns through memory or by value in registers, as the lift typed it
138
+ const returned = new Map<string, Extract<IrType, { kind: 'struct' }>>();
139
+ // …and the structs it is passed by value, by declared parameter index
140
+ const passedTo = new Map<string, Map<number, Extract<IrType, { kind: 'struct' }>>>();
141
+ // …and the globals passed bare as one, by its spelling
142
+ const passedAs = new Map<string, string>();
61
143
  const valueRefs = new Set<string>();
62
144
  const visitExpr = (e: Expr): void => {
63
- if (e.k === 'call') {
145
+ const named = mentionedName(e);
146
+ if (e.k === 'call' && typeof e.fn === 'string') {
64
147
  called.add(e.fn);
65
- } else if ((e.k === 'var' || e.k === 'addr') && symbols.has(e.name)) {
66
- valueRefs.add(e.name);
148
+ const struct = e.sret ?? e.returns;
149
+ if (struct?.kind === 'struct') {
150
+ returned.set(e.fn, struct);
151
+ }
152
+ for (const b of e.byValue ?? []) {
153
+ if (b.type.kind === 'struct') {
154
+ const params = passedTo.get(e.fn) ?? passedTo.set(e.fn, new Map()).get(e.fn)!;
155
+ params.set(b.param, b.type);
156
+ const arg = e.args[b.at];
157
+ if (arg?.k === 'var' && symbols.has(arg.name)) {
158
+ // by its tag, which declares even ahead of the definition and of a typedef of it
159
+ passedAs.set(arg.name, `struct ${b.type.name}`);
160
+ }
161
+ }
162
+ }
163
+ } else if (named !== undefined && symbols.has(named)) {
164
+ valueRefs.add(named);
67
165
  }
68
166
  exprChildren(e).forEach(visitExpr);
69
167
  };
@@ -76,19 +174,80 @@ export function collectSymbolRefs(
76
174
  stmtExprs(s).forEach(visitExpr);
77
175
  stmtChildren(s).forEach(visitStmt);
78
176
  };
79
- body.forEach(visitStmt);
80
- return [...valueRefs]
177
+ tree.body.forEach(visitStmt);
178
+ // A function-scope static's initializer names the globals whose addresses it holds
179
+ // (`(u32)&MetatileBehavior_IsTallGrass`), which the candidate must declare as much as the body's —
180
+ // and so does an anonymous image, which the emitter folds into a local's initializer.
181
+ const initAddresses = new Set<string>();
182
+ for (const st of [...(tree.statics ?? []), ...(tree.images ?? [])]) {
183
+ for (const v of st.init ?? []) {
184
+ if (v !== null && typeof v === 'object' && symbols.has(v.symbol)) {
185
+ valueRefs.add(v.symbol);
186
+ initAddresses.add(v.symbol);
187
+ }
188
+ }
189
+ }
190
+ const holdsPointer = new Set([...pointerCellsOf(tree), ...pointerPartners(tree)]);
191
+ // A call target is a ref in its own right, whether or not the body also names it as a value —
192
+ // `Object.hasOwn`, because `prototypes` is caller-supplied JSON and a callee may be named
193
+ // `toString`. The union is sorted as one list so the rendered block stays deterministic.
194
+ const proto = new Map<string, NonNullable<SymbolRef['proto']>>();
195
+ for (const n of called) {
196
+ // TWO FACTS ABOUT ONE NAME, AND A DISAGREEMENT IS SILENCE — decided at the TABLE, not here. A
197
+ // map entry that states a data shape for this name contradicts the prototype that calls it a
198
+ // function, and `proto.ts` `prototypesFromSymbols` has already taken the `returns` off such an
199
+ // entry, so what arrives here states no printable prototype and the frontend read the call the
200
+ // same way. Testing it a second time at this reader is what let the two answer differently.
201
+ const p = Object.hasOwn(prototypes, n)
202
+ ? spellableProto(prototypes[n], target, returned.get(n), new Set(passedTo.get(n)?.keys()))
203
+ : undefined;
204
+ if (p !== undefined) {
205
+ proto.set(n, p);
206
+ }
207
+ }
208
+ return [...new Set([...valueRefs, ...proto.keys()])]
81
209
  .sort()
82
210
  .filter((n) => {
83
- if (called.has(n)) {
211
+ // …except where an initializer holds its address: there the name is spelled ahead of every
212
+ // call, so leaving it to the call's implicit declaration leaves the initializer naming an
213
+ // undeclared identifier, and the implicit declaration itself is the one that is printed
214
+ // (`SymbolRef.implicitCall`).
215
+ if (called.has(n) && !proto.has(n) && !initAddresses.has(n)) {
84
216
  onRefused?.(n, 'call-target');
85
217
  return false;
86
218
  }
87
- if (n === selfName) {
219
+ if (n === tree.name) {
88
220
  onRefused?.(n, 'self-name');
89
221
  return false;
90
222
  }
91
223
  return true;
92
224
  })
93
- .map((n) => ({ name: n, info: symbols.get(n)! }));
225
+ .map((n) => {
226
+ const p = proto.get(n);
227
+ // A call target the map never mentioned still has to carry an `info` — it is a code symbol
228
+ // and that is all this knows about it. `declare.ts` prints the prototype and never reaches
229
+ // the shape, so nothing here is guessed about storage that does not exist.
230
+ const info = symbols.get(n) ?? { name: n, kind: 'code' as const };
231
+ const r = p ? returned.get(n) : undefined;
232
+ const layout = r?.declared !== undefined ? prototypes[n].returnLayout : undefined;
233
+ const definition = layout?.members !== undefined ? { name: r!.name, declared: r!.declared!, layout } : undefined;
234
+ const passed = p
235
+ ? [...(passedTo.get(n) ?? [])].flatMap(([k, t]) => {
236
+ const l = prototypes[n].byValueLayouts?.[k];
237
+ return l?.members !== undefined && t.declared !== undefined
238
+ ? [{ name: t.name, declared: t.declared, layout: l }]
239
+ : [];
240
+ })
241
+ : [];
242
+ return {
243
+ name: n,
244
+ info,
245
+ ...(p ? { proto: p } : {}),
246
+ ...(definition ? { returned: definition } : {}),
247
+ ...(passed.length > 0 ? { passed } : {}),
248
+ ...(passedAs.has(n) ? { passedAs: passedAs.get(n)! } : {}),
249
+ ...(holdsPointer.has(n) ? { holdsPointer: true as const } : {}),
250
+ ...(called.has(n) && !p ? { implicitCall: true as const } : {}),
251
+ };
252
+ });
94
253
  }
@@ -53,37 +53,24 @@
53
53
  // lifts alike. What no arm of the fan spells is the third form, the hidden statement merged
54
54
  // while KEEPING the record's order.
55
55
  //
56
- // SCOPE. Only `assign`/`store`/`exprstmt` merge, compared structurally through `exprEquals`.
56
+ // SCOPE. Only `assign`/`store`/`exprstmt` merge, compared structurally through `stmtEquals`.
57
57
  // Control flow (`break`/`continue`/`return`) is excluded: moving one out of an arm changes which
58
- // statements the arm can still reach. Nested `if`/loop/`switch` statements are excluded because
59
- // comparing them needs a full `Stmt` congruence, and there is no second inhabitant for one — the
60
- // `Expr`-level comparison is the part that already exists, is tested, and is all this needs.
58
+ // statements the arm can still reach. Nested `if`/loop/`switch` statements are excluded: merging
59
+ // one needs a congruence over loops and `switch` too, which `stmtEquals` does not have, and there
60
+ // is no second inhabitant for one.
61
61
  //
62
62
  // An `ASMLIFT_ERROR` marker ending both arms merges like anything else. The gap stays loud (the
63
63
  // artifact still refuses to compile) but `collectMarkers` then reports it once rather than twice,
64
64
  // which is accurate — it is one gap that ran on both paths.
65
65
  import type { SFn, Stmt } from './ast';
66
- import { exprEquals } from './ast';
66
+ import { stmtEquals, unionStmtEvidence } from './ast';
67
67
 
68
68
  /** Statements this pass may move. Deliberately narrow — see SCOPE. */
69
69
  type Mergeable = Extract<Stmt, { k: 'assign' } | { k: 'store' } | { k: 'exprstmt' }>;
70
70
  const isMergeable = (s: Stmt): s is Mergeable => s.k === 'assign' || s.k === 'store' || s.k === 'exprstmt';
71
71
 
72
72
  /** Do these two statements write the same thing from the same expression? */
73
- function sameStmt(a: Stmt, b: Stmt): boolean {
74
- if (!isMergeable(a) || !isMergeable(b) || a.k !== b.k) {
75
- return false;
76
- }
77
- if (a.k === 'assign' && b.k === 'assign') {
78
- return a.name === b.name && exprEquals(a.value, b.value);
79
- }
80
- if (a.k === 'store' && b.k === 'store') {
81
- return exprEquals(a.lval, b.lval) && exprEquals(a.value, b.value);
82
- }
83
- const av = (a as Extract<Stmt, { k: 'exprstmt' }>).value;
84
- const bv = (b as Extract<Stmt, { k: 'exprstmt' }>).value;
85
- return exprEquals(av, bv);
86
- }
73
+ const sameStmt = (a: Stmt, b: Stmt): boolean => isMergeable(a) && isMergeable(b) && stmtEquals(a, b);
87
74
 
88
75
  /** Rewrite one statement, then the list it lives in. */
89
76
  function rewrite(s: Stmt): Stmt[] {
@@ -102,7 +89,9 @@ function rewrite(s: Stmt): Stmt[] {
102
89
  // it, but it is the one shape where a zero-arm merge is qualitatively unlike a partial one.
103
90
  const tail: Stmt[] = [];
104
91
  while (then.length > 0 && els.length > 0 && sameStmt(then[then.length - 1], els[els.length - 1])) {
105
- tail.unshift(then[then.length - 1]);
92
+ // The two print alike, but an unsigned compare in one prints like the signed one in the
93
+ // other: the moved statement carries the evidence of both (ast.ts `unionEvidence`).
94
+ tail.unshift(unionStmtEvidence(then[then.length - 1], els[els.length - 1]));
106
95
  then.pop();
107
96
  els.pop();
108
97
  }