@asmlift/core 0.9.0 → 0.11.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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
|
@@ -12,26 +12,48 @@
|
|
|
12
12
|
// lowers to is built here too (`CallDeclarations.lower`), and asks the frontend (`CallSite`) only
|
|
13
13
|
// what its ISA alone knows: how it reads and writes a register, where an argument word past the
|
|
14
14
|
// registers lives, and how it builds a pair.
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
import {
|
|
16
|
+
type ByValueArg,
|
|
17
|
+
aggregateType,
|
|
18
|
+
byValueCallArgs,
|
|
19
|
+
byValueRefusal,
|
|
20
|
+
ownArgumentWordsOn,
|
|
21
|
+
ownLeadingWordParamsOn,
|
|
22
|
+
returnedAggregate,
|
|
23
|
+
returnsInMemory,
|
|
24
|
+
returnsWithoutHiddenPointer,
|
|
25
|
+
} from '../aggregate';
|
|
26
|
+
import { type Block, type Op, REGISTER_STRUCT, type Value, calleeName, mkOp, mkValue } from '../ir/core';
|
|
27
|
+
import { qualified } from '../ir/discipline';
|
|
28
|
+
import { OPCODES, type OpSig } from '../ir/opcodes';
|
|
29
|
+
import { type IrType, T, structSpelling, typeToString } from '../ir/types';
|
|
18
30
|
import {
|
|
19
31
|
type FnProto,
|
|
32
|
+
type ParamClass,
|
|
20
33
|
type Prototypes,
|
|
21
|
-
|
|
34
|
+
calleeDeclaration,
|
|
22
35
|
declaredCallArgs,
|
|
23
36
|
declaredReturnWidth,
|
|
24
37
|
declaresAggregateReturn,
|
|
25
38
|
declaresParams,
|
|
26
39
|
declaresVoidReturn,
|
|
40
|
+
floatingSpelling,
|
|
27
41
|
isFloatingSpelling,
|
|
42
|
+
ownDeclaresFloatingParam,
|
|
43
|
+
paramClassOf,
|
|
28
44
|
spellableProto,
|
|
45
|
+
variadicRefusal,
|
|
29
46
|
wordsOf,
|
|
30
47
|
} from '../proto';
|
|
31
|
-
import {
|
|
32
|
-
import
|
|
33
|
-
import type
|
|
48
|
+
import { type ContextGlobals, structNamed } from '../proto-context';
|
|
49
|
+
import { floatSidesOf, helperPrototypes, isFloatHelper, isWideHelper, lookupHelper } from '../runtime-helpers';
|
|
50
|
+
import { type SymbolMap, symbolsByName } from '../symbols';
|
|
51
|
+
import { type TargetDescription, argRegisterHoles } from '../target';
|
|
52
|
+
import { type CalleeBodyBound, asksCalleeBody } from './callee-body';
|
|
53
|
+
import { type FrontendRefusal, FrontendUnsupportedError } from './errors';
|
|
54
|
+
import { type OwnDeclaration, ownDeclarationOf } from './frontend';
|
|
34
55
|
import type { HighHalves } from './high-half';
|
|
56
|
+
import { unspellableReason } from './reloc-symbol';
|
|
35
57
|
import { type SsaBuilder, clobberedByCall, fallbackArgc } from './ssa';
|
|
36
58
|
|
|
37
59
|
/** What the frontend's own call lowering can carry out. */
|
|
@@ -68,6 +90,18 @@ export interface DeclaredCall {
|
|
|
68
90
|
/** the declared parameter count, a hidden pointer aside */
|
|
69
91
|
readonly params: number;
|
|
70
92
|
readonly returned?: StructReturn | 'register';
|
|
93
|
+
/** each struct passed by value, by its first word in `widths` (a hidden pointer counted) */
|
|
94
|
+
readonly byValue?: readonly ByValueArg[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** WHAT A DECLARED PARAMETER IS, as far as where the caller places it goes (`paramClassOf`,
|
|
98
|
+
* proto.ts, which `declaredCall`'s widths read too). */
|
|
99
|
+
export type { ParamClass };
|
|
100
|
+
|
|
101
|
+
/** One declared parameter: its class, and the spelling it was read from (empty for a count). */
|
|
102
|
+
export interface DeclaredParam {
|
|
103
|
+
readonly kind: ParamClass;
|
|
104
|
+
readonly spelling: string;
|
|
71
105
|
}
|
|
72
106
|
|
|
73
107
|
/** What one call is, decided from its callee's declaration before any instruction is read. */
|
|
@@ -77,17 +111,27 @@ export interface CallPlan {
|
|
|
77
111
|
readonly widths: readonly number[] | null;
|
|
78
112
|
/** the indices into `widths` its declaration types `double` */
|
|
79
113
|
readonly doubles: ReadonlySet<number>;
|
|
114
|
+
/** the structs its declaration passes by value, by their words in `widths` */
|
|
115
|
+
readonly byValue?: readonly ByValueArg[];
|
|
80
116
|
readonly returns:
|
|
81
117
|
| { readonly kind: 'word' }
|
|
82
118
|
| { readonly kind: 'void' }
|
|
83
119
|
| { readonly kind: 'pair' }
|
|
84
|
-
| {
|
|
120
|
+
| {
|
|
121
|
+
readonly kind: 'register-struct';
|
|
122
|
+
/** the return register holds the struct's image, which a local of it is read off
|
|
123
|
+
* (`registerStructLocal`); false, it holds nothing the caller may read */
|
|
124
|
+
readonly image: boolean;
|
|
125
|
+
}
|
|
85
126
|
| { readonly kind: 'memory-struct'; readonly type: IrType };
|
|
86
127
|
/** what the call leaves holding nothing this function can name, for `SsaBuilder.noteCall` */
|
|
87
128
|
readonly clobbers: readonly string[];
|
|
88
129
|
/** the callee is declared void, whether or not this lowering writes no value for it (`returns`
|
|
89
130
|
* is `void` only where `CallLowering.voidReturn` says so) */
|
|
90
131
|
readonly declaredVoid: boolean;
|
|
132
|
+
/** the floating type an ordinary callee is declared to return where the target has no FPU, so
|
|
133
|
+
* the return register holds its bits (`raise/floathelpers.ts` holds every read of them) */
|
|
134
|
+
readonly floatReturn?: 'float' | 'double' | 'long double';
|
|
91
135
|
}
|
|
92
136
|
|
|
93
137
|
/** WHAT EACH CALLEE'S DECLARATION SAYS, and what a call leaves holding nothing this function can
|
|
@@ -103,11 +147,37 @@ export interface CallDeclarations {
|
|
|
103
147
|
/** the callee's declared argument widths, for a frontend that places them before it lowers the
|
|
104
148
|
* call; null where no declaration sizes them */
|
|
105
149
|
declaredCall(callee: string): DeclaredCall | null;
|
|
150
|
+
/** what each parameter the callee's declaration spells is (`ParamClass`), read through the tiers
|
|
151
|
+
* `declaredCall` reads; null where no declaration states the parameters */
|
|
152
|
+
declaredParams(callee: string): readonly DeclaredParam[] | null;
|
|
106
153
|
/** `lower`'s first half, the call decided from the declaration alone: a value its tests compare
|
|
107
154
|
* whole, where `lower` leaves IR */
|
|
108
155
|
plan(callee: string): CallPlan;
|
|
109
156
|
/** the call `site` makes, planned and lowered: its `call` op, its result and its clobbers */
|
|
110
157
|
lower(site: CallSite): void;
|
|
158
|
+
/** every value an argument register may hold at a call `lower` lowered at an UNDECLARED arity — a
|
|
159
|
+
* guess, or the bound a call through an argument register sets — gathered as each such call is
|
|
160
|
+
* lowered. One the call does not pass is unused by the LIFT, not proved unused by the source: the
|
|
161
|
+
* dead-read pin refuses it (frontend/dead-reads.ts `held-at-undeclared-call`). */
|
|
162
|
+
readonly heldAtUndeclaredCall: ReadonlySet<Value>;
|
|
163
|
+
/** THE #351 GAPLESS GUESS, NOT LET IN THROUGH A JUMP TABLE'S CASES. Once every call is lowered:
|
|
164
|
+
* where the function lifts only because of what an elided or kept dispatch left in its registers
|
|
165
|
+
* — a dispatch's marker or scaled index reaching some guessed call's argument registers
|
|
166
|
+
* (`CallSite.parked`, `CallSite.scaledIndex`; a PowerPC pair's r4 too), which stands where the
|
|
167
|
+
* pre-dispatch value had made a gap refuse, or `caseStateRead`, a read of the scaled index a case
|
|
168
|
+
* target is entered with — refuse the first guessed arity the bytes cannot tell from another:
|
|
169
|
+
* - its last counted register holds a value some path from the dispatch's block to the call
|
|
170
|
+
* did not write (`SsaBuilder.writtenOnEveryPath`; a cross-jumped call's block writes nothing,
|
|
171
|
+
* its cases do), a value passed on unchanged (`g0(a, 9)` lifted `g0(a0, 9, a0)`, agbcc's pre-dispatch copy of
|
|
172
|
+
* a0 still in r2; mwcc `return g()` lifted `return g(a0)`);
|
|
173
|
+
* - a counted register holds a value an effect of its block uses, a store's address or value
|
|
174
|
+
* left in the register (`gc = ga; g()` lifted `g(&gc, v0)`);
|
|
175
|
+
* - a register it leaves out below that marker still holds its incoming value, an argument
|
|
176
|
+
* passed on or not (mwcc `g(a)` with the table base in r4 lifted `g()`).
|
|
177
|
+
* Elsewhere the guess takes these, an open gap (`g()` and `g(a0)` are the same bytes); there the
|
|
178
|
+
* function declined before the dispatch's registers were modelled, and a decline is not turned
|
|
179
|
+
* into that guess. */
|
|
180
|
+
refuseDispatchUnlockedPassThrough(caseStateRead: boolean): void;
|
|
111
181
|
}
|
|
112
182
|
|
|
113
183
|
/** WHAT ONE ISA ALONE KNOWS ABOUT A CALL: everything `CallDeclarations.lower` asks a frontend. The
|
|
@@ -124,17 +194,50 @@ export interface CallSite {
|
|
|
124
194
|
write(reg: string, v: Value): void;
|
|
125
195
|
/** argument word `k` past the registers, where this ABI places it (`CallLowering.stackArgs`) */
|
|
126
196
|
stackWord?(k: number): Value;
|
|
197
|
+
/** the stack words the frontend licensed for a call nothing declares, off its callee's body
|
|
198
|
+
* (frontend/stackargs.ts `StackArgsCall.bodyReads`): the call then takes every argument register
|
|
199
|
+
* and these words, and its arity is no guess. Absent or 0 where none was licensed. */
|
|
200
|
+
readonly stackWords?: number;
|
|
127
201
|
/** how this frontend builds and splits a pair (`CallLowering.pairs`) */
|
|
128
202
|
readonly pairs?: CallPairs;
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
203
|
+
/** what this frontend defines registers with that is no value and so no argument — a pending
|
|
204
|
+
* high half (frontend/high-half.ts), a jump table's base (frontend/jump-table.ts) */
|
|
205
|
+
readonly placeholders?: Pick<HighHalves, 'has' | 'guardRead'>;
|
|
206
|
+
/** whether `v`, in argument register `reg`, is what a jump-table dispatch left in that very
|
|
207
|
+
* register (frontend/jump-table.ts `parkedIn`): no argument, as a table base is none, so a guessed
|
|
208
|
+
* arity stops below the lowest register only it reaches (`fallbackArgc`'s `parked`), except one
|
|
209
|
+
* this ABI may skip (an O32 double's a1, a PowerPC pair's r4, a leading float's a0), which stays a
|
|
210
|
+
* gap. A copy of one below that ceiling is counted, and its read declines; a copy above it is
|
|
211
|
+
* dropped with everything else there (`mov r2,r1` ahead of `bl g` lifts `g(…)` without it), which
|
|
212
|
+
* only hand asm meets: no compiler copies a marker. */
|
|
213
|
+
readonly parked?: (v: Value, reg: string) => boolean;
|
|
214
|
+
/** whether `v`, in argument register `reg`, is the scaled index a jump-table dispatch left in that
|
|
215
|
+
* very register (frontend/jump-table.ts `scaledIndexIn`): a call handed it and one not compile
|
|
216
|
+
* alike, so a guessed arity that counts it refuses */
|
|
217
|
+
readonly scaledIndex?: (v: Value, reg: string) => boolean;
|
|
218
|
+
/** what a guessed arity refuses on. Required, as the gap refusal it feeds (`fallbackArgc`) has
|
|
219
|
+
* no opt-out: a count that stopped at the first register nothing reaches would drop every
|
|
220
|
+
* argument past an incoming one the call is handed unchanged (`logv(s, 1)` as `logv()`). */
|
|
221
|
+
readonly guess: {
|
|
222
|
+
/** where the call is, for the gap refusal: its address, or — for an ISA whose instructions
|
|
223
|
+
* carry none (Thumb's `.s` text) — a phrase that names it, printed as written */
|
|
224
|
+
readonly at: number | string;
|
|
225
|
+
/** refuses a guess of `argc` words this ABI's outgoing area could extend; absent where the
|
|
226
|
+
* frontend's frame model already refuses every word such a guess could leave out */
|
|
227
|
+
refuse?(argc: number): void;
|
|
137
228
|
};
|
|
229
|
+
/** handed the argument registers an UNDECLARED arity leaves out although a value reaches them —
|
|
230
|
+
* past a guessed count, or past the bound of a call through an argument register — and the ones
|
|
231
|
+
* it passes. The call drops what the left-out ones hold, so a frontend that knows one to be an
|
|
232
|
+
* argument all the same refuses here. */
|
|
233
|
+
readonly leftOut?: (regs: readonly string[], passed: readonly string[]) => void;
|
|
234
|
+
/** whether the frontend withholds the shorter `/setup-args` reading from THIS CALL whatever it
|
|
235
|
+
* cuts; the frontend says why where it sets it. A thunk: it is asked once every block is lowered. */
|
|
236
|
+
readonly withholdsSetupArgs?: () => boolean;
|
|
237
|
+
/** whether the shorter `/setup-args` reading of this call asks `leftOut` of EVERY register it
|
|
238
|
+
* cuts, not only of those the call's own block wrote (`GuessedCallSite.asksEveryCut`); the
|
|
239
|
+
* frontend says why where it sets it. A thunk: it is asked once every block is lowered. */
|
|
240
|
+
readonly asksEveryCut?: () => boolean;
|
|
138
241
|
}
|
|
139
242
|
|
|
140
243
|
/** A call through a register. It names no callee, so no declaration plans it: an argument
|
|
@@ -160,9 +263,26 @@ export function callDeclarations(
|
|
|
160
263
|
prototypes: Prototypes,
|
|
161
264
|
lowering: CallLowering,
|
|
162
265
|
fail: FrontendRefusal,
|
|
266
|
+
/** the function's own declaration, where one binds its body — asked as a call is lowered */
|
|
267
|
+
ownDeclaration: () => OwnDeclaration | undefined = () => ownDeclarationOf(name, prototypes, undefined),
|
|
268
|
+
/** the argument words a callee's own body reads at most, where it is in the input
|
|
269
|
+
* (frontend/callee-body.ts) — asked of a named call no declaration lays out */
|
|
270
|
+
calleeBodyBound?: CalleeBodyBound,
|
|
271
|
+
/** the callees taken at every argument register that may still hold its entry value
|
|
272
|
+
* (`LiftOptions.entryReadCalls`) */
|
|
273
|
+
entryReadCalls?: ReadonlySet<string>,
|
|
274
|
+
/** told of each call laid out at the stack words its callee's body loads (`LiftOptions.stackSized`) */
|
|
275
|
+
stackSized?: (callee: string, words: number) => void,
|
|
163
276
|
): CallDeclarations {
|
|
164
277
|
// What a call leaves holding nothing this function can name — checked against `argRegs` there.
|
|
165
278
|
const callClobbers = clobberedByCall(target);
|
|
279
|
+
// `refuseDispatchUnlockedPassThrough`'s two facts, gathered as each call is lowered: whether a
|
|
280
|
+
// dispatch's marker reached a guessed call's argument registers, and every guessed count the
|
|
281
|
+
// bytes cannot tell from another, in the order they were lowered, each with its refusal. `unread`
|
|
282
|
+
// says the register lies past what the callee's own body reads (frontend/callee-body.ts), which
|
|
283
|
+
// excuses it: asked only as the gate would refuse, so no body is lifted where it would not.
|
|
284
|
+
let parkedInArgs = false;
|
|
285
|
+
const passThrough: Array<{ call: string; reg: string; why: string; unread: () => boolean }> = [];
|
|
166
286
|
// …and what a call to a PAIR-RETURNING helper leaves: the same set minus the high half, because
|
|
167
287
|
// the low half is the return register (already excluded) and `lower` writes the high one itself
|
|
168
288
|
// from the callee's own result. The two arms of one rule: where the callee hands a register back,
|
|
@@ -180,9 +300,10 @@ export function callDeclarations(
|
|
|
180
300
|
);
|
|
181
301
|
}
|
|
182
302
|
const pairReturnClobbers = callClobbers.filter((r) => r !== target.argRegs[1]);
|
|
303
|
+
const heldAtUndeclaredCall = new Set<Value>();
|
|
183
304
|
const isFloatHelperName = (callee: string): boolean => {
|
|
184
305
|
const h = lookupHelper(target.runtimeHelpers, callee);
|
|
185
|
-
return h !== undefined && isFloatHelper(h);
|
|
306
|
+
return h !== undefined && isFloatHelper(h) && floatSidesOf(h).result;
|
|
186
307
|
};
|
|
187
308
|
/** the callee that handed back each pair a call returned, by the pair's value */
|
|
188
309
|
const pairCallee = new Map<Value, string>();
|
|
@@ -191,7 +312,7 @@ export function callDeclarations(
|
|
|
191
312
|
// The compiler's own runtime, off the TARGET (runtime-helpers.ts): which helpers a compiler
|
|
192
313
|
// emits is a compiler fact, and reading one table for every ISA is how a scan for `__*di3`
|
|
193
314
|
// reports zero on a compiler whose runtime spells them `__ll_*`.
|
|
194
|
-
const helperProtos = helperPrototypes(target
|
|
315
|
+
const helperProtos = helperPrototypes(target);
|
|
195
316
|
const wideHelper = (callee: string) => {
|
|
196
317
|
// Through the table's one reader (`lookupHelper`): a bare index answers with a member of
|
|
197
318
|
// `Object.prototype` for a callee named `toString`, which is truthy and has no `params` for
|
|
@@ -270,9 +391,11 @@ export function callDeclarations(
|
|
|
270
391
|
// WHAT THE CONVERSION CANNOT DO ON ITS OWN. `declaredWidth` answers for every type asmlift can
|
|
271
392
|
// spell, `long long` included, and `undefined` for a project typedef, a by-value struct or a
|
|
272
393
|
// floating type. `declaredCallArgs` sizes a `double` from the target
|
|
273
|
-
// (`TargetDescription.doubleArgWords`)
|
|
274
|
-
//
|
|
275
|
-
//
|
|
394
|
+
// (`TargetDescription.doubleArgWords`) and a typedef the declaration lists in `wordParams` (an
|
|
395
|
+
// enum, an array or a function type: `proto.ts` `paramClassOf`) at one word; one other such
|
|
396
|
+
// spelling and it states no layout at all, because the question here is not how wide that
|
|
397
|
+
// parameter is but whether it occupies one argument register or two, and the choice moves every
|
|
398
|
+
// later argument's home. So the declaration licenses no outgoing block and this returns `null`:
|
|
276
399
|
// the call is lifted at the arg-register guess, exactly as a callee the project never declared
|
|
277
400
|
// is, and the guess retracts the registers a call destroyed where a stated width would assert
|
|
278
401
|
// them.
|
|
@@ -286,6 +409,79 @@ export function callDeclarations(
|
|
|
286
409
|
// A frontend that places the widths before it lowers the call runs this over every call, so a
|
|
287
410
|
// refusal thrown here reaches the caller ahead of every other slot-model refusal, which is right
|
|
288
411
|
// because it is the most specific thing that was seen.
|
|
412
|
+
const known = (t: Prototypes, callee: string) => (Object.hasOwn(t, callee) ? t[callee] : undefined);
|
|
413
|
+
/** the declaration a callee is read at: the project's, then the runtime table's, then the C
|
|
414
|
+
* standard's (`proto.ts` `calleeDeclaration`; `declaredCall` gives the reasons for the order) */
|
|
415
|
+
const declarationOf = (callee: string): FnProto | undefined => calleeDeclaration(callee, prototypes, helperProtos);
|
|
416
|
+
const declaredParams = (callee: string): readonly DeclaredParam[] | null => {
|
|
417
|
+
const params = declarationOf(callee)?.params;
|
|
418
|
+
if (typeof params === 'number') {
|
|
419
|
+
return Array.from({ length: params }, () => ({ kind: 'word', spelling: '' }));
|
|
420
|
+
}
|
|
421
|
+
if (!Array.isArray(params)) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const declared = declarationOf(callee)!;
|
|
425
|
+
return params.map((spelling, k) => ({ kind: paramClassOf(declared, params, k), spelling }));
|
|
426
|
+
};
|
|
427
|
+
// A STRUCT OR UNION PASSED BY VALUE THAT NO RULE PLACES REFUSES; IT IS NOT GUESSED. The arity
|
|
428
|
+
// guess counts argument registers, and a struct is not one: it takes as many words as its size
|
|
429
|
+
// (agbcc, O32), starts at an even word when it is 8-aligned (O32), or travels as the address of
|
|
430
|
+
// a copy (mwcc). A guess over it drops the struct (agbcc `takeQ(k, gQ)` with no layout for
|
|
431
|
+
// `struct Q` guesses `takeQ();`) or reads its words as separate arguments (O32 `takeW(gW)` as
|
|
432
|
+
// `takeW(*(s32 *)&gW)`), and either is C that compiles. This is
|
|
433
|
+
// not the abstention `declaredCallArgs` documents for an unsized typedef, which may still be one
|
|
434
|
+
// word: a parameter the declaration names a struct or union is known not to be.
|
|
435
|
+
//
|
|
436
|
+
// A VARIADIC DECLARATION REFUSES THE SAME GUESS over a fixed parameter that is not one word: it
|
|
437
|
+
// states a layout only where each fixed parameter is one word (`variadicFloor`), and otherwise its
|
|
438
|
+
// call is placed by the guess (`FnProto.variadicParams`), which splits a struct, a 64-bit value or
|
|
439
|
+
// a `double` into words. Its parameters are the project's fact, read off its own entry,
|
|
440
|
+
// which `declarationOf` reads past because it states no `params`.
|
|
441
|
+
const refuseUnplacedByValue = (callee: string, hidden: number): void => {
|
|
442
|
+
const own = known(prototypes, callee);
|
|
443
|
+
const fixed = variadicRefusal(own);
|
|
444
|
+
if (fixed !== undefined) {
|
|
445
|
+
const spelling = own!.variadicParams![fixed.param];
|
|
446
|
+
const byValue = paramClassOf(own!, own!.variadicParams!, fixed.param) === 'by-value';
|
|
447
|
+
fail(
|
|
448
|
+
`cannot lift '${name}': \`${callee}\` takes '${spelling}'${byValue ? ' by value' : ''} as its parameter ` +
|
|
449
|
+
`${fixed.param + 1}, and ${fixed.refused} — a variadic call is lifted only where each fixed parameter is one argument word`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const p = declarationOf(callee);
|
|
453
|
+
const why = byValueRefusal(p, target, hidden);
|
|
454
|
+
if (why !== undefined) {
|
|
455
|
+
fail(
|
|
456
|
+
`cannot lift '${name}': \`${callee}\` takes '${(p!.params as readonly string[])[why.param]}' by value as its ` +
|
|
457
|
+
`parameter ${why.param + 1}, and ${why.refused} — a struct passed by value is lifted only where its argument words are known`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
// A FLOATING PARAMETER THE DECLARATION STATES BUT NOTHING LAYS OUT REFUSES; IT IS NOT GUESSED.
|
|
462
|
+
// Where the target has an FPU, `plan` refuses every floating parameter first (it travels in the
|
|
463
|
+
// FPU's registers). Where it has none — agbcc — a `double` is laid out in general words
|
|
464
|
+
// (`TargetDescription.doubleArgWords`) and a `float` or `long double` is not (`proto.ts`
|
|
465
|
+
// `declaredCallArgs`), so the call would fall to the arity guess, which is wrong both ways: a
|
|
466
|
+
// trailing argument passed through unchanged is dropped (`int g(int, float)`: `g(a + 1, b)` as
|
|
467
|
+
// `g(a0 + 1)`), and a register left over from the caller is counted (`float h(float)`: `h(v)` as
|
|
468
|
+
// `h(a1, a1)`). Neither compiles against the header. The unsized-typedef abstention does not
|
|
469
|
+
// apply: a floating spelling is known, and what is missing is a layout, not a width.
|
|
470
|
+
const refuseUnplacedFloat = (callee: string): void => {
|
|
471
|
+
const p = declarationOf(callee);
|
|
472
|
+
const params = p?.params;
|
|
473
|
+
if (target.fpu !== undefined || !Array.isArray(params)) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const at = params.findIndex((_, k) => paramClassOf(p!, params, k) === 'float');
|
|
477
|
+
if (at >= 0) {
|
|
478
|
+
fail(
|
|
479
|
+
`cannot lift '${name}': \`${callee}\` is declared to take '${params[at]}' as its parameter ${at + 1}, ` +
|
|
480
|
+
'which this target passes in general argument words that are not laid out here — the arity guess ' +
|
|
481
|
+
'in its place drops an argument passed through unchanged, or counts a leftover register as one',
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
289
485
|
const declaredCall = (callee: string): DeclaredCall | null => {
|
|
290
486
|
// Three tiers, narrowing: the project's own headers, then the compiler's runtime helpers,
|
|
291
487
|
// then the signatures the C standard fixes (proto.ts). A project that re-declares one of the
|
|
@@ -299,12 +495,17 @@ export function callDeclarations(
|
|
|
299
495
|
//
|
|
300
496
|
// `Object.hasOwn` on all three tables: a callee named `toString` or `valueOf` would otherwise
|
|
301
497
|
// read a `Function` off `Object.prototype` as its prototype entry.
|
|
302
|
-
const
|
|
303
|
-
const own = known(prototypes);
|
|
498
|
+
const own = known(prototypes, callee);
|
|
304
499
|
const returned = declaresAggregateReturn(own) ? structReturnOf(name, target, callee, own!, fail) : undefined;
|
|
305
|
-
const
|
|
306
|
-
const declared
|
|
500
|
+
const hidden = returned === undefined || returned === 'register' ? 0 : 1;
|
|
501
|
+
const declared:
|
|
502
|
+
{ widths: readonly number[]; doubles: ReadonlySet<number>; byValue?: readonly ByValueArg[] } | undefined =
|
|
503
|
+
declaredCallArgs(declarationOf(callee), target) ?? byValueCallArgs(declarationOf(callee), target, hidden);
|
|
307
504
|
const params = declared?.widths;
|
|
505
|
+
if (params === undefined) {
|
|
506
|
+
refuseUnplacedByValue(callee, hidden);
|
|
507
|
+
refuseUnplacedFloat(callee);
|
|
508
|
+
}
|
|
308
509
|
if (params === undefined && returned !== undefined && returned !== 'register') {
|
|
309
510
|
// a guessed arity reads argument registers from the first, which holds the hidden pointer
|
|
310
511
|
fail(
|
|
@@ -320,7 +521,12 @@ export function callDeclarations(
|
|
|
320
521
|
// (`double *`, `size_t`, or a bare parameter count, which states no type) lifts to a candidate
|
|
321
522
|
// that does not compile. Asked of the printer's own predicate, so the two cannot disagree; a
|
|
322
523
|
// headers world, which would declare the callee itself, loses the lift with it.
|
|
323
|
-
|
|
524
|
+
const byValueParams = new Set((declared?.byValue ?? []).map((b) => b.param));
|
|
525
|
+
if (
|
|
526
|
+
returned !== undefined &&
|
|
527
|
+
returned !== 'register' &&
|
|
528
|
+
spellableProto(own, target, returned.type, byValueParams) === undefined
|
|
529
|
+
) {
|
|
324
530
|
const via = `\`${callee}\` returns ${returned.type.kind === 'struct' ? returned.type.declared : typeToString(returned.type)} through a hidden pointer in ${target.argRegs[0]}`;
|
|
325
531
|
if (typeof own!.params === 'number') {
|
|
326
532
|
fail(
|
|
@@ -331,14 +537,27 @@ export function callDeclarations(
|
|
|
331
537
|
`cannot lift '${name}': ${via}, and a parameter type of its declaration has no spelling the lifted source can declare it with`,
|
|
332
538
|
);
|
|
333
539
|
}
|
|
334
|
-
const hidden = returned === undefined || returned === 'register' ? 0 : 1;
|
|
335
540
|
const widths = hidden === 0 ? params : [32, ...params];
|
|
336
541
|
const doubles = new Set([...declared!.doubles].map((i) => i + hidden));
|
|
542
|
+
const byValue = (declared!.byValue ?? []).map((b) => ({
|
|
543
|
+
...b,
|
|
544
|
+
word: b.word + hidden,
|
|
545
|
+
}));
|
|
546
|
+
// a struct partly in the registers and partly on the stack (FUNCTION_ARG_PARTIAL_NREGS) is
|
|
547
|
+
// one value read from two places, which no lowering here builds
|
|
548
|
+
const past = byValue.find((b) => wordsOf(widths.slice(0, b.word), target) + b.words > target.argRegs.length);
|
|
549
|
+
if (past !== undefined) {
|
|
550
|
+
fail(
|
|
551
|
+
`cannot lift '${name}': \`${callee}\` takes ${structSpelling(past.type)} by value as its parameter ${past.param + 1}, ` +
|
|
552
|
+
'and it does not fit in the argument registers — a struct passed on the stack is not modelled',
|
|
553
|
+
);
|
|
554
|
+
}
|
|
337
555
|
return {
|
|
338
556
|
widths,
|
|
339
557
|
doubles,
|
|
340
|
-
params:
|
|
558
|
+
params: declared!.widths.length - byValue.reduce((n, b) => n + b.words - 1, 0),
|
|
341
559
|
...(returned === undefined ? {} : { returned }),
|
|
560
|
+
...(byValue.length === 0 ? {} : { byValue }),
|
|
342
561
|
};
|
|
343
562
|
};
|
|
344
563
|
// ONE CALL, IN THE ORDER ITS QUESTIONS REFUSE: a struct the lowering cannot receive and a float,
|
|
@@ -388,14 +607,21 @@ export function callDeclarations(
|
|
|
388
607
|
// register as its own value would honour the declaration by handing the callee one half.
|
|
389
608
|
const wideAt = declared.widths.findIndex((w) => w > 32);
|
|
390
609
|
if (wideAt >= 0) {
|
|
610
|
+
// `widths` holds argument words — a hidden struct-return pointer ahead of the declared
|
|
611
|
+
// list, and each word of a struct passed by value — and the text names the parameter
|
|
612
|
+
const hidden = declared.returned !== undefined && declared.returned !== 'register' ? 1 : 0;
|
|
613
|
+
const param =
|
|
614
|
+
wideAt -
|
|
615
|
+
hidden -
|
|
616
|
+
(declared.byValue ?? []).filter((b) => b.word < wideAt).reduce((n, b) => n + b.words - 1, 0);
|
|
391
617
|
fail(
|
|
392
618
|
`cannot lift '${name}': one half of a 64-bit value would be handed to '${callee}' — its parameter ` +
|
|
393
|
-
`${
|
|
619
|
+
`${param + 1} is declared wider than a register, and this frontend passes each argument ` +
|
|
394
620
|
'register as its own value rather than building the pair the ABI passes it in',
|
|
395
621
|
);
|
|
396
622
|
}
|
|
397
623
|
}
|
|
398
|
-
if (!lowering.stackArgs && declared !== null && wordsOf(declared.widths) > target.argRegs.length) {
|
|
624
|
+
if (!lowering.stackArgs && declared !== null && wordsOf(declared.widths, target) > target.argRegs.length) {
|
|
399
625
|
fail(
|
|
400
626
|
`cannot lift '${name}': outgoing stack arguments not modelled — '${callee}' is declared with ` +
|
|
401
627
|
`${declared.widths.length} parameters and the argument registers carry ${target.argRegs.length}, so the rest ` +
|
|
@@ -424,9 +650,22 @@ export function callDeclarations(
|
|
|
424
650
|
// a runtime helper's return is its table's, whatever a project declares on its name
|
|
425
651
|
const declaredVoid = !isRuntimeHelperName(callee) && declaresVoidReturn(own);
|
|
426
652
|
const voided = lowering.voidReturn && declaredVoid;
|
|
653
|
+
const image = returned === 'register' && registerStructLocal(known(prototypes, callee), target) !== undefined;
|
|
654
|
+
// A FLOAT RETURNED WITHOUT AN FPU IS BITS IN THE RETURN REGISTER. The candidate prints the call
|
|
655
|
+
// against the declaration, so `v = f()` into anything but a float converts the value (agbcc:
|
|
656
|
+
// `bl f; bl __fixsfsi`), where the bytes moved the bits. The call names the type, and
|
|
657
|
+
// `raise/floathelpers.ts` reads the result as that float or refuses. A target with an FPU
|
|
658
|
+
// refused the call above; a runtime helper's return is its table's.
|
|
659
|
+
const returnSpelling = own?.returns ?? declarationOf(callee)?.returns;
|
|
660
|
+
const floatReturn =
|
|
661
|
+
target.fpu === undefined && !isRuntimeHelperName(callee) && !voided && typeof returnSpelling === 'string'
|
|
662
|
+
? floatingSpelling(returnSpelling)
|
|
663
|
+
: undefined;
|
|
427
664
|
return {
|
|
665
|
+
...(floatReturn === undefined ? {} : { floatReturn }),
|
|
428
666
|
widths: wide?.params ?? declared?.widths ?? null,
|
|
429
667
|
doubles: declared?.doubles ?? new Set(),
|
|
668
|
+
...(declared?.byValue === undefined ? {} : { byValue: declared.byValue }),
|
|
430
669
|
returns: voided
|
|
431
670
|
? { kind: 'void' }
|
|
432
671
|
: pair
|
|
@@ -434,22 +673,227 @@ export function callDeclarations(
|
|
|
434
673
|
: returned === undefined
|
|
435
674
|
? { kind: 'word' }
|
|
436
675
|
: returned === 'register'
|
|
437
|
-
? { kind: 'register-struct' }
|
|
676
|
+
? { kind: 'register-struct', image }
|
|
438
677
|
: { kind: 'memory-struct', type: returned.type },
|
|
439
|
-
// a struct leaves the return register holding nothing the caller may read as a value,
|
|
440
|
-
// void callee leaves it holding whatever the
|
|
678
|
+
// a struct leaves the return register holding nothing the caller may read as a value, save
|
|
679
|
+
// the image of one a local can be read off, and a void callee leaves it holding whatever the
|
|
680
|
+
// callee did
|
|
441
681
|
clobbers: pair
|
|
442
682
|
? pairReturnClobbers
|
|
443
|
-
: returned === undefined && !voided
|
|
683
|
+
: (returned === undefined && !voided) || image
|
|
444
684
|
? callClobbers
|
|
445
685
|
: [...callClobbers, target.returnReg],
|
|
446
686
|
declaredVoid,
|
|
447
687
|
};
|
|
448
688
|
};
|
|
689
|
+
// A VARIADIC DECLARATION WHOSE FIXED PARAMETERS ARE EACH ONE WORD STATES ITS FIRST ARGUMENTS:
|
|
690
|
+
// they are the first that many argument words, exactly, so they are read as stated words and only
|
|
691
|
+
// what the `...` takes is guessed, above them (`fallbackArgc`'s and the trim's `floor`). Read the
|
|
692
|
+
// way a call through argument register rN reads the registers below N (`lower`): one this
|
|
693
|
+
// function never wrote is its own argument passed on, and one a call destroyed names nothing and
|
|
694
|
+
// refuses (`SsaBuilder.finish`). So `int logv(const char *, int, ...)` lifts `logv(s + 1, c)` as
|
|
695
|
+
// `logv(a0 + 1, a1)`, where a guess from r0 sees r1 untouched and refuses the gap. A fixed list
|
|
696
|
+
// holding an UNSIZED spelling states no such words — the one may be two — and gets no floor; a
|
|
697
|
+
// fixed parameter known not to be one word refuses (`refuseUnplacedByValue`, in `declaredCall`).
|
|
698
|
+
const variadicFloor = (callee: string | undefined): number => {
|
|
699
|
+
const own = callee === undefined ? undefined : known(prototypes, callee);
|
|
700
|
+
const fixed = own?.params === undefined ? own?.variadicParams : undefined;
|
|
701
|
+
return fixed !== undefined && fixed.every((_, k) => paramClassOf(own!, fixed, k) === 'word') ? fixed.length : 0;
|
|
702
|
+
};
|
|
703
|
+
// A GUESS SHORTER THAN THE DECLARATION REFUSES. A declaration the lowering cannot lay out (an
|
|
704
|
+
// UNSIZED spelling: `proto.ts` `declaredCallArgs`), or a variadic one whose fixed parameters
|
|
705
|
+
// include one, still states how many parameters it has — fixed ones, for a variadic — and every
|
|
706
|
+
// one of them is at least one argument word. A guess that counts fewer registers has dropped
|
|
707
|
+
// arguments: the last one passed through unchanged leaves its register with no definition, so
|
|
708
|
+
// `void h(int, Foo)` with `Foo` unresolved lifts `h(n + 1, v)` as `h(a0 + 1)`. The count bounds
|
|
709
|
+
// the guess from below only — an unsized parameter may be two words, or a struct of several. A
|
|
710
|
+
// variadic call with a `variadicFloor` never reaches the bound: its guess starts there.
|
|
711
|
+
//
|
|
712
|
+
// KNOWN GAP: this bounds the count `fallbackArgc` takes, not what `SsaBuilder.finish` later
|
|
713
|
+
// retracts from it (a register a call between the setup and this one destroyed).
|
|
714
|
+
const refuseShortOfDeclared = (callee: string | undefined, argc: number): void => {
|
|
715
|
+
if (callee === undefined) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const p = declarationOf(callee);
|
|
719
|
+
const own = known(prototypes, callee);
|
|
720
|
+
const stated = Array.isArray(p?.params)
|
|
721
|
+
? { n: p.params.length, what: 'parameters' }
|
|
722
|
+
: own?.variadicParams !== undefined && own.params === undefined
|
|
723
|
+
? { n: own.variadicParams.length, what: 'fixed parameters before its `...`' }
|
|
724
|
+
: undefined;
|
|
725
|
+
if (stated !== undefined && argc < stated.n) {
|
|
726
|
+
fail(
|
|
727
|
+
`cannot lift '${name}': \`${callee}\` is declared with ${stated.n} ${stated.what}, which no layout here ` +
|
|
728
|
+
`places, and the arity guess in its place finds ${argc} argument register${argc === 1 ? '' : 's'} set up — ` +
|
|
729
|
+
'an argument passed through unchanged leaves its register with nothing the guess can see',
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
// A GAP IN A GUESSED ARITY IS DECIDED WHERE THE REGISTER ABOVE IT IS SURELY AN ARGUMENT
|
|
734
|
+
// (`fallbackArgc`'s `decide`). The gap refuses because an argument register left at its incoming
|
|
735
|
+
// value and one the call does not pass look the same — but only while nothing says the call takes
|
|
736
|
+
// an argument above it. A register the call's own block wrote with a value that no memory read
|
|
737
|
+
// produced and that nothing but the call reads says exactly that: an optimising compiler deletes a
|
|
738
|
+
// dead computation, so the value exists only to be passed. And where argument words fill in order,
|
|
739
|
+
// every word below an argument is an argument too, so the gap's registers are passed on unchanged.
|
|
740
|
+
// Measured on 2026-10-18 at every toolchain's canonical flags: `logv(s, 1)` sets r1/a1/r4 alone,
|
|
741
|
+
// and at -O0 no gap arises at all (every argument is reloaded from its home first).
|
|
742
|
+
//
|
|
743
|
+
// EACH CLAUSE IS A MEASURED COUNTEREXAMPLE, so each refuses:
|
|
744
|
+
// - the value must be the call's own block's. Where a discarded read may land in an argument
|
|
745
|
+
// register (`compilerBehaviors.discardedReadMayFillArgRegister`), it must also be something no
|
|
746
|
+
// memory read produced: agbcc puts a DISCARDED volatile read in the lowest free argument
|
|
747
|
+
// register (`*(volatile int *)K; g(s)` is `ldr r1,[r1]; bl g`, at -O0 too), the passgap shape
|
|
748
|
+
// exactly. gcc2.7.2kmc, gcc2.7.2 and the three mwcc builds do the same once their usual
|
|
749
|
+
// discard register (v0/v1, r0) is busy: `int a = ga * 3; *(volatile int *)&gx; ...; logv(s)`
|
|
750
|
+
// is `lw a1,0(v0); ... jal logv` on KMC and `lwz r4,gx` on mwcc, the bytes of the twin that
|
|
751
|
+
// passes the read. Only ido7.1 discards into $zero every time, so only there a load only the
|
|
752
|
+
// call reads is an argument: `logv(s, gx)` lifts. Nothing else may read the value either
|
|
753
|
+
// (`SsaBuilder.requireSoleReader`, asked of the finished function): `gx = 1; logv(s)` and
|
|
754
|
+
// `gx = 1; logv(s, 1)` are one agbcc object, the stored constant left in r1.
|
|
755
|
+
// - the value must be no instruction's side effect (`SsaBuilder.noteIncidentalWrite`): a base
|
|
756
|
+
// writeback is left in its register whether anything wants it or not. agbcc copies a struct of
|
|
757
|
+
// 3+ words as `ldmia r3!, {r4, r5, r6}; stmia r2!, {r4, r5, r6}` and leaves both writebacks
|
|
758
|
+
// dead, so `*p = *q; return logv(s, n) + 1;` would lift `logv(a0, a1, a2 + 3, a3 + 3)`.
|
|
759
|
+
// - where a caller forwards its own destination as a callee's hidden pointer
|
|
760
|
+
// (`compilerBehaviors.forwardsStructReturnDestination`, ido7.1), a gap at the first argument
|
|
761
|
+
// register is decided only where the call's result is read (`SsaBuilder.requireReader`,
|
|
762
|
+
// owed by `lower`), by anything but a `ret`. `int b3(struct S *p){ *p = sg(1); return 0; }` is
|
|
763
|
+
// `jal sg; li a1,1` with a0 untouched, and would lift `sg(a0, 1)` even with `int b3(struct S *)`
|
|
764
|
+
// declared: the hidden pointer stated as a C argument, and the `*p =` store dropped. IDO reads
|
|
765
|
+
// no forwarded struct return's result register (five shapes, at the target.ts field), but a
|
|
766
|
+
// `ret` reads it in a function ending in the call, `void b7(struct S *p){ *p = sg(1); }`.
|
|
767
|
+
// - each gap register must hold the function's own incoming value on every path
|
|
768
|
+
// (`holdsEntryValue`): one a call destroyed is the trim's, and holds nothing.
|
|
769
|
+
// - where the ABI can leave a register empty below a filled one (target.ts `argRegisterHoles`)
|
|
770
|
+
// the gap may be that hole. An argument aligned to 8 skips an odd word (O32 a1, PowerPC
|
|
771
|
+
// r4/r6/r8), and the hole is ruled out only where that argument's second word holds nothing:
|
|
772
|
+
// no value reaches it and the own declaration fills fewer words. A leading float shadows the
|
|
773
|
+
// first general words on O32. It is ruled out only where the own declaration surely puts an
|
|
774
|
+
// integer word in each gap register. No register value can rule it out: the MIPS frontend
|
|
775
|
+
// refuses every instruction naming the FPU's file in a function that calls
|
|
776
|
+
// (frontend/mips.ts), so a float here is one passed on untouched in $f12, which nothing
|
|
777
|
+
// reads. A gap that starts at a1 under an a0 that only a read set up (`gx = a; gp(d, 5)`) is
|
|
778
|
+
// the 8-byte skip's case and refuses through it: a1 is odd, so it needs the own declaration's
|
|
779
|
+
// word bound, and `ownLeadingWordParamsOn` stops at the double.
|
|
780
|
+
// - where the own declaration binds the body, each gap register must surely hold one of its
|
|
781
|
+
// integer word parameters (`ownLeadingWordParamsOn`): no hidden struct pointer, no float.
|
|
782
|
+
// - at flags that keep a read nothing uses (`compilerBehaviors.keepsUnusedReads`, the -O0
|
|
783
|
+
// levels) the premise is suspended, as frontend/ppc.ts `computedPairHalf` suspends it. mwcc -O0
|
|
784
|
+
// keeps a dead `li r31,5`. The gap was measured never to arise there (mwcc -O0,p on all three
|
|
785
|
+
// builds, IDO -O1 and agbcc -O0 reload every argument from its home first), so this costs no
|
|
786
|
+
// lift.
|
|
787
|
+
// What the decision leaves open above the proven register is the call's own check
|
|
788
|
+
// (`decidedCallRefusal`). The proven register's value is returned, for the sole-reader
|
|
789
|
+
// obligation the call then takes on.
|
|
790
|
+
const decideGap = (
|
|
791
|
+
ssa: SsaBuilder,
|
|
792
|
+
bi: number,
|
|
793
|
+
gap: readonly string[],
|
|
794
|
+
set: string,
|
|
795
|
+
accept: (v: Value) => boolean,
|
|
796
|
+
): Value | undefined => {
|
|
797
|
+
const regs = target.argRegs;
|
|
798
|
+
if (target.compilerBehaviors.keepsUnusedReads === true || !ssa.writtenSinceCall(set, bi)) {
|
|
799
|
+
return undefined;
|
|
800
|
+
}
|
|
801
|
+
// a lookup, not the frontend's read: `leftover` asks what instructions read (`readAfterEveryReachingWrite`)
|
|
802
|
+
const v = ssa.readGuessedArg(set, bi);
|
|
803
|
+
const def = ssa.irBlocks[bi].ops.find((op) => op.results.includes(v));
|
|
804
|
+
const sig = def === undefined ? undefined : (OPCODES[def.opcode as keyof typeof OPCODES] as OpSig | undefined);
|
|
805
|
+
const readsRefuse = target.compilerBehaviors.discardedReadMayFillArgRegister !== false;
|
|
806
|
+
if (
|
|
807
|
+
!accept(v) ||
|
|
808
|
+
ssa.isIncidentalWrite(v) ||
|
|
809
|
+
sig === undefined ||
|
|
810
|
+
sig.effects ||
|
|
811
|
+
(sig.reads && readsRefuse) ||
|
|
812
|
+
sig.terminator ||
|
|
813
|
+
def!.opcode === 'undef'
|
|
814
|
+
) {
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
if (gap.some((g) => ssa.writtenSinceCall(g, bi) || !ssa.holdsEntryValue(g, bi))) {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
const own = ownDeclaration();
|
|
821
|
+
const surelyOwn = own === undefined ? undefined : ownLeadingWordParamsOn(name, own.proto, target);
|
|
822
|
+
if (surelyOwn !== undefined && gap.some((g) => regs.indexOf(g) >= surelyOwn)) {
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
const holes = argRegisterHoles(target);
|
|
826
|
+
if (holes.leadingFloatShadow && regs.indexOf(gap[0]) === 0 && own?.proves !== true) {
|
|
827
|
+
return undefined;
|
|
828
|
+
}
|
|
829
|
+
const ownWords = own?.proves !== true ? undefined : ownArgumentWordsOn(name, own.proto, target);
|
|
830
|
+
for (const g of gap.filter((r) => holes.skippable.has(r))) {
|
|
831
|
+
const j = regs.indexOf(g);
|
|
832
|
+
const second = regs[j + 2];
|
|
833
|
+
if (
|
|
834
|
+
regs[j + 1] !== set ||
|
|
835
|
+
second === undefined ||
|
|
836
|
+
ssa.hasReachingDef(second, bi, accept) ||
|
|
837
|
+
ownWords === undefined ||
|
|
838
|
+
j + 2 < ownWords
|
|
839
|
+
) {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return v;
|
|
844
|
+
};
|
|
845
|
+
// WHAT A DECIDED GAP LEAVES OPEN ABOVE THE COUNT: whether the call takes more than the `argc`
|
|
846
|
+
// words counted. The decision proves that the gap registers are this function's own arguments
|
|
847
|
+
// passed on. Past the count, the guess reads an unwritten register as no argument, as it does at
|
|
848
|
+
// every gapless call, and three things say otherwise. Each refuses (returns true):
|
|
849
|
+
// - an own parameter that may sit past the count (`ownArgumentWordsOn`, an upper bound). It is
|
|
850
|
+
// just as likely passed on as the gap below it was. On gcc2.7.2kmc and ido7.1, `int ll6(int a,
|
|
851
|
+
// int b, int c, int d){ return g(a, 2, c, d, 5) + 1; }` leaves c and d in a2/a3 and would lift
|
|
852
|
+
// `g(a0, 2)`.
|
|
853
|
+
// - a word of the outgoing area that reaches the call (`guess.refuse`, asked as if the count
|
|
854
|
+
// filled every argument register). It is a fifth argument, so every register below it is an
|
|
855
|
+
// argument too: the same `ll6`'s `sw v0,16(sp)`.
|
|
856
|
+
// - a floating argument this ABI passes apart from the general words (`fpu.slots: 'separate'`,
|
|
857
|
+
// PowerPC), which no guess counts. `int fl2(int b, float x){ return g(b, x, 3) + 1; }` leaves
|
|
858
|
+
// x in f1 on all three mwcc builds and would lift `g(a0, 3)`. A float in f1 that nothing reads is
|
|
859
|
+
// invisible, so on such an ABI only an own declaration whose parameters are all integers rules
|
|
860
|
+
// it out. Without a declaration this refuses. A value reaching an FPU argument register refuses
|
|
861
|
+
// on every ABI.
|
|
862
|
+
const decidedCallRefusal = (ssa: SsaBuilder, bi: number, argc: number, site: CallSite): boolean => {
|
|
863
|
+
const own = ownDeclaration();
|
|
864
|
+
if (own !== undefined) {
|
|
865
|
+
const words = ownArgumentWordsOn(name, own.proto, target);
|
|
866
|
+
if (words === undefined || words > argc) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const fpu = target.fpu;
|
|
871
|
+
if (fpu !== undefined) {
|
|
872
|
+
if (fpu.argRegs.some((r) => ssa.hasReachingDef(r, bi, (v) => !site.placeholders?.has(v)))) {
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
const floats = own === undefined ? undefined : ownDeclaresFloatingParam(name, own.proto);
|
|
876
|
+
if (floats === true || (fpu.slots === 'separate' && !(own?.proves === true && floats === false))) {
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
site.guess.refuse?.(target.argRegs.length);
|
|
882
|
+
} catch (e) {
|
|
883
|
+
if (e instanceof FrontendUnsupportedError) {
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
throw e;
|
|
887
|
+
}
|
|
888
|
+
return false;
|
|
889
|
+
};
|
|
449
890
|
// THE CALL A PLAN LOWERS TO, in one order on every ISA: the arity, each argument word, the
|
|
450
891
|
// `call` op, the guessed arity's record, the result and the clobbers.
|
|
451
892
|
const lower = (site: CallSite): void => {
|
|
452
|
-
const { ssa, bi, pairs, guess,
|
|
893
|
+
const { ssa, bi, pairs, guess, placeholders } = site;
|
|
894
|
+
// whether `v`, reaching argument register `r`, may be an argument the caller set up
|
|
895
|
+
const mayPass = (v: Value, r: string) => !placeholders?.has(v) && site.parked?.(v, r) !== true;
|
|
896
|
+
const holes = argRegisterHoles(target);
|
|
453
897
|
if (lowering.pairs !== (pairs !== undefined) || lowering.stackArgs !== (site.stackWord !== undefined)) {
|
|
454
898
|
throw new Error(`target '${target.id}': a call site's pairs and stack words must be what its lowering states`);
|
|
455
899
|
}
|
|
@@ -486,16 +930,274 @@ export function callDeclarations(
|
|
|
486
930
|
// A DECLARATION HOLDING A SPELLING NOTHING CAN SIZE STATES NO LAYOUT (`proto.ts`
|
|
487
931
|
// `declaredCallArgs`), so `widths` is null for it and this falls to the guess below —
|
|
488
932
|
// the same answer the callee would get with no prototype at all.
|
|
489
|
-
|
|
933
|
+
// the leading arguments a variadic declaration states, where the rest is guessed
|
|
934
|
+
const floor = p.widths === null ? variadicFloor(callee) : 0;
|
|
935
|
+
// WHAT THE CALLEE'S OWN BODY READS, where it is in the input and nothing lays the call out
|
|
936
|
+
// (frontend/callee-body.ts): at most `bodyWords()` argument words. It answers only a refusal
|
|
937
|
+
// about registers at or past it — the gap rule's (`fallbackArgc`'s `bound`) and the dispatch
|
|
938
|
+
// pass-through gate's — since whether those are arguments changes nothing the callee computes.
|
|
939
|
+
// It never caps the count: it bounds what the callee READS, and a source call passes more
|
|
940
|
+
// whenever a parameter goes unused or a K&R call hands extra arguments, so a register the caller
|
|
941
|
+
// set up for the call stays an argument (`tick(5)` to a body that reads none, `rd1(a, b, 7)` to
|
|
942
|
+
// one that reads r0, whose 7 also proves r0 is the caller's own a passed on). Asked lazily, as a
|
|
943
|
+
// refusal would fire, so a call no gate questions lifts no body. Only a callee whose entry
|
|
944
|
+
// states no parameter list is asked: a list nothing here can size, or a variadic one, still
|
|
945
|
+
// states parameters the call must be handed (`refuseShortOfDeclared`).
|
|
946
|
+
const stated = callee === undefined ? undefined : known(prototypes, callee);
|
|
947
|
+
const unstated = callee !== undefined && p.widths === null && asksCalleeBody(stated);
|
|
948
|
+
let bodyMemo: { words: number | undefined } | undefined;
|
|
949
|
+
const bodyWords = (): number | undefined =>
|
|
950
|
+
unstated ? (bodyMemo ??= { words: calleeBodyBound?.(callee!) }).words : undefined;
|
|
951
|
+
// whether an op lowered so far reads `v`: the call being lowered is not one yet
|
|
952
|
+
const reads = (o: Op, v: Value): boolean => o.operands.includes(v) || o.successors.some((e) => e.args.includes(v));
|
|
953
|
+
const readSoFar = (v: Value): boolean => ssa.irBlocks.some((b) => b.ops.some((o) => reads(o, v)));
|
|
954
|
+
// the function's own incoming value of argument register `r`
|
|
955
|
+
const incoming = (v: Value, r: string): boolean => ssa.paramReg.get(v) === r && ssa.irBlocks[0].params.includes(v);
|
|
956
|
+
// WHETHER WHAT ARGUMENT REGISTER `r` HOLDS WAS LEFT THERE BY A WRITE MADE FOR SOMETHING ELSE: a
|
|
957
|
+
// question about the register's last WRITE, not about its value. `mov r0, #5; strh r0, [r1];
|
|
958
|
+
// bl rnd` left the store's 5 in r0; `add r1, r5, #0; bl un` is a copy made for the call, though
|
|
959
|
+
// an earlier call read its value (`g4(a, b, c, d); un(a, b, c, d)`: agbcc and mwcc copy them back
|
|
960
|
+
// from callee-saved homes). A copy names no new value, so no value-level question tells them apart.
|
|
961
|
+
//
|
|
962
|
+
// WHAT READ COUNTS DEPENDS ON WHERE THE WRITE WAS, because a read that lowers to nothing — a copy
|
|
963
|
+
// into a callee-saved register, a store to a frame home — SAVES a value across the call, and a
|
|
964
|
+
// saved value may be handed to the call as well:
|
|
965
|
+
// - the call's own block wrote it, or nothing in this function did (the incoming value): only a
|
|
966
|
+
// lowered op's read since. `un(a); return g2(a);` and `un(); return g2(a);` compile to the
|
|
967
|
+
// same caller bytes on agbcc, IDO, gcc 2.7.2, KMC and mwcc (agbcc `add r4, r0, #0; bl un`).
|
|
968
|
+
// - another block wrote it: a lowered op's read in the call's block, or a read after EACH write
|
|
969
|
+
// that reaches the call, a save included, on ANY path out of it but the ones into the call
|
|
970
|
+
// (`SsaBuilder.readAfterEveryReachingWrite`). agbcc's case CSE moves the cases' `i << 3` from
|
|
971
|
+
// r0 to r4 at their join (`add r4, r0, #0`) before the call's join, while a value handed to
|
|
972
|
+
// the call is copied back for it in the call's block (`add r0, r4, #0; bl rnd`, measured).
|
|
973
|
+
// EACH write, because one no read follows was made for the call (`if (c) { t = d + 3; gX = t;
|
|
974
|
+
// } else t = d - 9; rnd(t);` at IDO stores one arm's a0). ANY path, because gcc 2.7.2 and KMC
|
|
975
|
+
// fill a branch's delay slot with one call's argument (`bnez v0, L; move a0, zero; jal cr`),
|
|
976
|
+
// and the 0 reaches `L: jal stat` unread. NOT the call's own block's saves: IDO saves the
|
|
977
|
+
// argument it hands a call in the jal's delay slot (`jal rnd; sw a0, 28(sp)`, measured) and
|
|
978
|
+
// computes t into v1 where rnd is handed nothing, so that save is evidence FOR the argument.
|
|
979
|
+
// An earlier call's result is no such write (sa3 Task_274_8056314: `strb r0` of one arm's
|
|
980
|
+
// value, `bl sub_8056408` in the other, then `bl Task_274_8054E38`, whose source takes nothing).
|
|
981
|
+
const leftover = (r: string): boolean => {
|
|
982
|
+
const at = ssa.lastWriteOp(r, bi);
|
|
983
|
+
if (at !== undefined) {
|
|
984
|
+
const since = ssa.irBlocks[bi].ops.slice(at);
|
|
985
|
+
return ssa.holdsValue(r, bi, (v) => mayPass(v, r) && since.some((o) => reads(o, v)));
|
|
986
|
+
}
|
|
987
|
+
if (ssa.holdsValue(r, bi, (v) => mayPass(v, r) && ssa.irBlocks[bi].ops.some((o) => reads(o, v)))) {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
const callResult = (v: Value): boolean =>
|
|
991
|
+
ssa.irBlocks.some((b) => b.ops.some((o) => o.opcode === 'call' && o.results.includes(v)));
|
|
992
|
+
// the values a join's phi of `v` merges, through phis of phis, or `undefined` where an edge
|
|
993
|
+
// into one is not lowered yet
|
|
994
|
+
const merged = (v: Value, seen = new Set<Value>()): Value[] | undefined => {
|
|
995
|
+
const at = ssa.irBlocks.find((b) => b.params.includes(v));
|
|
996
|
+
if (at === undefined) {
|
|
997
|
+
return [v];
|
|
998
|
+
}
|
|
999
|
+
if (seen.has(v)) {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
seen.add(v);
|
|
1003
|
+
const k = at.params.indexOf(v);
|
|
1004
|
+
const edges = ssa.irBlocks.flatMap((b) => b.ops.flatMap((o) => o.successors.filter((e) => e.block === at)));
|
|
1005
|
+
const out: Value[] = [];
|
|
1006
|
+
for (const e of edges) {
|
|
1007
|
+
const sub = e.args[k] === undefined ? undefined : merged(e.args[k], seen);
|
|
1008
|
+
if (sub === undefined) {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
out.push(...sub);
|
|
1012
|
+
}
|
|
1013
|
+
return edges.length > 0 ? out : undefined;
|
|
1014
|
+
};
|
|
1015
|
+
// an earlier call's result on every path in: a join's phi of copies of one (IDO's `move a3,
|
|
1016
|
+
// v0` in a branch's delay slot meets the path that skips the store between)
|
|
1017
|
+
const resultOnEveryPath = (v: Value): boolean => {
|
|
1018
|
+
const vs = merged(v);
|
|
1019
|
+
return vs !== undefined && vs.length > 0 && vs.every(callResult);
|
|
1020
|
+
};
|
|
1021
|
+
return (
|
|
1022
|
+
ssa.readAfterEveryReachingWrite(r, bi, {
|
|
1023
|
+
// an earlier call's result where that call left it is no write this function made, for this
|
|
1024
|
+
// call or another
|
|
1025
|
+
passes: (v) => mayPass(v, r) && !(r === target.returnReg && callResult(v)),
|
|
1026
|
+
accept: (v) => mayPass(v, r) && (!incoming(v, r) || readSoFar(v)),
|
|
1027
|
+
// a path into a block not lowered yet: what its value answers, as the base question did
|
|
1028
|
+
unlowered: (v) => readSoFar(v),
|
|
1029
|
+
// a copy of an earlier call's result into another register, or a reload of one, answers
|
|
1030
|
+
// only where read after: `bl gets; add r1, r0, #0` and `jal gets; move a1, v0` were, by the
|
|
1031
|
+
// loop's loads, while IDO's `lw a3, 28(sp)` of a result saved across a call (af
|
|
1032
|
+
// mCPk_PakPrivateLoad) was not, and says nothing
|
|
1033
|
+
answersIfRead: (v) => resultOnEveryPath(v),
|
|
1034
|
+
}) ??
|
|
1035
|
+
// nothing written here left to judge: what reaches answers by its value, the base question,
|
|
1036
|
+
// a join's phi by the values it merges (af mCPk_PakPrivateLoad compares the result it keeps)
|
|
1037
|
+
ssa.holdsValue(r, bi, (v) => mayPass(v, r) && (readSoFar(v) || (merged(v) ?? []).some(readSoFar)))
|
|
1038
|
+
);
|
|
1039
|
+
};
|
|
1040
|
+
const unreadByBody = (r: string): boolean => {
|
|
1041
|
+
const words = bodyWords();
|
|
1042
|
+
return words !== undefined && target.argRegs.indexOf(r) >= words;
|
|
1043
|
+
};
|
|
1044
|
+
// A CALL THE BODY BOUND READS AT EVERY INCOMING VALUE IT MAY BE HANDED (`entryReadCalls`): up to
|
|
1045
|
+
// the highest argument register that some path into the call leaves unwritten since the entry.
|
|
1046
|
+
// A register a call destroyed on every path holds nothing of this function's arguments, so a
|
|
1047
|
+
// read of it could add nothing to what the body reads of them, and it is not taken: taken, a
|
|
1048
|
+
// callee making two calls that nothing declares would refuse its own lift, and lose its bound.
|
|
1049
|
+
const entryWords =
|
|
1050
|
+
unstated && entryReadCalls?.has(callee!) === true
|
|
1051
|
+
? target.argRegs.reduce((n, r, k) => (ssa.writtenOnEveryPath(r, bi, () => false) ? n : k + 1), 0)
|
|
1052
|
+
: undefined;
|
|
1053
|
+
// A STACK BLOCK THE CALLEE'S BODY LICENSED states the arity outright: arguments past the
|
|
1054
|
+
// registers are staged only once every register is one, so the call takes all of them and the
|
|
1055
|
+
// block, one word each — what a count of that many words in a prototype would state. That holds
|
|
1056
|
+
// as stated on an ABI that never skips an argument register (agbcc's APCS). One that does
|
|
1057
|
+
// (`evenWordAlignedArgs`: O32 leaves a3 empty before a `long long` passed at 16(sp)) licenses a
|
|
1058
|
+
// block only where the frontend saw the call write every register and the callee's lift read
|
|
1059
|
+
// each (frontend/mips.ts `licensedStackWords`, frontend/callee-body.ts `readsEveryArgReg`).
|
|
1060
|
+
const licensed = unstated && (site.stackWords ?? 0) > 0 ? target.argRegs.length + site.stackWords! : undefined;
|
|
1061
|
+
if (licensed !== undefined) {
|
|
1062
|
+
stackSized?.(callee!, site.stackWords!);
|
|
1063
|
+
}
|
|
1064
|
+
const widths =
|
|
1065
|
+
licensed !== undefined
|
|
1066
|
+
? Array.from({ length: licensed }, () => 32)
|
|
1067
|
+
: entryWords !== undefined
|
|
1068
|
+
? Array.from({ length: entryWords }, () => 32)
|
|
1069
|
+
: p.widths;
|
|
1070
|
+
const { returns } = p;
|
|
1071
|
+
// the gaps `decideGap` decided: the value that proved each, and the gap's own refusal, owed if
|
|
1072
|
+
// that proof fails once the function is lifted
|
|
1073
|
+
const decided: Array<{ value: Value; refuse: () => void }> = [];
|
|
1074
|
+
// the refusals of the decided gaps at the first argument register that the call's result must
|
|
1075
|
+
// be read to keep (`compilerBehaviors.forwardsStructReturnDestination`, at `decideGap`)
|
|
1076
|
+
const resultOwed: Array<() => void> = [];
|
|
1077
|
+
// how many argument words the decided gaps put beyond doubt
|
|
1078
|
+
let decidedWords = 0;
|
|
490
1079
|
const argc =
|
|
491
1080
|
widths === null
|
|
492
1081
|
? fallbackArgc(ssa, target.argRegs, bi, {
|
|
493
|
-
accept:
|
|
494
|
-
|
|
1082
|
+
accept: mayPass,
|
|
1083
|
+
bound: bodyWords,
|
|
1084
|
+
leftover,
|
|
1085
|
+
gap: { name, at: guess.at, fail },
|
|
1086
|
+
floor,
|
|
1087
|
+
parked: (r) =>
|
|
1088
|
+
!holes.skippable.has(r) &&
|
|
1089
|
+
!(holes.leadingFloatShadow && r === target.argRegs[0]) &&
|
|
1090
|
+
(ssa.holdsValue(r, bi, (v) => site.parked?.(v, r) === true) ||
|
|
1091
|
+
// a call in the case destroyed it: the hole is that call's, and refuses only where a
|
|
1092
|
+
// register above holds a value written since (`fallbackArgc`'s `parked`)
|
|
1093
|
+
(ssa.hasReachingDef(r, bi, (v) => site.parked?.(v, r) === true) &&
|
|
1094
|
+
!target.argRegs
|
|
1095
|
+
.slice(target.argRegs.indexOf(r) + 1)
|
|
1096
|
+
.some((a) => ssa.holdsValue(a, bi, (v) => mayPass(v, a))))),
|
|
1097
|
+
decide: (gap, set, refuse) => {
|
|
1098
|
+
const value = decideGap(ssa, bi, gap, set, (v) => mayPass(v, set));
|
|
1099
|
+
if (value === undefined) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
decided.push({ value, refuse });
|
|
1103
|
+
if (gap[0] === target.argRegs[0] && target.compilerBehaviors.forwardsStructReturnDestination !== false) {
|
|
1104
|
+
resultOwed.push(refuse);
|
|
1105
|
+
}
|
|
1106
|
+
decidedWords = target.argRegs.indexOf(set) + 1;
|
|
1107
|
+
return true;
|
|
1108
|
+
},
|
|
1109
|
+
entry: (reg) =>
|
|
1110
|
+
!ssa.writtenSinceCall(reg, bi) &&
|
|
1111
|
+
ssa.holdsValue(
|
|
1112
|
+
reg,
|
|
1113
|
+
bi,
|
|
1114
|
+
(v) => mayPass(v, reg) && ssa.paramReg.get(v) === reg && ssa.irBlocks[0].params.includes(v),
|
|
1115
|
+
),
|
|
495
1116
|
})
|
|
496
|
-
: wordsOf(widths);
|
|
1117
|
+
: wordsOf(widths, target);
|
|
497
1118
|
if (widths === null) {
|
|
498
|
-
|
|
1119
|
+
const index = target.argRegs
|
|
1120
|
+
.slice(floor, argc)
|
|
1121
|
+
.find((r) => ssa.hasReachingDef(r, bi, (v) => site.scaledIndex?.(v, r) === true));
|
|
1122
|
+
if (index !== undefined) {
|
|
1123
|
+
fail(
|
|
1124
|
+
`cannot lift '${name}': the call ${typeof guess.at === 'number' ? `at 0x${guess.at.toString(16)}` : guess.at} ` +
|
|
1125
|
+
`has no prototype, and ${index}, which the arity guessed without one counts, still holds the index the ` +
|
|
1126
|
+
"jump table's dispatch scaled — a call handed it and one that is not compile to the same bytes, so the " +
|
|
1127
|
+
'argument count is not decidable',
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
// the lowest argument register past the stated ones that a marker or the scaled index a
|
|
1131
|
+
// dispatch left there may reach, parked or not (a PowerPC pair's r4 never parks): where the
|
|
1132
|
+
// pre-dispatch value stood
|
|
1133
|
+
const markedAt = target.argRegs.findIndex(
|
|
1134
|
+
(r, k) =>
|
|
1135
|
+
k >= floor &&
|
|
1136
|
+
ssa.hasReachingDef(r, bi, (v) => site.parked?.(v, r) === true || site.scaledIndex?.(v, r) === true),
|
|
1137
|
+
);
|
|
1138
|
+
parkedInArgs ||= markedAt >= 0;
|
|
1139
|
+
const call = typeof guess.at === 'number' ? `at 0x${guess.at.toString(16)}` : guess.at;
|
|
1140
|
+
const last = target.argRegs[argc - 1];
|
|
1141
|
+
// the blocks since the dispatch on the way to the call: a path stops at a `switch_br`'s block
|
|
1142
|
+
const since = new Set<number>([bi]);
|
|
1143
|
+
const dispatch = (at: number) => ssa.irBlocks[at].ops.some((op) => op.opcode === 'switch_br');
|
|
1144
|
+
const lastWritten = argc <= floor || ssa.writtenOnEveryPath(last, bi, dispatch, (at) => since.add(at));
|
|
1145
|
+
// what an effect those blocks made so far reads
|
|
1146
|
+
const effectReads = new Set(
|
|
1147
|
+
[...since].flatMap((at) =>
|
|
1148
|
+
ssa.irBlocks[at].ops.flatMap((op) =>
|
|
1149
|
+
(OPCODES[op.opcode as keyof typeof OPCODES] as OpSig | undefined)?.effects ? op.operands : [],
|
|
1150
|
+
),
|
|
1151
|
+
),
|
|
1152
|
+
);
|
|
1153
|
+
const stored = target.argRegs
|
|
1154
|
+
.slice(floor, argc)
|
|
1155
|
+
.find((r) => ssa.holdsValue(r, bi, (v) => mayPass(v, r) && effectReads.has(v)));
|
|
1156
|
+
const leftIncoming =
|
|
1157
|
+
markedAt < 0 ? undefined : target.argRegs.slice(argc, markedAt).find((r) => ssa.holdsEntryValue(r, bi));
|
|
1158
|
+
// each the lowest register of its kind, so one past the body's bound has none below it
|
|
1159
|
+
const open = (reg: string, why: string) => passThrough.push({ call, reg, why, unread: () => unreadByBody(reg) });
|
|
1160
|
+
if (!lastWritten) {
|
|
1161
|
+
open(last, 'the last register the count takes, holds a value some path since the dispatch did not write');
|
|
1162
|
+
}
|
|
1163
|
+
if (stored !== undefined) {
|
|
1164
|
+
open(stored, 'which the count takes, holds a value an effect of its block uses');
|
|
1165
|
+
}
|
|
1166
|
+
if (leftIncoming !== undefined) {
|
|
1167
|
+
open(leftIncoming, "which the count leaves out below a dispatch's register, holds its incoming value");
|
|
1168
|
+
}
|
|
1169
|
+
guess.refuse?.(argc);
|
|
1170
|
+
refuseShortOfDeclared(callee, argc);
|
|
1171
|
+
if (decided.length > 0 && decidedCallRefusal(ssa, bi, argc, site)) {
|
|
1172
|
+
decided[0].refuse();
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// WHAT EVERY ARGUMENT REGISTER HOLDS HERE, where no declaration stated the arity: a value the
|
|
1176
|
+
// count leaves out (or a later trim cuts) may be an argument the source passed, so the dead-read
|
|
1177
|
+
// pin may not call it unused. Asked through `holdsValue`, which reads no register.
|
|
1178
|
+
if (widths === null || bound >= 0) {
|
|
1179
|
+
for (const r of target.argRegs) {
|
|
1180
|
+
ssa.holdsValue(r, bi, (v) => {
|
|
1181
|
+
heldAtUndeclaredCall.add(v);
|
|
1182
|
+
return false; // keep walking: every path's value, not the first
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
// WHAT THE ARITY LEFT OUT, where no declaration stated it: an argument register past a guessed
|
|
1187
|
+
// count, or past the bound a call through an argument register sets, that a value reaches.
|
|
1188
|
+
// The gap refusal has already fired on any such register past a guess; past the bound, the
|
|
1189
|
+
// register leaves the call silently, and the frontend says whether it knows that one to be an
|
|
1190
|
+
// argument all the same. A register at or past what the callee's own body reads is no
|
|
1191
|
+
// argument of it whatever it holds, so it is not asked: read only where the count already asked
|
|
1192
|
+
// the body, the one place the bound left a register out (`fallbackArgc`'s `bound`).
|
|
1193
|
+
if (site.leftOut !== undefined && (widths === null || bound >= 0)) {
|
|
1194
|
+
const readTo = bodyMemo?.words ?? target.argRegs.length;
|
|
1195
|
+
site.leftOut(
|
|
1196
|
+
target.argRegs
|
|
1197
|
+
.slice(argc, Math.max(argc, readTo))
|
|
1198
|
+
.filter((r) => r !== indirect?.reg && ssa.hasReachingDef(r, bi, (v) => mayPass(v, r))),
|
|
1199
|
+
target.argRegs.slice(0, argc),
|
|
1200
|
+
);
|
|
499
1201
|
}
|
|
500
1202
|
// ARGUMENT WORD `k`: a register, or past the registers the word the frontend's ABI places
|
|
501
1203
|
// there. A guess never exceeds `argRegs.length`, so a stack word can only come from a stated
|
|
@@ -508,7 +1210,8 @@ export function callDeclarations(
|
|
|
508
1210
|
const guessed = (k: number): Value => {
|
|
509
1211
|
const r = target.argRegs[k];
|
|
510
1212
|
const v = ssa.readGuessedArg(r, bi);
|
|
511
|
-
|
|
1213
|
+
ssa.noteGuessedArgRead(r, bi, v);
|
|
1214
|
+
return placeholders ? placeholders.guardRead(name, r, v) : v;
|
|
512
1215
|
};
|
|
513
1216
|
const args: Value[] = [];
|
|
514
1217
|
let k = 0;
|
|
@@ -517,7 +1220,7 @@ export function callDeclarations(
|
|
|
517
1220
|
// rather than recovered from two 32-bit arguments later — `contracts.ts` would fire on
|
|
518
1221
|
// the second reading anyway, since the structurer materialises an effectful call once
|
|
519
1222
|
// per result. Its words are wherever the frontend's ABI placed them.
|
|
520
|
-
args.push(w > 32 ? pairs!.fuse(word(k), word(k + 1)) : widths === null ? guessed(k) : word(k));
|
|
1223
|
+
args.push(w > 32 ? pairs!.fuse(word(k), word(k + 1)) : widths === null && k >= floor ? guessed(k) : word(k));
|
|
521
1224
|
k += w > 32 ? 2 : 1;
|
|
522
1225
|
}
|
|
523
1226
|
// A 64-BIT VALUE MAY NOT LEAVE AS A WORD WHERE NOTHING SAYS HOW WIDE THE PARAMETER IS,
|
|
@@ -543,8 +1246,9 @@ export function callDeclarations(
|
|
|
543
1246
|
// no prototype keys a call through a register, so the hint names one only for a named callee
|
|
544
1247
|
const theCall = indirect === undefined ? `the call to '${callee}'` : `the call through ${indirect.reg}`;
|
|
545
1248
|
const itsParams = indirect === undefined ? `'${callee}'s parameters` : "the function pointer's parameters";
|
|
1249
|
+
// a stated fixed word (`variadicFloor`) takes a half as the narrowing its declaration authorises
|
|
546
1250
|
for (const [j, v] of args.entries()) {
|
|
547
|
-
const half = pairs.halfOf.get(v);
|
|
1251
|
+
const half = j < floor ? undefined : pairs.halfOf.get(v);
|
|
548
1252
|
// A DOUBLE THE RUNTIME RETURNED IS NO long long, so the hint below would send the
|
|
549
1253
|
// reader to the wrong declaration: a double leaves a soft-float helper only into
|
|
550
1254
|
// another one, the return, or a parameter declared `double` (`raise/floathelpers.ts`),
|
|
@@ -583,16 +1287,33 @@ export function callDeclarations(
|
|
|
583
1287
|
// spells a different number — `g(1.5)` as `g(1073217536, 0)`. The call names the operands
|
|
584
1288
|
// that are doubles, and `raise/floathelpers.ts` retypes each or refuses it.
|
|
585
1289
|
const doubles = p.doubles.size ? [...p.doubles] : undefined;
|
|
1290
|
+
// A STRUCT PASSED BY VALUE IS ITS WORDS HERE, one operand each, and the call names them as
|
|
1291
|
+
// (first operand, words) pairs: `foldByValueArgs` folds them into the one object passed whole
|
|
1292
|
+
// once the function is lifted, or refuses.
|
|
1293
|
+
const byValue = p.byValue?.flatMap((b) => [b.word, b.words]);
|
|
586
1294
|
const call = mkOp('call', {
|
|
587
1295
|
operands: indirect === undefined ? args : [...args, indirect.address],
|
|
588
1296
|
results: [res],
|
|
589
1297
|
attrs: {
|
|
590
1298
|
...(callee !== undefined ? { target: callee } : { indirect: true }),
|
|
591
1299
|
...(sret === undefined ? {} : { sret: true }),
|
|
1300
|
+
...(returns.kind === 'register-struct' && returns.image ? { [REGISTER_STRUCT]: true } : {}),
|
|
592
1301
|
...(doubles === undefined ? {} : { doubles }),
|
|
1302
|
+
...(p.floatReturn === undefined ? {} : { floatReturn: p.floatReturn }),
|
|
1303
|
+
...(byValue === undefined ? {} : { byValueWords: byValue }),
|
|
593
1304
|
},
|
|
594
1305
|
});
|
|
595
1306
|
ssa.irBlocks[bi].ops.push(call);
|
|
1307
|
+
for (const d of decided) {
|
|
1308
|
+
ssa.requireSoleReader(d.value, call, d.refuse);
|
|
1309
|
+
}
|
|
1310
|
+
for (const refuse of resultOwed) {
|
|
1311
|
+
if (returns.kind === 'word') {
|
|
1312
|
+
ssa.requireReader(res, refuse);
|
|
1313
|
+
} else {
|
|
1314
|
+
refuse();
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
596
1317
|
// A GUESSED arity is revisited in `finish()`: only once the whole function is lifted is it
|
|
597
1318
|
// known whether every path to here passes through another call, which would have clobbered
|
|
598
1319
|
// the argument registers this guess just read.
|
|
@@ -602,8 +1323,30 @@ export function callDeclarations(
|
|
|
602
1323
|
// to an unprototyped type, so a dropped argument would never be refused, and `p(g())` would lift
|
|
603
1324
|
// as `g(); p();`, which compiles to the same bytes. Kept, it lifts as `p(g())`, which passes what
|
|
604
1325
|
// the machine passes — unless `g` is declared void, and its result names nothing.
|
|
1326
|
+
//
|
|
1327
|
+
// …AND WHAT THE TRIM CUTS IS LEFT OUT TOO, so the frontend is asked the `leftOut` question of it
|
|
1328
|
+
// as well: a guess of four the check above passed can still lose a frame address in r2 when
|
|
1329
|
+
// the trim cuts it back to the run below an unfresh r1 (`v = f64(); g(v, &s)` with neither
|
|
1330
|
+
// declared, which otherwise lifts as `g(v)`).
|
|
605
1331
|
if (widths === null) {
|
|
606
|
-
|
|
1332
|
+
const leftOut = site.leftOut;
|
|
1333
|
+
ssa.recordGuessedCall(
|
|
1334
|
+
call,
|
|
1335
|
+
bi,
|
|
1336
|
+
target,
|
|
1337
|
+
indirect === undefined ? undefined : (r) => !voidResults.has(r),
|
|
1338
|
+
leftOut === undefined
|
|
1339
|
+
? undefined
|
|
1340
|
+
: (cut, kept) =>
|
|
1341
|
+
leftOut(
|
|
1342
|
+
cut.filter((r) => r !== indirect?.reg),
|
|
1343
|
+
kept,
|
|
1344
|
+
),
|
|
1345
|
+
site.withholdsSetupArgs,
|
|
1346
|
+
site.asksEveryCut,
|
|
1347
|
+
// a decided gap puts its words beyond doubt, so neither reading cuts below them
|
|
1348
|
+
Math.max(floor, decidedWords),
|
|
1349
|
+
);
|
|
607
1350
|
}
|
|
608
1351
|
if (p.declaredVoid) {
|
|
609
1352
|
voidResults.add(res);
|
|
@@ -617,20 +1360,37 @@ export function callDeclarations(
|
|
|
617
1360
|
if (callee !== undefined) {
|
|
618
1361
|
pairCallee.set(res, callee);
|
|
619
1362
|
}
|
|
620
|
-
} else if (returns.kind === 'word') {
|
|
1363
|
+
} else if (returns.kind === 'word' || (returns.kind === 'register-struct' && returns.image)) {
|
|
621
1364
|
site.write(target.returnReg, res); // the callee defines the return register …
|
|
622
1365
|
}
|
|
623
1366
|
// … and the clobber is recorded after it, so that def is the CALLEE's; a struct, the memory at
|
|
624
1367
|
// argument 0 or the return register's bytes, leaves that register holding nothing the caller
|
|
625
|
-
// may read as a value
|
|
1368
|
+
// may read as a value, save the image of one a local is declared for, which a guessed arity
|
|
1369
|
+
// still does not count (`SsaBuilder.noteStructImage`)
|
|
626
1370
|
ssa.noteCall(bi, p.clobbers);
|
|
1371
|
+
if (returns.kind === 'register-struct' && returns.image) {
|
|
1372
|
+
ssa.noteStructImage(bi, target.returnReg, res);
|
|
1373
|
+
}
|
|
627
1374
|
};
|
|
628
1375
|
return {
|
|
629
1376
|
callClobbers,
|
|
630
1377
|
returnsWithoutHiddenPointer: (callee) => returnsWithoutHiddenPointer(callee, prototypes, target),
|
|
631
1378
|
declaredCall,
|
|
1379
|
+
declaredParams,
|
|
632
1380
|
plan,
|
|
633
1381
|
lower,
|
|
1382
|
+
heldAtUndeclaredCall,
|
|
1383
|
+
refuseDispatchUnlockedPassThrough: (caseStateRead) => {
|
|
1384
|
+
const open = parkedInArgs || caseStateRead ? passThrough.find((p) => !p.unread()) : undefined;
|
|
1385
|
+
if (open !== undefined) {
|
|
1386
|
+
fail(
|
|
1387
|
+
`cannot lift '${name}': the call ${open.call} has no prototype, and ${open.reg}, ` +
|
|
1388
|
+
`${open.why}, in a function that lifts only through what a jump-table dispatch left in its ` +
|
|
1389
|
+
'registers — the arity guessed without a prototype and another compile to the same bytes, so the ' +
|
|
1390
|
+
'argument count is not decidable',
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
634
1394
|
};
|
|
635
1395
|
}
|
|
636
1396
|
|
|
@@ -639,9 +1399,9 @@ export function callDeclarations(
|
|
|
639
1399
|
// up (on agbcc, thumb.h:644-645, 672), so the call's first word is that pointer. What it returns in
|
|
640
1400
|
// the return register is not a value the caller reads (calls.c: the struct is the memory at the
|
|
641
1401
|
// address). One the target returns in registers takes its arguments where they are declared, and
|
|
642
|
-
// its registers hold the struct's bytes
|
|
643
|
-
//
|
|
644
|
-
// call refuses, naming why.
|
|
1402
|
+
// its registers hold the struct's bytes: the call is `'register'`, and the return register is the
|
|
1403
|
+
// struct's image where a local of it can be declared (`registerStructLocal`) and holds nothing the
|
|
1404
|
+
// caller may read otherwise. Every other struct-returning call refuses, naming why.
|
|
645
1405
|
function structReturnOf(
|
|
646
1406
|
name: string,
|
|
647
1407
|
target: TargetDescription,
|
|
@@ -649,22 +1409,46 @@ function structReturnOf(
|
|
|
649
1409
|
own: FnProto,
|
|
650
1410
|
fail: FrontendRefusal,
|
|
651
1411
|
): StructReturn | 'register' {
|
|
652
|
-
const
|
|
1412
|
+
const read = returnedStruct(own, target);
|
|
1413
|
+
if (typeof read === 'object' && 'refused' in read) {
|
|
653
1414
|
fail(
|
|
654
|
-
`cannot lift '${name}': \`${callee}\` is declared to return ${own.returns ?? 'a struct or union'} by value, and ${
|
|
1415
|
+
`cannot lift '${name}': \`${callee}\` is declared to return ${own.returns ?? 'a struct or union'} by value, and ${read.refused}`,
|
|
655
1416
|
);
|
|
1417
|
+
}
|
|
1418
|
+
return read as StructReturn | 'register';
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/** HOW A FUNCTION DECLARED TO RETURN A STRUCT OR UNION BY VALUE HANDS IT BACK on this target: the
|
|
1422
|
+
* struct laid out as a local of it is declared (`type`) where the target returns it through a
|
|
1423
|
+
* hidden pointer in argument 0, `'register'` where it returns it in registers, and the reason
|
|
1424
|
+
* otherwise. Both halves of the return read this: a call to such a callee (`structReturnOf`) and a
|
|
1425
|
+
* function that itself returns one (pipeline.ts `ownStructReturnThroughMemory`). */
|
|
1426
|
+
export function returnedStruct(
|
|
1427
|
+
own: FnProto,
|
|
1428
|
+
target: TargetDescription,
|
|
1429
|
+
): { type: Extract<IrType, { kind: 'struct' }> } | 'register' | { refused: string } {
|
|
656
1430
|
const layout = returnedAggregate(own);
|
|
657
1431
|
const inMemory = returnsInMemory(layout, target);
|
|
658
1432
|
if (inMemory === undefined) {
|
|
659
|
-
|
|
660
|
-
|
|
1433
|
+
return {
|
|
1434
|
+
refused:
|
|
1435
|
+
'nothing here says whether it comes back through a hidden pointer — its members are not all known, this target does not ' +
|
|
661
1436
|
'say how it lays one of them out (an enum, a bitfield), or it states no rule ' +
|
|
662
1437
|
'(a prototype states them as `returnLayout`, a context by defining the struct)',
|
|
663
|
-
|
|
1438
|
+
};
|
|
664
1439
|
}
|
|
665
1440
|
if (inMemory === false) {
|
|
666
1441
|
return 'register';
|
|
667
1442
|
}
|
|
1443
|
+
const type = structLocalType(own, target);
|
|
1444
|
+
return 'refused' in type ? type : { type };
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** The struct a declaration returns by value as a local of it is declared, or why it has none. */
|
|
1448
|
+
export function structLocalType(
|
|
1449
|
+
own: FnProto,
|
|
1450
|
+
target: TargetDescription,
|
|
1451
|
+
): Extract<IrType, { kind: 'struct' }> | { refused: string } {
|
|
668
1452
|
// the local it lands in is declared as the header spells the type, qualifiers aside; the tag
|
|
669
1453
|
// is what the declarations block defines it by, and a typedef name is that tag too
|
|
670
1454
|
const spelling = (own.returns ?? '')
|
|
@@ -673,14 +1457,286 @@ function structReturnOf(
|
|
|
673
1457
|
.replace(/\s+/g, ' ');
|
|
674
1458
|
const tag = /^(?:(?:struct|union)\s+)?([A-Za-z_]\w*)$/.exec(spelling)?.[1];
|
|
675
1459
|
if (tag === undefined) {
|
|
676
|
-
|
|
1460
|
+
return { refused: 'it names no type a local of it could be declared with' };
|
|
677
1461
|
}
|
|
678
|
-
const type = aggregateType(tag
|
|
679
|
-
if (type
|
|
680
|
-
|
|
681
|
-
|
|
1462
|
+
const type = aggregateType(tag, spelling, returnedAggregate(own), target);
|
|
1463
|
+
if (type?.kind !== 'struct') {
|
|
1464
|
+
return {
|
|
1465
|
+
refused:
|
|
1466
|
+
'it is a union, or its members are not all known or this target does not say how it lays one of them out ' +
|
|
682
1467
|
'(an enum, a bitfield) — the local it lands in has no type here',
|
|
683
|
-
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
return type;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// A STRUCT RETURNED IN THE RETURN REGISTER IS READ OFF A LOCAL OF IT (frontend/register-struct-call.ts).
|
|
1474
|
+
// The register holds the struct's bytes as the target lays them in a word: from bit 0 up where
|
|
1475
|
+
// the target is little-endian (agbcc's `{u16}` is `strh r0` off the call), from bit 31 down where it
|
|
1476
|
+
// is big-endian (mwcc's is `srwi r0,r3,16`). So the register is the struct's image, which the call
|
|
1477
|
+
// writes and `liftRegisterStructCalls` re-reads as a load of the local the call assigns, and the
|
|
1478
|
+
// member a later read takes out of it is that local's member: `s = mk(x); gV = s.h;`, which is
|
|
1479
|
+
// what every such compiler compiles to these bytes. Only the declaration says the call returns a
|
|
1480
|
+
// struct at all, since the bytes are a scalar callee's, so only a callee the project declares
|
|
1481
|
+
// gets one, and only where the local can be:
|
|
1482
|
+
// - declared with a type (`structLocalType`): a union or a bitfield has none here;
|
|
1483
|
+
// - one, two or four bytes, the widths one load reads whole — three bytes, or the second
|
|
1484
|
+
// register of an eight-byte struct, is an image no load here builds;
|
|
1485
|
+
// - printed beside its callee's prototype, which the declarations block defines it by;
|
|
1486
|
+
// - all integers: a floating member has no spelling here that reads its bits as the float they
|
|
1487
|
+
// are (structure.ts `frameStructMember` spells an integer field), and `*(s32 *)&s` stored to a
|
|
1488
|
+
// `float` converts the value (agbcc: `bl __floatsisf`).
|
|
1489
|
+
// Otherwise the register still holds nothing the caller may read. The ONE derivation of that type:
|
|
1490
|
+
// the call plan marks the call by it, and `liftRegisterStructCalls` declares the local by it.
|
|
1491
|
+
export function registerStructLocal(
|
|
1492
|
+
own: FnProto | undefined,
|
|
1493
|
+
target: TargetDescription,
|
|
1494
|
+
): Extract<IrType, { kind: 'struct' }> | undefined {
|
|
1495
|
+
if (own === undefined) {
|
|
1496
|
+
return undefined;
|
|
1497
|
+
}
|
|
1498
|
+
const type = structLocalType(own, target);
|
|
1499
|
+
if ('refused' in type || ![1, 2, 4].includes(type.size ?? 0) || hasFloatingMember(type)) {
|
|
1500
|
+
return undefined;
|
|
1501
|
+
}
|
|
1502
|
+
return spellableProto(own, target, type) === undefined ? undefined : type;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function hasFloatingMember(t: IrType): boolean {
|
|
1506
|
+
switch (t.kind) {
|
|
1507
|
+
case 'float':
|
|
1508
|
+
return true;
|
|
1509
|
+
case 'struct':
|
|
1510
|
+
return t.fields.some((f) => hasFloatingMember(f.type));
|
|
1511
|
+
case 'union':
|
|
1512
|
+
return t.members.some((f) => hasFloatingMember(f.type));
|
|
1513
|
+
case 'array':
|
|
1514
|
+
return hasFloatingMember(t.elem);
|
|
1515
|
+
default:
|
|
1516
|
+
return false;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** A STRUCT PASSED BY VALUE IS THE ONE OBJECT ITS ARGUMENT WORDS HOLD AT THE CALL. The lowering
|
|
1521
|
+
* hands its words over one operand each (`byValueWords`, pairs of first operand and word count);
|
|
1522
|
+
* C passes the struct, so the words must be one object's, whole, and that object is what the call
|
|
1523
|
+
* is handed. Two objects are:
|
|
1524
|
+
*
|
|
1525
|
+
* - A FRAME OBJECT the words are loaded off — word loads at consecutive words from one `laddr`
|
|
1526
|
+
* offset. The operand becomes that offset's address, and the frame-object audit judges the
|
|
1527
|
+
* object as the struct (frame-objects.ts `objectShapes`).
|
|
1528
|
+
* - A GLOBAL the words are loaded off, or were just stored to — word accesses at consecutive words
|
|
1529
|
+
* from the symbol's own address (`gaddr`, one or one per word of the same symbol), loaded off it
|
|
1530
|
+
* or stored there as the very values passed. The operand becomes the symbol's address, spelled
|
|
1531
|
+
* bare (`take(gS)`), which compiles only where the declarations type the global as the struct.
|
|
1532
|
+
* So a symbol C cannot name (a section symbol, `.bss`; a function-scope static's `sQ.3`)
|
|
1533
|
+
* refuses, and so does one the declaration context or the symbol map types as anything else:
|
|
1534
|
+
* the struct at offset 0 of `gQs[4]` or of `struct O2 { struct Q q; … } gO2` sits at the
|
|
1535
|
+
* symbol's own address too, and `take(gQs)` is a type error against the header that declares
|
|
1536
|
+
* it (`bareStructRefusal`).
|
|
1537
|
+
*
|
|
1538
|
+
* Either way every access is unqualified and in the call's own block, and no store or call lies
|
|
1539
|
+
* between the earliest of them and the call but those accesses themselves, so the object holds at
|
|
1540
|
+
* the call what the words carry. Every other source — registers computed in place, words off two
|
|
1541
|
+
* objects, off a pointer or into a global's interior, a store the call may not see last — refuses.
|
|
1542
|
+
*
|
|
1543
|
+
* The operand's value is typed a pointer to the struct, and a global's `gaddr` is a fresh one that
|
|
1544
|
+
* names the struct too (`passedAs`), so value numbering never merges it with a plain address of
|
|
1545
|
+
* the symbol. The call then names which operands it passes by value (`byValue`, pairs of operand
|
|
1546
|
+
* and declared parameter, read by ir/core.ts `byValueOperands`). */
|
|
1547
|
+
export function foldByValueArgs(
|
|
1548
|
+
name: string,
|
|
1549
|
+
irBlocks: Block[],
|
|
1550
|
+
calls: Pick<CallDeclarations, 'declaredCall'>,
|
|
1551
|
+
declarations: { symbols: SymbolMap | undefined; context: ContextGlobals | undefined },
|
|
1552
|
+
): void {
|
|
1553
|
+
const defs = new Map<Value, { op: Op; blk: Block }>();
|
|
1554
|
+
for (const blk of irBlocks) {
|
|
1555
|
+
for (const op of blk.ops) {
|
|
1556
|
+
op.results.forEach((r) => defs.set(r, { op, blk }));
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
for (const blk of irBlocks) {
|
|
1560
|
+
for (const call of [...blk.ops]) {
|
|
1561
|
+
const words = call.attrs.byValueWords as number[] | undefined;
|
|
1562
|
+
if (call.opcode !== 'call' || words === undefined) {
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
const callee = calleeName(call);
|
|
1566
|
+
const declared = callee === undefined ? undefined : calls.declaredCall(callee)?.byValue;
|
|
1567
|
+
const pairs = Array.from({ length: words.length / 2 }, (_, i) => {
|
|
1568
|
+
const arg = declared?.find((b) => b.word === words[2 * i]);
|
|
1569
|
+
const type = arg?.type;
|
|
1570
|
+
if (type?.kind !== 'struct' || type.size === undefined) {
|
|
1571
|
+
throw new Error(`internal: a by-value operand of a call to '${callee}' has no declared struct`);
|
|
1572
|
+
}
|
|
1573
|
+
return { first: words[2 * i], n: words[2 * i + 1], type, param: arg!.param };
|
|
1574
|
+
});
|
|
1575
|
+
// highest first, so folding one leaves the operand places of those below it as they were
|
|
1576
|
+
for (const { first, n, type } of [...pairs].sort((x, y) => y.first - x.first)) {
|
|
1577
|
+
const at = blk.ops.indexOf(call);
|
|
1578
|
+
const passed = call.operands.slice(first, first + n);
|
|
1579
|
+
// the earliest of `accesses` and every op after it up to the call writes nothing else
|
|
1580
|
+
const settled = (accesses: readonly Op[]): boolean => {
|
|
1581
|
+
const earliest = Math.min(...accesses.map((a) => blk.ops.indexOf(a)));
|
|
1582
|
+
return blk.ops
|
|
1583
|
+
.slice(earliest, at)
|
|
1584
|
+
.every((op) => (op.opcode !== 'store' && op.opcode !== 'call') || accesses.includes(op));
|
|
1585
|
+
};
|
|
1586
|
+
// a word access at byte `start + 4 * i` from one base, unqualified, in the call's block
|
|
1587
|
+
// before the call
|
|
1588
|
+
const wordAt = (op: Op | undefined, i: number, base: Value | undefined, start = 0): op is Op =>
|
|
1589
|
+
op !== undefined &&
|
|
1590
|
+
blk.ops.indexOf(op) >= 0 &&
|
|
1591
|
+
blk.ops.indexOf(op) < at &&
|
|
1592
|
+
(op.attrs.width as number) === 4 &&
|
|
1593
|
+
!qualified(op) &&
|
|
1594
|
+
op.operands[0] === base &&
|
|
1595
|
+
(op.attrs.off as number) === start + 4 * i;
|
|
1596
|
+
const loads = passed.map((v) => defs.get(v)?.op);
|
|
1597
|
+
const loaded = loads.every((d) => d?.opcode === 'load');
|
|
1598
|
+
const base0 = loaded ? loads[0]!.operands[0] : undefined;
|
|
1599
|
+
const baseDef = base0 === undefined ? undefined : defs.get(base0)?.op;
|
|
1600
|
+
let object: Op | undefined;
|
|
1601
|
+
if (baseDef?.opcode === 'laddr') {
|
|
1602
|
+
// off the frame, through one `laddr` or several of the same offset (the slot model mints
|
|
1603
|
+
// one per access): the displacement is past the `laddr`'s own offset
|
|
1604
|
+
const frameAt = loads.map((d) => {
|
|
1605
|
+
const b = defs.get(d!.operands[0])?.op;
|
|
1606
|
+
return b?.opcode === 'laddr' && wordAt(d, 0, d!.operands[0], d!.attrs.off as number)
|
|
1607
|
+
? (b.attrs.off as number) + (d!.attrs.off as number)
|
|
1608
|
+
: undefined;
|
|
1609
|
+
});
|
|
1610
|
+
const from = frameAt[0];
|
|
1611
|
+
if (from !== undefined && frameAt.every((o, i) => o === from + 4 * i) && settled(loads as Op[])) {
|
|
1612
|
+
object = mkOp('laddr', { results: [mkValue(T.ptr(type))], attrs: { off: from } });
|
|
1613
|
+
}
|
|
1614
|
+
} else {
|
|
1615
|
+
const symbol = (v: Value | undefined): string | undefined => {
|
|
1616
|
+
const d = v === undefined ? undefined : defs.get(v)?.op;
|
|
1617
|
+
return d?.opcode === 'gaddr' && d.attrs.code !== true ? (d.attrs.sym as string) : undefined;
|
|
1618
|
+
};
|
|
1619
|
+
// ONE SYMBOL, NOT ONE VALUE: kmc addresses each word through a `lui`/`lw` pair of its own,
|
|
1620
|
+
// so the words come off several `gaddr`s of the same symbol rather than one base. Each
|
|
1621
|
+
// access is still at its word's offset from the symbol's own address.
|
|
1622
|
+
const ofSymbol = (op: Op | undefined, i: number, sym0: string): op is Op =>
|
|
1623
|
+
op !== undefined && symbol(op.operands[0]) === sym0 && wordAt(op, i, op.operands[0]);
|
|
1624
|
+
const sym0 = symbol(base0);
|
|
1625
|
+
let sym: string | undefined;
|
|
1626
|
+
if (loaded && sym0 !== undefined && loads.every((d, i) => ofSymbol(d, i, sym0)) && settled(loads as Op[])) {
|
|
1627
|
+
sym = sym0;
|
|
1628
|
+
} else {
|
|
1629
|
+
// the words stored to a global just before the call: the last store of each, in order
|
|
1630
|
+
const stores = passed.map((v) =>
|
|
1631
|
+
blk.ops
|
|
1632
|
+
.slice(0, at)
|
|
1633
|
+
.filter((op) => op.opcode === 'store' && op.operands[1] === v)
|
|
1634
|
+
.at(-1),
|
|
1635
|
+
);
|
|
1636
|
+
const sink = symbol(stores[0]?.operands[0]);
|
|
1637
|
+
if (sink !== undefined && stores.every((st, i) => ofSymbol(st, i, sink)) && settled(stores as Op[])) {
|
|
1638
|
+
sym = sink;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const misnamed = sym === undefined ? undefined : bareStructRefusal(sym, type, declarations);
|
|
1642
|
+
if (misnamed !== undefined) {
|
|
1643
|
+
throw new FrontendUnsupportedError(
|
|
1644
|
+
`cannot lift '${name}': the call to \`${callee ?? 'a function pointer'}\` passes ${structSpelling(type)} ` +
|
|
1645
|
+
`by value off '${sym}', and ${misnamed} — the struct would be spelled as that symbol bare`,
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
if (sym !== undefined) {
|
|
1649
|
+
object = mkOp('gaddr', {
|
|
1650
|
+
results: [mkValue(T.ptr(type))],
|
|
1651
|
+
attrs: { sym, passedAs: structSpelling(type) },
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (object === undefined) {
|
|
1656
|
+
throw new FrontendUnsupportedError(
|
|
1657
|
+
`cannot lift '${name}': the call to \`${callee ?? 'a function pointer'}\` passes ${structSpelling(type)} ` +
|
|
1658
|
+
'by value, and its words are not one frame object or one global, read whole just before the call — ' +
|
|
1659
|
+
'a struct passed from anywhere else is not modelled',
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
blk.ops.splice(at, 0, object);
|
|
1663
|
+
call.operands.splice(first, n, object.results[0]);
|
|
1664
|
+
// the word loads, and a frame address only they read, are dead now, and would otherwise be
|
|
1665
|
+
// judged as accesses of their own
|
|
1666
|
+
for (const d of loads) {
|
|
1667
|
+
if (d?.opcode === 'load') {
|
|
1668
|
+
dropIfUnread(irBlocks, d);
|
|
1669
|
+
const b = defs.get(d.operands[0])?.op;
|
|
1670
|
+
if (b?.opcode === 'laddr') {
|
|
1671
|
+
dropIfUnread(irBlocks, b);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
// each folded struct is one operand now: its place is its first word less the words folded
|
|
1677
|
+
// below it
|
|
1678
|
+
const sorted = [...pairs].sort((x, y) => x.first - y.first);
|
|
1679
|
+
const { byValueWords: _, ...attrs } = call.attrs;
|
|
1680
|
+
call.attrs = {
|
|
1681
|
+
...attrs,
|
|
1682
|
+
byValue: sorted.flatMap(({ first, param }) => [
|
|
1683
|
+
first - pairs.filter((q) => q.first < first).reduce((m, q) => m + q.n - 1, 0),
|
|
1684
|
+
param,
|
|
1685
|
+
]),
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/** Why the global `sym` cannot be spelled bare as the struct `type` a call passes by value, or
|
|
1692
|
+
* undefined where it can: a C name, declared as exactly that struct where a declaration context or
|
|
1693
|
+
* a symbol map types it at all. A symbol neither types is declared from the call (declare.ts
|
|
1694
|
+
* `passedAs`). */
|
|
1695
|
+
function bareStructRefusal(
|
|
1696
|
+
sym: string,
|
|
1697
|
+
type: IrType,
|
|
1698
|
+
{ symbols, context }: { symbols: SymbolMap | undefined; context: ContextGlobals | undefined },
|
|
1699
|
+
): string | undefined {
|
|
1700
|
+
// the linker's name for the object, where it is one no C source spells: a static at its
|
|
1701
|
+
// section's start relocated against the section (`.bss`), or a function-scope static's name and
|
|
1702
|
+
// counter (`sQ.3`), which only the static's definition spells (frontend/reloc-symbol.ts)
|
|
1703
|
+
const unspellable = unspellableReason(sym);
|
|
1704
|
+
if (unspellable !== null) {
|
|
1705
|
+
return `that is no name C can spell: it ${unspellable}`;
|
|
1706
|
+
}
|
|
1707
|
+
// ONE STRUCT, MANY SPELLINGS: `typedef struct Q {…} Q_t;` spells it `Q_t` and `struct Q`, and a
|
|
1708
|
+
// header passes one and declares the other. Each side is resolved to the struct it names through
|
|
1709
|
+
// the context's typedefs (`structNamed`), and a spelling that resolves to none matches only as
|
|
1710
|
+
// written.
|
|
1711
|
+
const spelling = structSpelling(type);
|
|
1712
|
+
const passed = structNamed(spelling, context);
|
|
1713
|
+
const isPassed = (other: string): boolean =>
|
|
1714
|
+
other === spelling || (passed !== undefined && structNamed(other, context) === passed);
|
|
1715
|
+
if (context?.declared.has(sym) && !isPassed(context.objectTypes.get(sym) ?? '')) {
|
|
1716
|
+
return `the declaration context does not declare it as ${spelling} (an array, an enclosing struct, another type)`;
|
|
1717
|
+
}
|
|
1718
|
+
const info = symbols === undefined ? undefined : symbolsByName(symbols, new Set([sym])).get(sym);
|
|
1719
|
+
// the map names a struct by its tag, or by the typedef name that binds an untagged one
|
|
1720
|
+
// (symbols.ts `structName`)
|
|
1721
|
+
const mapped = info?.structName;
|
|
1722
|
+
if (
|
|
1723
|
+
info?.shape !== undefined &&
|
|
1724
|
+
!(info.shape === 'struct' && mapped !== undefined && (isPassed(`struct ${mapped}`) || passed === mapped))
|
|
1725
|
+
) {
|
|
1726
|
+
return `the symbol map types it as ${info.shape === 'struct' ? `struct ${info.structName ?? '(untagged)'}` : `${info.shape === 'array' ? 'an' : 'a'} ${info.shape}`}`;
|
|
1727
|
+
}
|
|
1728
|
+
return undefined;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/** Removes `op` where nothing reads its result. */
|
|
1732
|
+
function dropIfUnread(irBlocks: Block[], op: Op): void {
|
|
1733
|
+
const r = op.results[0];
|
|
1734
|
+
const read = irBlocks.some((b) =>
|
|
1735
|
+
b.ops.some((x) => x.operands.includes(r) || x.successors.some((sc) => sc.args.includes(r))),
|
|
1736
|
+
);
|
|
1737
|
+
if (!read) {
|
|
1738
|
+
for (const b of irBlocks) {
|
|
1739
|
+
b.ops = b.ops.filter((x) => x !== op);
|
|
1740
|
+
}
|
|
684
1741
|
}
|
|
685
|
-
return { type: type! };
|
|
686
1742
|
}
|