@asmlift/core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/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
|
-
|
|
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
|
+
}
|
package/src/l3/symbol-refs.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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).
|
|
44
|
-
*
|
|
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
|
-
|
|
126
|
+
tree: SFn,
|
|
56
127
|
symbols: Map<string, SymbolInfo>,
|
|
57
|
-
|
|
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
|
-
|
|
145
|
+
const named = mentionedName(e);
|
|
146
|
+
if (e.k === 'call' && typeof e.fn === 'string') {
|
|
64
147
|
called.add(e.fn);
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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) =>
|
|
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
|
}
|
package/src/l3/tailmerge.ts
CHANGED
|
@@ -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 `
|
|
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
|
|
59
|
-
//
|
|
60
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|