@asmlift/core 0.10.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
package/src/pipeline.ts
CHANGED
|
@@ -12,8 +12,14 @@ import {
|
|
|
12
12
|
assertResolved,
|
|
13
13
|
assertTypesRecovered,
|
|
14
14
|
} from './contracts';
|
|
15
|
-
import type { AsmData } from './frontend/asmdata';
|
|
15
|
+
import type { AsmData, Reloc } from './frontend/asmdata';
|
|
16
16
|
import { returnedStruct, structLocalType } from './frontend/call-plan';
|
|
17
|
+
import {
|
|
18
|
+
type CalleeBodies,
|
|
19
|
+
type CalleeBodyBound,
|
|
20
|
+
type CalleeStackWords,
|
|
21
|
+
calleeBodyReads,
|
|
22
|
+
} from './frontend/callee-body';
|
|
17
23
|
import { FrontendUnsupportedError } from './frontend/errors';
|
|
18
24
|
import type { LiftOptions } from './frontend/frontend';
|
|
19
25
|
import { liftOwnRegisterStruct, liftOwnStructReturn } from './frontend/own-struct-return';
|
|
@@ -44,12 +50,20 @@ import { type ContextGlobals, splitOwnContext } from './proto-context';
|
|
|
44
50
|
import { type StampBehaviors, stampDeclaredVolatileUnder } from './raise/declared-volatile';
|
|
45
51
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
46
52
|
import { assumedShapes, inferGlobalArrays, orderLicensedGlobals } from './raise/globalshape';
|
|
47
|
-
import { OFFSET_NAME_PASS } from './raise/offsetnames';
|
|
53
|
+
import { OFFSET_NAME_PASS, type OffsetNameBehaviors } from './raise/offsetnames';
|
|
48
54
|
import { type PreRecoveryOptions, type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
|
|
49
55
|
import { PRE_STRUCTURE_FOLDS, type PreStructureFold } from './raise/pre-structure';
|
|
50
56
|
import { recoverTypes } from './raise/recover';
|
|
51
57
|
import { sinkReturns } from './raise/retsink';
|
|
52
|
-
import {
|
|
58
|
+
import { crossJumpedTail } from './structure/cross-jumped-tail';
|
|
59
|
+
import {
|
|
60
|
+
DefaultAcceptsMemo,
|
|
61
|
+
DefaultAcceptsMemoMisuse,
|
|
62
|
+
type JoinPastReturnsSite,
|
|
63
|
+
StructureError,
|
|
64
|
+
structure,
|
|
65
|
+
} from './structure/structure';
|
|
66
|
+
import { unfoldedBranchConstant } from './structure/unfolded-branch-constant';
|
|
53
67
|
import { type SymbolInfo, type SymbolMap, symbolsByName } from './symbols';
|
|
54
68
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
55
69
|
|
|
@@ -101,8 +115,21 @@ export interface DecompileOptions {
|
|
|
101
115
|
* `fromContext` is read as this, and taken out of the table (proto-context.ts
|
|
102
116
|
* `splitOwnContext`). */
|
|
103
117
|
ownContextProto?: FnProto;
|
|
118
|
+
/** OPTIONAL bodies of callees the input does not define, found elsewhere by the driver (the CLI
|
|
119
|
+
* reads them off `tools.asmlift.elf`): each answers what its callee can read of its arguments
|
|
120
|
+
* where the input holds no label of that name (frontend/callee-body.ts). Absent ⇒ only the
|
|
121
|
+
* input's own bodies answer. */
|
|
122
|
+
calleeBodies?: CalleeBodies;
|
|
104
123
|
/** gap policy — see `OnGap`. Default "strict". */
|
|
105
124
|
onGap?: OnGap;
|
|
125
|
+
/** force the return-tail copies (`StructureOptions.copyReturnTails`, rank.ts's `/ret-copy`).
|
|
126
|
+
* Absent ⇒ the shorter of the copies and the join past early returns, ties to the join
|
|
127
|
+
* (`structureReturnTailReading`). */
|
|
128
|
+
copyReturnTails?: boolean;
|
|
129
|
+
/** force the join past early returns wherever the structuring takes it, however long it spells
|
|
130
|
+
* the function (rank.ts's `/ret-join` where the copies are the shorter). Absent ⇒ the shorter,
|
|
131
|
+
* as above. Ignored under `copyReturnTails`. */
|
|
132
|
+
joinPastReturns?: boolean;
|
|
106
133
|
}
|
|
107
134
|
|
|
108
135
|
export interface DecompileResult {
|
|
@@ -155,6 +182,16 @@ export interface DecompileResult {
|
|
|
155
182
|
* conflicting types. Empty where no context entry was handed over, or where the lift stayed
|
|
156
183
|
* within it. */
|
|
157
184
|
ownArityNotes: string[];
|
|
185
|
+
/** WHICH BODIES HANDED IN (`DecompileOptions.calleeBodies`) THE LIFT ASKED, and what each answered
|
|
186
|
+
* (frontend/callee-body.ts): the argument words that callee's body reads at most. A call asks only
|
|
187
|
+
* where a gate questions a register the caller did not set up for it, and only of a callee whose
|
|
188
|
+
* entry states no parameter list (frontend/call-plan.ts `bodyWords`), so a body handed in that no
|
|
189
|
+
* call asked, or that answered nothing, is not here. Empty where no bodies were handed in. */
|
|
190
|
+
calleeBodiesAsked: Record<string, number>;
|
|
191
|
+
/** WHICH CALLEES NOTHING DECLARES THE LIFT SIZED OFF THEIR OWN BODIES, with the stack words each was
|
|
192
|
+
* laid out at past the argument registers (frontend/callee-body.ts `CalleeStackWords`): a call to
|
|
193
|
+
* one takes every argument register and those words, no guess. Empty where none was. */
|
|
194
|
+
calleeStackSized: Record<string, number>;
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
export function decompile(
|
|
@@ -192,6 +229,8 @@ function runTower(
|
|
|
192
229
|
const prototypes = prototypesFromSymbols(opts.symbols, own.prototypes ?? {});
|
|
193
230
|
// (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
|
|
194
231
|
const ownArityNotes: string[] = [];
|
|
232
|
+
const bodiesAsked = new Map<string, number | undefined>();
|
|
233
|
+
const stacksSized = new Map<string, number>();
|
|
195
234
|
const fn = liftStamped(
|
|
196
235
|
name,
|
|
197
236
|
asm,
|
|
@@ -200,7 +239,7 @@ function runTower(
|
|
|
200
239
|
opts.asmData,
|
|
201
240
|
opts.symbols,
|
|
202
241
|
opts.contextGlobals,
|
|
203
|
-
|
|
242
|
+
{ calleeBodies: opts.calleeBodies, bodiesAsked, stacksSized },
|
|
204
243
|
own.ownContextProto,
|
|
205
244
|
ownArityNotes,
|
|
206
245
|
);
|
|
@@ -226,8 +265,14 @@ function runTower(
|
|
|
226
265
|
// `inferredSymbols`, and the ORDER licence only enables a cast spelling that is byte-correct
|
|
227
266
|
// under any declaration. Measured: with this call moved ABOVE both readings, the three corpus
|
|
228
267
|
// rows the pass fires on keep their fan size, their winner and their score exactly.
|
|
268
|
+
//
|
|
269
|
+
// The pass is also where the lift's names become final, so it DECLINES the function — whether or
|
|
270
|
+
// not it named anything — when two accesses reach one cell under two spellings, or one spelling
|
|
271
|
+
// reaches two of the cells the map places a name at (raise/offsetnames.ts `cellSpelledTwice`,
|
|
272
|
+
// RaiseUnsupportedError). That check is no gate: `bench gates --pass offsetnames` tables the walk
|
|
273
|
+
// refusals only, and a decline from it reads as this stage's.
|
|
229
274
|
const walkedNames = opts.symbols
|
|
230
|
-
? OFFSET_NAME_PASS.run(fn, opts.symbols,
|
|
275
|
+
? OFFSET_NAME_PASS.run(fn, opts.symbols, offsetNameBehaviorsFor(target, opts.contextGlobals))
|
|
231
276
|
: [];
|
|
232
277
|
|
|
233
278
|
// (2) idiom fold: apply serializable patterns on the IR (the AI-improvement surface),
|
|
@@ -243,19 +288,34 @@ function runTower(
|
|
|
243
288
|
// (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
|
|
244
289
|
// every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
|
|
245
290
|
const mapSymbols = opts.symbols ? symbolsByName(opts.symbols) : undefined;
|
|
246
|
-
const sfn =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
291
|
+
const { sfn } = structureReturnTailReading(
|
|
292
|
+
fn,
|
|
293
|
+
{
|
|
294
|
+
...structureOptionsFor(target, declaresVoidReturn(prototypes[name]), prototypes),
|
|
295
|
+
// What the EMITTED LANGUAGE can say is a structuring input wherever two recoveries of one
|
|
296
|
+
// shape are behaviourally identical and only one of them is printable (switch fall-through
|
|
297
|
+
// vs plain if-nesting): recovery must not mint a tree this backend would refuse.
|
|
298
|
+
spellSwitchFallthrough: backend.spellsSwitchFallthrough,
|
|
299
|
+
spellSwitchBreak: backend.spellsSwitchBreak,
|
|
300
|
+
spellEarlyReturns: backend.spellsEarlyReturn,
|
|
301
|
+
...(opts.copyReturnTails ? { copyReturnTails: true } : {}),
|
|
302
|
+
...(opts.joinPastReturns ? { joinPastReturns: true } : {}),
|
|
303
|
+
onGap,
|
|
304
|
+
...(mapSymbols ? { symbols: mapSymbols } : {}),
|
|
305
|
+
...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
|
|
306
|
+
...(inferredSymbols.size ? { inferredSymbols } : {}),
|
|
307
|
+
...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
|
|
308
|
+
},
|
|
309
|
+
(t) => backend.emit(t),
|
|
310
|
+
{},
|
|
311
|
+
// ONE default-accepts memo for this lift, as rank.ts keeps one per lift: the copies the measured
|
|
312
|
+
// default structures first carry `copyReturnTails`, so every run of their ladder asks
|
|
313
|
+
// `assertDefaultAccepts` to structure the default first, and without a memo each rerun pays that
|
|
314
|
+
// whole structuring again. `structure()` does not mutate `fn` (structure-purity.test.ts), so the
|
|
315
|
+
// memo's answer is the one each rerun would compute. Measured on Klonoa's sub_0800D188 (1,783
|
|
316
|
+
// blocks): every one of its copies' reruns structured the whole join ladder again.
|
|
317
|
+
new DefaultAcceptsMemo(fn),
|
|
318
|
+
);
|
|
259
319
|
|
|
260
320
|
// (5) lower + print: neutral AST → target language
|
|
261
321
|
const source = backend.emit(sfn);
|
|
@@ -272,6 +332,58 @@ function runTower(
|
|
|
272
332
|
assumedSymbols: assumedShapes(inferredSymbols, sfn, mapSymbols),
|
|
273
333
|
walkedNames,
|
|
274
334
|
ownArityNotes,
|
|
335
|
+
calleeBodiesAsked: answeredBodies(bodiesAsked, opts.calleeBodies),
|
|
336
|
+
calleeStackSized: Object.fromEntries(stacksSized),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The bodies handed in that a lift asked and that answered (`DecompileResult.calleeBodiesAsked`). */
|
|
341
|
+
function answeredBodies(
|
|
342
|
+
asked: ReadonlyMap<string, number | undefined>,
|
|
343
|
+
bodies: CalleeBodies | undefined,
|
|
344
|
+
): Record<string, number> {
|
|
345
|
+
const out: Record<string, number> = {};
|
|
346
|
+
for (const [callee, words] of asked) {
|
|
347
|
+
if (words !== undefined && bodies !== undefined && Object.hasOwn(bodies, callee)) {
|
|
348
|
+
out[callee] = words;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** The `calleeBodiesAsked` and `calleeStackSized` (`DecompileResult`) of one lift under `opts`, for a
|
|
355
|
+
* caller whose result carries none — the ranked path, whose every candidate lifts as this one does.
|
|
356
|
+
* Empty where the lift fails: the run reports that failure itself. */
|
|
357
|
+
export function calleeBodyNotesOf(
|
|
358
|
+
name: string,
|
|
359
|
+
asm: string,
|
|
360
|
+
target: TargetDescription,
|
|
361
|
+
opts: Pick<
|
|
362
|
+
DecompileOptions,
|
|
363
|
+
'prototypes' | 'asmData' | 'symbols' | 'contextGlobals' | 'ownContextProto' | 'calleeBodies'
|
|
364
|
+
>,
|
|
365
|
+
): Pick<DecompileResult, 'calleeBodiesAsked' | 'calleeStackSized'> {
|
|
366
|
+
const own = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
|
|
367
|
+
const asked = new Map<string, number | undefined>();
|
|
368
|
+
const stacksSized = new Map<string, number>();
|
|
369
|
+
try {
|
|
370
|
+
liftStamped(
|
|
371
|
+
name,
|
|
372
|
+
asm,
|
|
373
|
+
target,
|
|
374
|
+
prototypesFromSymbols(opts.symbols, own.prototypes ?? {}),
|
|
375
|
+
opts.asmData,
|
|
376
|
+
opts.symbols,
|
|
377
|
+
opts.contextGlobals,
|
|
378
|
+
{ calleeBodies: opts.calleeBodies, bodiesAsked: asked, stacksSized },
|
|
379
|
+
own.ownContextProto,
|
|
380
|
+
);
|
|
381
|
+
} catch {
|
|
382
|
+
return { calleeBodiesAsked: {}, calleeStackSized: {} };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
calleeBodiesAsked: answeredBodies(asked, opts.calleeBodies),
|
|
386
|
+
calleeStackSized: Object.fromEntries(stacksSized),
|
|
275
387
|
};
|
|
276
388
|
}
|
|
277
389
|
|
|
@@ -282,7 +394,10 @@ export function ownArityNotesOf(
|
|
|
282
394
|
name: string,
|
|
283
395
|
asm: string,
|
|
284
396
|
target: TargetDescription,
|
|
285
|
-
opts: Pick<
|
|
397
|
+
opts: Pick<
|
|
398
|
+
DecompileOptions,
|
|
399
|
+
'prototypes' | 'asmData' | 'symbols' | 'contextGlobals' | 'ownContextProto' | 'calleeBodies'
|
|
400
|
+
>,
|
|
286
401
|
): string[] {
|
|
287
402
|
const own = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
|
|
288
403
|
if (own.ownContextProto === undefined) {
|
|
@@ -298,7 +413,7 @@ export function ownArityNotesOf(
|
|
|
298
413
|
opts.asmData,
|
|
299
414
|
opts.symbols,
|
|
300
415
|
opts.contextGlobals,
|
|
301
|
-
|
|
416
|
+
{ calleeBodies: opts.calleeBodies },
|
|
302
417
|
own.ownContextProto,
|
|
303
418
|
notes,
|
|
304
419
|
);
|
|
@@ -320,6 +435,29 @@ export function stampBehaviorsFor(
|
|
|
320
435
|
};
|
|
321
436
|
}
|
|
322
437
|
|
|
438
|
+
/** What the offset-name pass reads besides the IR and the map: the stamp's behaviours (it re-stamps
|
|
439
|
+
* after naming) and whether the compiler reloads across two spellings of one cell (its one-spelling
|
|
440
|
+
* check, raise/offsetnames.ts `cellSpelledTwice`). */
|
|
441
|
+
export function offsetNameBehaviorsFor(
|
|
442
|
+
target: TargetDescription,
|
|
443
|
+
contextGlobals: ContextGlobals | undefined,
|
|
444
|
+
): OffsetNameBehaviors {
|
|
445
|
+
return {
|
|
446
|
+
...stampBehaviorsFor(target, contextGlobals),
|
|
447
|
+
reloadsAcrossSpellings: target.compilerBehaviors.reloadsAcrossSpellings,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** What `liftStamped` is handed besides the frontend's own options: the bodies of callees found
|
|
452
|
+
* outside the input, which only the callee-body readers read (`bodyReadsIn`), and a record of each
|
|
453
|
+
* callee whose bound a call asked, with its answer (`DecompileResult.calleeBodiesAsked`). */
|
|
454
|
+
export type StampedLiftOptions = LiftOptions & {
|
|
455
|
+
calleeBodies?: CalleeBodies;
|
|
456
|
+
bodiesAsked?: Map<string, number | undefined>;
|
|
457
|
+
/** each callee a call was sized at off its body, with its stack words (`DecompileResult.calleeStackSized`) */
|
|
458
|
+
stacksSized?: Map<string, number>;
|
|
459
|
+
};
|
|
460
|
+
|
|
323
461
|
/** Stage 1 — lift: the target's ISA frontend → L1, verified, with every read of an object
|
|
324
462
|
* `symbols` or `contextGlobals` declares volatile stamped `declared` (raise/declared-volatile.ts)
|
|
325
463
|
* before any pass asks whether it may be deleted. Every entry path lifts through here, each lift
|
|
@@ -334,10 +472,11 @@ export function liftStamped(
|
|
|
334
472
|
asmData: AsmData | undefined,
|
|
335
473
|
symbols: SymbolMap | undefined,
|
|
336
474
|
contextGlobals: ContextGlobals | undefined,
|
|
337
|
-
|
|
475
|
+
stampedOptions?: StampedLiftOptions,
|
|
338
476
|
ownContext?: FnProto,
|
|
339
477
|
notes?: string[],
|
|
340
478
|
): Fn {
|
|
479
|
+
const { calleeBodies, bodiesAsked, stacksSized, ...liftOptions } = stampedOptions ?? {};
|
|
341
480
|
// Its callers split the table already; split again here, where every lift passes, so that no
|
|
342
481
|
// frontend is handed a marked context entry to read as a stated, hard bound.
|
|
343
482
|
const split = splitOwnContext(prototypes, name, ownContext);
|
|
@@ -347,6 +486,30 @@ export function liftStamped(
|
|
|
347
486
|
// a refusal about the return's width that the declared struct explains waits for the proof
|
|
348
487
|
let deferred: ((unproved: string) => never) | undefined;
|
|
349
488
|
const bytes = ownReturn?.register === true ? undefined : ownReturn?.type.size;
|
|
489
|
+
// what each callee whose body is in this input, or handed in, reads of its arguments, lifted from
|
|
490
|
+
// that body as a call to it is first lowered (frontend/callee-body.ts); the callee's own lift reads
|
|
491
|
+
// no bodies, and, off the same lift, what each one loads of its incoming stack words
|
|
492
|
+
const read =
|
|
493
|
+
liftOptions.calleeBodyBound === undefined || liftOptions.calleeStackWords === undefined
|
|
494
|
+
? bodyReadsIn(name, asm, target, prototypes, ownContext, table, asmData, symbols, contextGlobals, calleeBodies)
|
|
495
|
+
: undefined;
|
|
496
|
+
const bound = liftOptions.calleeBodyBound ?? read!.bound;
|
|
497
|
+
const calleeBodyBound: CalleeBodyBound =
|
|
498
|
+
bodiesAsked === undefined
|
|
499
|
+
? bound
|
|
500
|
+
: (callee) => {
|
|
501
|
+
const words = bound(callee);
|
|
502
|
+
bodiesAsked.set(callee, words);
|
|
503
|
+
return words;
|
|
504
|
+
};
|
|
505
|
+
const options = {
|
|
506
|
+
...liftOptions,
|
|
507
|
+
calleeBodyBound,
|
|
508
|
+
calleeStackWords: liftOptions.calleeStackWords ?? read!.stackWords,
|
|
509
|
+
...(stacksSized === undefined
|
|
510
|
+
? {}
|
|
511
|
+
: { stackSized: (callee: string, words: number) => stacksSized.set(callee, words) }),
|
|
512
|
+
};
|
|
350
513
|
const fn = liftWithinContextBound(
|
|
351
514
|
name,
|
|
352
515
|
asm,
|
|
@@ -356,8 +519,8 @@ export function liftStamped(
|
|
|
356
519
|
symbols,
|
|
357
520
|
contextGlobals,
|
|
358
521
|
bytes === undefined
|
|
359
|
-
?
|
|
360
|
-
: { ...
|
|
522
|
+
? options
|
|
523
|
+
: { ...options, ownMemoryReturn: { bytes, defer: (refuse) => void (deferred ??= refuse) } },
|
|
361
524
|
split.ownContextProto,
|
|
362
525
|
notes,
|
|
363
526
|
);
|
|
@@ -380,6 +543,81 @@ export function liftStamped(
|
|
|
380
543
|
return fn;
|
|
381
544
|
}
|
|
382
545
|
|
|
546
|
+
/** Both readers of a callee's body (frontend/callee-body.ts `calleeBodyReads`). */
|
|
547
|
+
type BodyReaders = { bound: CalleeBodyBound; stackWords: CalleeStackWords };
|
|
548
|
+
|
|
549
|
+
/** The readers already built, per caller table as handed to `liftStamped`, each with what else it was
|
|
550
|
+
* read under. A ranked run lifts one function a dozen times under one table, and each callee body
|
|
551
|
+
* is a lift of the whole input, so reading it once per lift would double the lift time of a large
|
|
552
|
+
* unit. Keyed by identity: a table rebuilt reads its bounds again, which costs and never lies. */
|
|
553
|
+
const readsRead = new WeakMap<
|
|
554
|
+
Prototypes,
|
|
555
|
+
Array<{
|
|
556
|
+
key: readonly unknown[];
|
|
557
|
+
reads: BodyReaders;
|
|
558
|
+
}>
|
|
559
|
+
>();
|
|
560
|
+
|
|
561
|
+
/** Each callee's body readers (frontend/callee-body.ts) as a lift of `name` under `table` reads them: every
|
|
562
|
+
* callee lifted from the same input, or from its body in `calleeBodies`, under the same table, map
|
|
563
|
+
* and context, reading no bodies. A body from outside the input is lifted with none of the input's
|
|
564
|
+
* side table, which describes the input's own object, and with only the relocations it carries
|
|
565
|
+
* (`CalleeBody.relocs`). Kept across lifts of the same function under the same caller table,
|
|
566
|
+
* map, data, context and bodies (`readsRead`). */
|
|
567
|
+
function bodyReadsIn(
|
|
568
|
+
name: string,
|
|
569
|
+
asm: string,
|
|
570
|
+
target: TargetDescription,
|
|
571
|
+
prototypes: Prototypes,
|
|
572
|
+
ownContext: FnProto | undefined,
|
|
573
|
+
table: Prototypes,
|
|
574
|
+
asmData: AsmData | undefined,
|
|
575
|
+
symbols: SymbolMap | undefined,
|
|
576
|
+
contextGlobals: ContextGlobals | undefined,
|
|
577
|
+
calleeBodies: CalleeBodies | undefined,
|
|
578
|
+
): BodyReaders {
|
|
579
|
+
const key = [name, asm, target, ownContext, asmData, symbols, contextGlobals, calleeBodies];
|
|
580
|
+
const read = readsRead.get(prototypes) ?? [];
|
|
581
|
+
readsRead.set(prototypes, read);
|
|
582
|
+
const hit = read.find((r) => r.key.every((k, i) => k === key[i]));
|
|
583
|
+
if (hit !== undefined) {
|
|
584
|
+
return hit.reads;
|
|
585
|
+
}
|
|
586
|
+
const reads = calleeBodyReads(
|
|
587
|
+
name,
|
|
588
|
+
asm,
|
|
589
|
+
target,
|
|
590
|
+
table,
|
|
591
|
+
(callee, calleeTable, entryReadCalls, text, relocs) =>
|
|
592
|
+
liftStamped(
|
|
593
|
+
callee,
|
|
594
|
+
text,
|
|
595
|
+
target,
|
|
596
|
+
calleeTable,
|
|
597
|
+
text === asm ? asmData : relocs === undefined ? undefined : relocsOnly(relocs, target),
|
|
598
|
+
symbols,
|
|
599
|
+
contextGlobals,
|
|
600
|
+
{ calleeBodyBound: () => undefined, calleeStackWords: () => undefined, entryReadCalls },
|
|
601
|
+
),
|
|
602
|
+
calleeBodies,
|
|
603
|
+
frontendFor(target).entryStackWords,
|
|
604
|
+
);
|
|
605
|
+
// a handful per table: the symbol settings a ranked run tries, never a growing list
|
|
606
|
+
read.unshift({ key, reads });
|
|
607
|
+
read.length = Math.min(read.length, 8);
|
|
608
|
+
return reads;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** A side table holding a body's own relocations and nothing of the input's object: no section
|
|
612
|
+
* bytes, so no jump table reads, and no symbols. */
|
|
613
|
+
const relocsOnly = (relocs: readonly Reloc[], target: TargetDescription): AsmData => ({
|
|
614
|
+
sections: new Map(),
|
|
615
|
+
relocs: [...relocs],
|
|
616
|
+
symbols: new Map(),
|
|
617
|
+
symbolCount: 0,
|
|
618
|
+
bigEndian: target.capabilities.endianness === 'big',
|
|
619
|
+
});
|
|
620
|
+
|
|
383
621
|
/** THE CONTEXT'S OWN ENTRY BOUNDS THE ARGUMENT REGISTERS, SOFTLY: an argument register past the
|
|
384
622
|
* words it fills (`ownArgumentWordsOn`, the layout a stated entry gets) is lifted as an uninitialised
|
|
385
623
|
* local's home, and where the finished lift still READS one, the function is lifted again with
|
|
@@ -953,6 +1191,250 @@ export function structureChecked(
|
|
|
953
1191
|
return sfn;
|
|
954
1192
|
}
|
|
955
1193
|
|
|
1194
|
+
/** Non-blank lines in an emitted source: what the return-tail reading below is bounded by, and
|
|
1195
|
+
* rank.ts's compactness tie-break, counted exactly as the report counts `lines`
|
|
1196
|
+
* (apps/benchmark/src/eval/quality.ts). Deterministic, and total on any string. */
|
|
1197
|
+
export function sourceLineCount(source: string): number {
|
|
1198
|
+
return source.split('\n').filter((l) => l.trim().length > 0).length;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** The reading a {@link structureReturnTailReading} did not publish, for rank.ts's alternative
|
|
1202
|
+
* candidate: its tree and which reading it is, or why the copies have none. */
|
|
1203
|
+
export type UnpublishedReading =
|
|
1204
|
+
{ readonly reading: 'copies' | 'join'; readonly sfn: SFn } | { readonly reading: 'copies'; readonly error: unknown };
|
|
1205
|
+
|
|
1206
|
+
export interface ReturnTailReading {
|
|
1207
|
+
/** the tree this structuring publishes */
|
|
1208
|
+
readonly sfn: SFn;
|
|
1209
|
+
/** the other reading of the same structuring, where the published one was a choice between two:
|
|
1210
|
+
* absent where no divergent branch took the join past early returns, or where a reading was
|
|
1211
|
+
* forced */
|
|
1212
|
+
readonly unpublished?: UnpublishedReading;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/** Stage 4, THE RETURN-TAIL READING BOUNDED BY SIZE AND READ OFF THE LAYOUT. On a target that
|
|
1216
|
+
* declares the join past early returns its default (target.ts `joinPastReturnsByDefault`:
|
|
1217
|
+
* mwcc_242_81 and mwcc_233_163n), `structureChecked`, and where that structuring took the join
|
|
1218
|
+
* somewhere, the same structuring again reading the return-tail copies (`copyReturnTails`). The copies are
|
|
1219
|
+
* published where they spell the function SHORTER, or where the target's asm layout shows that the
|
|
1220
|
+
* compiler cross-jumped a tail the source wrote into every arm, or its cse left a return unfolded
|
|
1221
|
+
* (below). Every other target publishes the copies (THE MEASURED DEFAULT).
|
|
1222
|
+
*
|
|
1223
|
+
* WHY A BOUND. The join writes the code below a divergent branch once where the copies write it
|
|
1224
|
+
* into every arm; but the join copies a return tail into each early return in its turn, so neither
|
|
1225
|
+
* reading is the shorter in general (structure.ts `structureWithinBudget`). Where the join is the
|
|
1226
|
+
* longer, it is the reading that duplicates code. Measured with the real compilers over the 620
|
|
1227
|
+
* real functions the unbounded join re-spells (2026-11-05): the 180 it makes longer score further
|
|
1228
|
+
* from their bytes than the copies on 89 and closer on 10 (MATCH 23 → 16).
|
|
1229
|
+
*
|
|
1230
|
+
* THE UNIT IS THE WHOLE FUNCTION'S EMITTED SOURCE, in non-blank lines (`sourceLineCount`), and a
|
|
1231
|
+
* tie goes to the join.
|
|
1232
|
+
* - Whole function, not per branch: these are the two whole-function readings the ranked fan
|
|
1233
|
+
* holds (rank.ts's default and its `/ret-copy` or `/ret-join` sibling), so the published tree
|
|
1234
|
+
* is always one of them. A per-branch unit differs only where the join fires at two or more
|
|
1235
|
+
* branches (196 of those 620), and its gain there is not measured. Of the 13 that MATCH only
|
|
1236
|
+
* with the copies (all sa3 agbcc) this bound publishes the copies on 9; on the other 4 the
|
|
1237
|
+
* join is the shorter, and 3 of them take it at a single branch, so a per-branch unit could
|
|
1238
|
+
* recover at most 1. The layout recovers all 4 (THE LAYOUT, below).
|
|
1239
|
+
* - Emitted, not the structured tree: `readabilityRewrites` runs after `structure()` and can fold
|
|
1240
|
+
* copied tails back together (`mergeCommonTails`), so a size read inside `structure()` is not
|
|
1241
|
+
* the size published.
|
|
1242
|
+
* - Lines, not statements: on those 620 the final tree's statement count disagrees with the line
|
|
1243
|
+
* class on 49, and it ties the mwcc jump-table window (cli
|
|
1244
|
+
* test/matching/jump-table-window.test.ts) at 17 statements where the copies are 30 lines and
|
|
1245
|
+
* byte-exact, the join 32.
|
|
1246
|
+
* - Lines alone, not rank.ts `compareScored`'s goto and cast counts ahead of them: on those 620
|
|
1247
|
+
* neither reading spells a `goto`, and casts-first parts from lines on 17, where lines scores
|
|
1248
|
+
* the closer on 5 and casts on 3, with no MATCH between them. The ranked fan holds both
|
|
1249
|
+
* readings, so where the default is ranked `compareScored` still orders them.
|
|
1250
|
+
*
|
|
1251
|
+
* THE LAYOUT. A compiler that cross-jumps return tails (target.ts `crossJumpsReturnTails`) compiles
|
|
1252
|
+
* the copies into the CFG the join reads, with one copy of the tail. So the join is the shorter
|
|
1253
|
+
* reading of those copies, and the bound alone publishes it. What the copies leave instead is a
|
|
1254
|
+
* mark in the layout at a site the join took: the surviving tail ends in a jump over an arm laid
|
|
1255
|
+
* out after it, identical arms stayed separate blocks, or a call at the join was cut from the
|
|
1256
|
+
* argument setup each arm kept (structure/cross-jumped-tail.ts). Where
|
|
1257
|
+
* any site the published join run took shows a mark, the copies are published whatever their size.
|
|
1258
|
+
* - Read only where the target declares the merge. ido7.1 lays a tail out mid-construct with no
|
|
1259
|
+
* cross-jump, and there the same layout fact is not evidence of the copies.
|
|
1260
|
+
* - Whole function, as the bound is: the two readings are still rank.ts's default and its
|
|
1261
|
+
* `/ret-copy` or `/ret-join` sibling, so the ranked fan holds the same two trees. A mark at one
|
|
1262
|
+
* site re-spells every join the function took, marked or not (14 of the 116 agbcc functions
|
|
1263
|
+
* with a mark over those 620 have unmarked sites too); a per-site reading would add trees to
|
|
1264
|
+
* the fan, and is not built.
|
|
1265
|
+
* - MEASURED over those 620 functions, each reading really compiled: the bound with the layout
|
|
1266
|
+
* gives MATCH 44 (copies everywhere 35, the bound alone 40), and loses none of the copies'
|
|
1267
|
+
* matches. The bound alone loses 4, all sa3 agbcc: three where the tail is left mid-construct,
|
|
1268
|
+
* and one where identical case bodies stayed apart. The first two marks were chosen on those
|
|
1269
|
+
* 620; the third, and the split tail of the first, on held-out generated sources written with
|
|
1270
|
+
* the copies, where the bound with the first two still lost matches the copies make (agbcc 7,
|
|
1271
|
+
* gcc2.7.2kmc 9, gcc2.7.2 2 of 160; with all three, 2, 3, 1). Their numbers are in
|
|
1272
|
+
* structure/cross-jumped-tail.ts. So, held out, this reading still loses some of the copies'
|
|
1273
|
+
* matches; the 620 and the full bench lose none. Those 620 MATCH figures are of the bound with
|
|
1274
|
+
* the marks on every compiler; agbcc, ido7.1 and the two GCC 2.7.2 lanes publish the copies
|
|
1275
|
+
* (THE MEASURED DEFAULT), so the marks are read by the tests and by no default (no compiler
|
|
1276
|
+
* that declares `crossJumpsReturnTails` declares the join its default), and the join's gains
|
|
1277
|
+
* there are given up (agbcc 2 of the 620, ido7.1 7: the field's note).
|
|
1278
|
+
* - A mark is evidence, not proof: jump2 cross-jumps shared arm code that is not a return tail
|
|
1279
|
+
* too, and leaves the same marks under a join-written source (structure/cross-jumped-tail.ts).
|
|
1280
|
+
* So a mark moves which verified reading is the default, never what is lifted.
|
|
1281
|
+
*
|
|
1282
|
+
* THE UNFOLDED RETURN. Where the target's cse folds a branch-proven constant only into the branch's
|
|
1283
|
+
* fall-through arm (target.ts `foldsBranchConstantOnTakenEdge` false: gcc 2.x at -O1), an early
|
|
1284
|
+
* return of the very value its branch tested equal to a constant, left unfolded in the object, was
|
|
1285
|
+
* the jump-target (`else`) arm in the source, and the join cannot spell it: `if (x == 0) return x;`
|
|
1286
|
+
* compiles to `return 0` (structure/unfolded-branch-constant.ts). Where any site shows one, the
|
|
1287
|
+
* copies are published whatever their size, as for a layout mark. MEASURED on Mario Party 3's
|
|
1288
|
+
* `func_8010BD28_472F98_mgmode` (gcc2.7.2 -O1), the one held-out real function the join lost a
|
|
1289
|
+
* match on there (2026-11-06): the copies match, the join is 1 line off. Like the layout marks it
|
|
1290
|
+
* is read by no default now: gcc2.7.2 publishes the copies (THE MEASURED DEFAULT).
|
|
1291
|
+
*
|
|
1292
|
+
* THE MEASURED DEFAULT. A target that does not declare `joinPastReturnsByDefault` (agbcc, ido7.1,
|
|
1293
|
+
* gcc2.7.2kmc, gcc2.7.2, mwcc_247_107, and any description that leaves it out) publishes the copies
|
|
1294
|
+
* whatever the bound and the marks would say, and keeps the join as the unpublished reading, so
|
|
1295
|
+
* the ranked fan holds both as before (`copiesByDefault`). The join is declared only where the
|
|
1296
|
+
* held-out real functions and the generated ones written with the copies show the bound with every
|
|
1297
|
+
* mark losing none of the copies' matches; on agbcc and ido7.1 (held out) and on gcc2.7.2kmc and
|
|
1298
|
+
* gcc2.7.2 (generated) it loses some no mark read from the target separates, and mwcc_247_107 is
|
|
1299
|
+
* unmeasured (the field's note has the counts and the gains given up).
|
|
1300
|
+
*
|
|
1301
|
+
* REFUSALS. Never a lift turned into a decline:
|
|
1302
|
+
* - a join that declines or crosses the copy budget already falls to the copies inside
|
|
1303
|
+
* `structure()` (`structureWithinBudget`), and that tree is published as it was;
|
|
1304
|
+
* - outside the measured default, the copies are structured only once the join has lifted, and
|
|
1305
|
+
* where they decline, fail a boundary contract, or the backend cannot emit one of the two, the
|
|
1306
|
+
* join stands;
|
|
1307
|
+
* - under the measured default the copies come first, and where they refuse the join stands, so
|
|
1308
|
+
* a function the old default declined can lift there (`copiesByDefault`);
|
|
1309
|
+
* - a forced reading (`copyReturnTails`, `joinPastReturns`), or a language with no early `return`
|
|
1310
|
+
* (`spellEarlyReturns: false`), is structured once, as asked. rank.ts forces the join this way
|
|
1311
|
+
* for a setting whose stripped sibling dropped its copies.
|
|
1312
|
+
*
|
|
1313
|
+
* HOOKS. The caller's hooks fire for both structurings, so an observer reads the union of what the
|
|
1314
|
+
* two runs saw. That is what keeps rank.ts's off-sibling witness sound: a setting is a tree dedup
|
|
1315
|
+
* of its sibling only where neither reading of the sibling fired the witness. A
|
|
1316
|
+
* `DefaultAcceptsMemoMisuse` is a defect and is rethrown, never an unpublished reading. */
|
|
1317
|
+
export function structureReturnTailReading(
|
|
1318
|
+
fn: Fn,
|
|
1319
|
+
opts: Parameters<typeof structure>[1] & {
|
|
1320
|
+
joinPastReturns?: boolean;
|
|
1321
|
+
crossJumpsReturnTails?: boolean;
|
|
1322
|
+
foldsBranchConstantOnTakenEdge?: boolean;
|
|
1323
|
+
joinPastReturnsByDefault?: boolean;
|
|
1324
|
+
},
|
|
1325
|
+
emit: (sfn: SFn) => string,
|
|
1326
|
+
hooks: Parameters<typeof structure>[2] = {},
|
|
1327
|
+
memo?: Parameters<typeof structure>[3],
|
|
1328
|
+
): ReturnTailReading {
|
|
1329
|
+
// `crossJumpsReturnTails` rides along into `structure()`, which never reads it, as every compiler
|
|
1330
|
+
// behaviour `structureOptionsFor` spreads does: stripped here, it would key this structuring's
|
|
1331
|
+
// default-accepts memo apart from rank.ts's other passes over the same setting.
|
|
1332
|
+
const { joinPastReturns, ...structOpts } = opts;
|
|
1333
|
+
if (structOpts.copyReturnTails === true || structOpts.spellEarlyReturns === false) {
|
|
1334
|
+
return { sfn: structureChecked(fn, structOpts, hooks, memo) };
|
|
1335
|
+
}
|
|
1336
|
+
const sites: JoinPastReturnsSite[] = [];
|
|
1337
|
+
const structureJoin = () =>
|
|
1338
|
+
structureChecked(
|
|
1339
|
+
fn,
|
|
1340
|
+
structOpts,
|
|
1341
|
+
{
|
|
1342
|
+
...hooks,
|
|
1343
|
+
onJoinPastReturns: (site) => {
|
|
1344
|
+
sites.push(site);
|
|
1345
|
+
hooks.onJoinPastReturns?.(site);
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
memo,
|
|
1349
|
+
);
|
|
1350
|
+
if (opts.joinPastReturnsByDefault !== true && joinPastReturns !== true) {
|
|
1351
|
+
return copiesByDefault(fn, structOpts, structureJoin, sites, hooks, memo);
|
|
1352
|
+
}
|
|
1353
|
+
const join = structureJoin();
|
|
1354
|
+
if (sites.length === 0 || joinPastReturns === true) {
|
|
1355
|
+
return { sfn: join };
|
|
1356
|
+
}
|
|
1357
|
+
let copies: SFn;
|
|
1358
|
+
try {
|
|
1359
|
+
copies = structureChecked(fn, { ...structOpts, copyReturnTails: true }, hooks, memo);
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
if (error instanceof DefaultAcceptsMemoMisuse) {
|
|
1362
|
+
throw error;
|
|
1363
|
+
}
|
|
1364
|
+
return { sfn: join, unpublished: { reading: 'copies', error } };
|
|
1365
|
+
}
|
|
1366
|
+
// Read only where the compiler is one that merges return tails: elsewhere the same layout is no
|
|
1367
|
+
// evidence of the copies (ido7.1 lays a tail out mid-construct with no cross-jump at all).
|
|
1368
|
+
const crossJumped = opts.crossJumpsReturnTails === true && sites.some((site) => crossJumpedTail(fn, site));
|
|
1369
|
+
// Read only where the target's cse leaves the taken edge unfolded (gcc 2.x at -O1): elsewhere a
|
|
1370
|
+
// return of the tested value says nothing about which arm the source wrote it in.
|
|
1371
|
+
const unfolded =
|
|
1372
|
+
opts.foldsBranchConstantOnTakenEdge === false && sites.some((site) => unfoldedBranchConstant(fn, site));
|
|
1373
|
+
let publishCopies = false;
|
|
1374
|
+
try {
|
|
1375
|
+
const [copyLines, joinLines] = [sourceLineCount(emit(copies)), sourceLineCount(emit(join))];
|
|
1376
|
+
publishCopies = crossJumped || unfolded || copyLines < joinLines;
|
|
1377
|
+
} catch {
|
|
1378
|
+
// a backend that cannot spell one of the two leaves the join standing; the caller's own emit of
|
|
1379
|
+
// the join reports its refusal where it has one
|
|
1380
|
+
}
|
|
1381
|
+
return publishCopies
|
|
1382
|
+
? { sfn: copies, unpublished: { reading: 'join', sfn: join } }
|
|
1383
|
+
: { sfn: join, unpublished: { reading: 'copies', sfn: copies } };
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/** THE MEASURED DEFAULT (no `joinPastReturnsByDefault`): the copies, structured FIRST and
|
|
1387
|
+
* published wherever they lift, as the default before the join past early returns was one. The join
|
|
1388
|
+
* is structured after them for the ranked fan, and its refusal costs only that reading.
|
|
1389
|
+
*
|
|
1390
|
+
* Where the copies REFUSE, the join is published: never a lift turned into a decline. The copies
|
|
1391
|
+
* here (`copyReturnTails`) are not that old default's structuring: its ladder's other rungs live in
|
|
1392
|
+
* the join's reading (structure.ts THE COPIES, where the join refuses), and a function only they
|
|
1393
|
+
* lift is one the copies alone decline (`synthetic:sw_gotocase2` on
|
|
1394
|
+
* ido7.1, a case that leaves its switch for a loop exit past the switch's end). So this default
|
|
1395
|
+
* also LIFTS some functions the old one declined, where only the join structures them (pokeemerald
|
|
1396
|
+
* `Task_ShowContestEntryMonPic`, agbcc, a jump table whose case arms the copies cannot linearize):
|
|
1397
|
+
* a decline turned into a lift, which the held-out sweep counts as moved, not as level. A refusal a
|
|
1398
|
+
* function's first structuring alone can see (a volatile read stamped late under a `&&`/`||`) is
|
|
1399
|
+
* remembered on the function (structure.ts `STAMPED_LATE`), so the join run after the copies
|
|
1400
|
+
* declines it as they did. Where both refuse, the copies' refusal is the one reported. */
|
|
1401
|
+
function copiesByDefault(
|
|
1402
|
+
fn: Fn,
|
|
1403
|
+
structOpts: Parameters<typeof structure>[1],
|
|
1404
|
+
structureJoin: () => SFn,
|
|
1405
|
+
sites: readonly JoinPastReturnsSite[],
|
|
1406
|
+
hooks: Parameters<typeof structure>[2],
|
|
1407
|
+
memo: Parameters<typeof structure>[3],
|
|
1408
|
+
): ReturnTailReading {
|
|
1409
|
+
const refusal = (error: unknown) => {
|
|
1410
|
+
if (error instanceof DefaultAcceptsMemoMisuse) {
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
return error;
|
|
1414
|
+
};
|
|
1415
|
+
let copies: SFn | undefined;
|
|
1416
|
+
let copiesError: unknown;
|
|
1417
|
+
try {
|
|
1418
|
+
copies = structureChecked(fn, { ...structOpts, copyReturnTails: true }, hooks, memo);
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
copiesError = refusal(error);
|
|
1421
|
+
}
|
|
1422
|
+
let join: SFn;
|
|
1423
|
+
try {
|
|
1424
|
+
join = structureJoin();
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
refusal(error);
|
|
1427
|
+
if (copies === undefined) {
|
|
1428
|
+
throw copiesError;
|
|
1429
|
+
}
|
|
1430
|
+
return { sfn: copies };
|
|
1431
|
+
}
|
|
1432
|
+
if (copies === undefined) {
|
|
1433
|
+
return sites.length === 0 ? { sfn: join } : { sfn: join, unpublished: { reading: 'copies', error: copiesError } };
|
|
1434
|
+
}
|
|
1435
|
+
return sites.length === 0 ? { sfn: copies } : { sfn: copies, unpublished: { reading: 'join', sfn: join } };
|
|
1436
|
+
}
|
|
1437
|
+
|
|
956
1438
|
// ── annotate-mode support ─────────────────────────────────────────────────────────────────
|
|
957
1439
|
|
|
958
1440
|
/** Classify a caught failure by which stage's designed signal it is. `instanceof`, not name
|
|
@@ -1009,6 +1491,8 @@ export function stubResult(name: string, asm: string, backend: LanguageBackend,
|
|
|
1009
1491
|
assumedSymbols: [],
|
|
1010
1492
|
walkedNames: [],
|
|
1011
1493
|
ownArityNotes: [],
|
|
1494
|
+
calleeBodiesAsked: {},
|
|
1495
|
+
calleeStackSized: {},
|
|
1012
1496
|
};
|
|
1013
1497
|
}
|
|
1014
1498
|
|