@asmlift/core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/ir/core.ts
CHANGED
|
@@ -56,8 +56,101 @@ export interface Fn {
|
|
|
56
56
|
/** L1 SIDE DATA (see {@link ParamEvidence}); set by the SSA builder, `undefined` on parsed IR.
|
|
57
57
|
* REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` and `slotHomes` are. */
|
|
58
58
|
paramEvidence: ParamEvidence | undefined;
|
|
59
|
+
/** L1 SIDE DATA (see {@link LocalObjects}); set by a frontend that recovered one, `undefined` on
|
|
60
|
+
* every other `Fn`. REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` is. */
|
|
61
|
+
localObjects: LocalObjects | undefined;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/** Where a function-scope static lives, which decides how its definition is spelled: `rodata` is
|
|
65
|
+
* `const` with an initializer, `data` has an initializer, `bss` has none. */
|
|
66
|
+
export type LocalObjectSection = 'rodata' | 'data' | 'bss';
|
|
67
|
+
|
|
68
|
+
/** One function-scope static, as the target defines it (frontend/local-object.ts). */
|
|
69
|
+
export interface LocalObject {
|
|
70
|
+
/** the name the source wrote — the linker name without the compiler's counter */
|
|
71
|
+
name: string;
|
|
72
|
+
/** the linker name the target carries (`tide.3`, `sprHideTbl$797`) */
|
|
73
|
+
symbol: string;
|
|
74
|
+
/** the counter in that name, which is the order the source declared the function's statics in
|
|
75
|
+
* (frontend/reloc-symbol.ts `localStaticName`) */
|
|
76
|
+
order: number;
|
|
77
|
+
section: LocalObjectSection;
|
|
78
|
+
/** bytes */
|
|
79
|
+
size: number;
|
|
80
|
+
/** the initial contents in target byte order; absent exactly for a `bss` object */
|
|
81
|
+
bytes?: Uint8Array;
|
|
82
|
+
bigEndian: boolean;
|
|
83
|
+
/** What an assembler LISTING shows of an initialized definition beyond its bytes, absent where
|
|
84
|
+
* the target is an object file, which keeps no directives. */
|
|
85
|
+
directives?: LocalObjectDirectives;
|
|
86
|
+
/** What an object FILE records of where the object sits, absent for an assembler listing, whose
|
|
87
|
+
* `.align` is among its directives. */
|
|
88
|
+
placement?: LocalObjectPlacement;
|
|
89
|
+
/** Every address the initializer holds, in offset order — the words a relocation fills in at
|
|
90
|
+
* link time, which the bytes alone do not carry. Absent for an object holding none. The bytes
|
|
91
|
+
* under one say nothing: the relocation's `addend` is the whole of it. */
|
|
92
|
+
relocs?: readonly LocalObjectReloc[];
|
|
93
|
+
/** The object is the compiler's own, under a label the source never wrote (agbcc's `.LC0`): the
|
|
94
|
+
* constant image a local aggregate's initializer is copied from. `name` is then the label, no
|
|
95
|
+
* declaration spells it, and it reaches the C only as that local's initializer
|
|
96
|
+
* (l3/constinit.ts). */
|
|
97
|
+
anonymous?: true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The bytes of every address an initializer holds ({@link LocalObjectReloc}): a pointer's, on every
|
|
101
|
+
* target whose initializers the frontends relocate (agbcc's `.word sym`, mwcc's `R_PPC_ADDR32`). */
|
|
102
|
+
export const LOCAL_OBJECT_ADDRESS_BYTES = 4;
|
|
103
|
+
|
|
104
|
+
/** One address an initializer holds: the pointer-width word at byte `offset` of the object is
|
|
105
|
+
* `symbol`'s address plus `addend` — an ordinary global's, the only kind a declaration outside the
|
|
106
|
+
* function can name (frontend/reloc-symbol.ts). */
|
|
107
|
+
export interface LocalObjectReloc {
|
|
108
|
+
offset: number;
|
|
109
|
+
symbol: string;
|
|
110
|
+
addend: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** What an object file records of where a static sits. */
|
|
114
|
+
export interface LocalObjectPlacement {
|
|
115
|
+
/** the alignment the compiler gave the object (mwcc's `.comment` record for its symbol) */
|
|
116
|
+
align: number;
|
|
117
|
+
/** the object file's own section (`.sbss`, where {@link LocalObject.section} says `bss`) */
|
|
118
|
+
section: string;
|
|
119
|
+
/** its offset in that section */
|
|
120
|
+
offset: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** An initialized static's data directives, as the compiler wrote them. The compiler writes each
|
|
124
|
+
* scalar of the initializer with the directive of its width, and aligns the object to its type's
|
|
125
|
+
* alignment; both are facts about the source's TYPE that the bytes alone have lost. */
|
|
126
|
+
export interface LocalObjectDirectives {
|
|
127
|
+
/** the alignment the object was placed at, in bytes (`.align N` ahead of the label: 2^N; none: 1) */
|
|
128
|
+
align: number;
|
|
129
|
+
/** the width every value directive writes, when they all agree (`.word` → 4); padding does not count */
|
|
130
|
+
unit?: number;
|
|
131
|
+
/** the directives share a `unit` narrower than a word and one wrote a negative value, which the
|
|
132
|
+
* compiler only does for a signed element type */
|
|
133
|
+
negative: boolean;
|
|
134
|
+
/** every value directive writes a string (`.ascii`), as the compiler writes a string literal —
|
|
135
|
+
* whole, or as the elements of an array or struct of strings */
|
|
136
|
+
string: boolean;
|
|
137
|
+
/** every run of padding among the value directives, as [byte offset, byte length], when there
|
|
138
|
+
* is one: in an array, the elements its brace initializer left out — the compiler writes every
|
|
139
|
+
* element the source spelled, zero or not, as a value, and each run it left out as a `.space`
|
|
140
|
+
* (varasm.c `output_constructor`), wherever the run falls: after the last value, ahead of the
|
|
141
|
+
* first, or between two (a designator, a nested brace's short row). In a struct, alignment too. */
|
|
142
|
+
gaps?: [number, number][];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The function-scope statics a function DEFINES, keyed by {@link LocalObject.symbol} — the object a
|
|
146
|
+
* `gaddr` of that name denotes. The IR keeps the linker name because it carries a `.` or `$` that
|
|
147
|
+
* no C identifier and no declared symbol has, so a static can never be one name with a global of
|
|
148
|
+
* its source name (a symbol map's `tide` beside `tide.3`); structure spells it by its source name.
|
|
149
|
+
* A `gaddr` names a symbol some declaration elsewhere describes; these have none, because the
|
|
150
|
+
* source defined them inside the function, so the definition travels with the function to the
|
|
151
|
+
* emitter, which writes it in the body (structure/local-statics.ts). */
|
|
152
|
+
export type LocalObjects = ReadonlyMap<string, LocalObject>;
|
|
153
|
+
|
|
61
154
|
/** What the machine's own object shows about each ENTRY PARAMETER, beyond the value graph — two
|
|
62
155
|
* observations the lift destroys, recorded so raise/paramwidth.ts can read them. Every entry
|
|
63
156
|
* parameter of a lifted function has an entry; a `Fn` with no map is one nobody measured.
|
|
@@ -67,15 +160,19 @@ export interface Fn {
|
|
|
67
160
|
* `compilerBehaviors.narrowParamWitness` decides what the pair means, and the disassemblies for
|
|
68
161
|
* each value live there.
|
|
69
162
|
*
|
|
70
|
-
* `deadHome` — THE STORE DOES NOT REACH THE IR.
|
|
163
|
+
* `deadHome` — THE STORE DOES NOT REACH THE IR. Every slot-modelling frontend spells a word
|
|
71
164
|
* sp-relative store as a write to the SSA key `sp@k` instead of a `store` op (`stackSlotKey`), so
|
|
72
|
-
* a slot nothing reloads has no reader, no op and no value: it simply is not there by L1.
|
|
165
|
+
* a slot nothing reloads has no reader, no op and no value: it simply is not there by L1. MIPS and
|
|
166
|
+
* Thumb lift such a store. PowerPC refuses it, because an unread slot store may be a call's
|
|
167
|
+
* outgoing stack argument; in a function that makes no call, mwcc's local area takes such a store
|
|
168
|
+
* as a store to a frame object instead of a slot (frontend/ppc.ts `measurePpcFrame`). Either way a
|
|
169
|
+
* PowerPC lift never reports a dead home. A
|
|
73
170
|
* parameter stored to two slots, one of them reloaded, still counts — the dead store happened —
|
|
74
171
|
* and the reader's own gates decide what a parameter with that much traffic may become.
|
|
75
172
|
*
|
|
76
173
|
* It asks NO FRAME PARTITION, and that is the difference from {@link SlotHomes}. A slot home is a
|
|
77
174
|
* DECLARATION RANK, so `sp@40` had to be classified as this function's local before it could be
|
|
78
|
-
* stamped, and MIPS declares no
|
|
175
|
+
* stamped, and MIPS declares no `declaredLocals` and so stamps none. "Stored here and never read back"
|
|
79
176
|
* needs no such classification: it is a statement about the store, true at any offset.
|
|
80
177
|
*
|
|
81
178
|
* `selfRedefined` — THE REGISTER IDENTITY DOES NOT REACH THE IR EITHER. SSA renames, so nothing
|
|
@@ -111,7 +208,7 @@ export type ParamEvidence = ReadonlyMap<Value, ParamObservation>;
|
|
|
111
208
|
* A KEY IS NOT A FRAME COORDINATE, and the frontends differ on exactly that. `sp@40` is a local
|
|
112
209
|
* on one ABI and the caller's fifth argument on another, so the shared stamp asks the frontend's
|
|
113
210
|
* own `LiveInModel.declaredLocals` and refuses an offset outside it — Thumb declares a range,
|
|
114
|
-
* MIPS and PPC declare
|
|
211
|
+
* MIPS and PPC declare none and so stamp nothing. An entry here therefore means "storage
|
|
115
212
|
* this function DECLARES, homed at k", not "some sp-relative key".
|
|
116
213
|
*
|
|
117
214
|
* DECLARES, NOT OWNS, AND THE DIFFERENCE IS AN AGBCC FACT WITH A LIVE DEPENDENCY: `ownedLocals`,
|
|
@@ -216,6 +313,35 @@ export function foldWriteOrder(order: WriteOrder | undefined, from: Block, into:
|
|
|
216
313
|
order.writes.set(into, base + (order.writes.get(from) ?? 0));
|
|
217
314
|
}
|
|
218
315
|
|
|
316
|
+
/** The `laddr` attribute that marks a function's OWN struct return object: the local its hidden
|
|
317
|
+
* struct-return pointer became, or the one its struct returned in a register is built in
|
|
318
|
+
* (frontend/own-struct-return.ts). Neither lives in this frame, so no frame offset names it and
|
|
319
|
+
* its `off` means nothing. */
|
|
320
|
+
export const OWN_RETURN_OBJECT = 'returned';
|
|
321
|
+
|
|
322
|
+
/** The `call` attribute of L1 that says its result is the IMAGE of a struct its callee is declared
|
|
323
|
+
* to return in the return register (frontend/call-plan.ts `registerStructLocal`): a word the
|
|
324
|
+
* frontend reads as any call's, which `liftRegisterStructCalls` (frontend/register-struct-call.ts)
|
|
325
|
+
* turns into the struct assigned to a local and re-read off it. No call carries it past that.
|
|
326
|
+
* `true`, or {@link STRUCT_HOMED} where the frontend saw the image stored to a frame home. */
|
|
327
|
+
export const REGISTER_STRUCT = 'registerStruct';
|
|
328
|
+
|
|
329
|
+
/** The value of {@link REGISTER_STRUCT} on a call whose image the function stores to a frame word
|
|
330
|
+
* (frontend/ppc.ts): mwcc gives a struct the source names a home, read or not, and one the source
|
|
331
|
+
* only returns none — `struct T s = mk(1); return s;` is `bl mk; stw r3,8(r1)`, `return mk(1);` is
|
|
332
|
+
* `bl mk` alone. */
|
|
333
|
+
export const STRUCT_HOMED = 'homed';
|
|
334
|
+
|
|
335
|
+
/** The `laddr` attribute that marks the local a struct returned in a register is assigned to
|
|
336
|
+
* (frontend/register-struct-call.ts), holding the callee's name, which names the local. The
|
|
337
|
+
* struct lives in the return register, not in this frame, so no frame offset names it and its
|
|
338
|
+
* `off` means nothing, as for {@link OWN_RETURN_OBJECT}. */
|
|
339
|
+
export const CALL_RETURN_OBJECT = 'callReturned';
|
|
340
|
+
|
|
341
|
+
/** The `ret` attribute that says it hands back the {@link OWN_RETURN_OBJECT} whole, by value: its
|
|
342
|
+
* operand is that object's address, and the C returns the object. */
|
|
343
|
+
export const RETURNS_OBJECT = 'returnsObject';
|
|
344
|
+
|
|
219
345
|
export function mkValue(type: IrType): Value {
|
|
220
346
|
return { type };
|
|
221
347
|
}
|
|
@@ -230,6 +356,46 @@ export function mkOp(opcode: Opcode, o: Partial<Op> = {}): Op {
|
|
|
230
356
|
};
|
|
231
357
|
}
|
|
232
358
|
|
|
359
|
+
// ── a call's callee: a NAME (`attrs.target`) or, for a call through a register, a VALUE ─────────
|
|
360
|
+
//
|
|
361
|
+
// An INDIRECT call carries `indirect: true` and no `target`, and calls the function whose address
|
|
362
|
+
// is its LAST operand. The arguments come first on both forms, so `operands[k]` is argument k on
|
|
363
|
+
// either and a reader that indexes an argument needs no case; a reader that takes the WHOLE operand
|
|
364
|
+
// list as the arguments must go through `callArgs`.
|
|
365
|
+
|
|
366
|
+
/** The function an indirect call calls, or undefined for a call that names its callee. */
|
|
367
|
+
export function calleeValue(op: Op): Value | undefined {
|
|
368
|
+
return op.opcode === 'call' && op.attrs.indirect === true ? op.operands[op.operands.length - 1] : undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The callee a call names, or undefined for an indirect one. */
|
|
372
|
+
export function calleeName(op: Op): string | undefined {
|
|
373
|
+
return op.opcode === 'call' && typeof op.attrs.target === 'string' ? op.attrs.target : undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** The operands a call passes a struct BY VALUE (frontend/call-plan.ts `foldByValueArgs`): each
|
|
377
|
+
* one's place in the operands and the declared parameter it is. The two differ by more than a
|
|
378
|
+
* hidden return pointer wherever a parameter before it takes two operands, so the parameter is
|
|
379
|
+
* carried rather than re-derived. */
|
|
380
|
+
export function byValueOperands(op: Op): { at: number; param: number }[] {
|
|
381
|
+
const pairs = op.opcode === 'call' ? ((op.attrs.byValue as number[] | undefined) ?? []) : [];
|
|
382
|
+
return Array.from({ length: pairs.length / 2 }, (_, i) => ({ at: pairs[2 * i], param: pairs[2 * i + 1] }));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** A call's arguments, the callee value of an indirect call aside. */
|
|
386
|
+
export function callArgs(op: Op): Value[] {
|
|
387
|
+
return calleeValue(op) === undefined ? op.operands : op.operands.slice(0, -1);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Cut a call down to its first `n` arguments, keeping an indirect call's callee. */
|
|
391
|
+
export function truncateCallArgs(op: Op, n: number): void {
|
|
392
|
+
const callee = calleeValue(op);
|
|
393
|
+
op.operands.length = n;
|
|
394
|
+
if (callee !== undefined) {
|
|
395
|
+
op.operands.push(callee);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
233
399
|
/** A block's last op — its terminator on well-formed IR, `undefined` on a block with no ops.
|
|
234
400
|
*
|
|
235
401
|
* `ir/verify.ts` rejects an empty block and every entry path verifies before raising, so the
|
|
@@ -290,6 +456,33 @@ export function reachableBlocks(fn: Fn): Set<Block> {
|
|
|
290
456
|
return seen;
|
|
291
457
|
}
|
|
292
458
|
|
|
459
|
+
/** The blocks `from` reaches along FORWARD edges — never a back-edge, whose target dominates its
|
|
460
|
+
* source — `from` itself included. A block of `stop` is reached but not walked past.
|
|
461
|
+
*
|
|
462
|
+
* Forward only, so an enclosing loop's next iteration does not count: the exit of a loop nested in
|
|
463
|
+
* another reaches every block of the inner body round the outer back-edge, and a full walk would
|
|
464
|
+
* say it runs the inner loop's own arms. Two passes ask "does a loop's exit run this block":
|
|
465
|
+
* `raise/latch.ts`'s exit-trampoline scope gate and `structure.ts`'s `offExitPathOf` (the
|
|
466
|
+
* duplication policy `codeBreakArm` and `earlyReturnArm` share). */
|
|
467
|
+
export function reachForward(
|
|
468
|
+
from: Block,
|
|
469
|
+
dom: ReadonlyMap<Block, ReadonlySet<Block>>,
|
|
470
|
+
stop: ReadonlySet<Block> = new Set(),
|
|
471
|
+
): Set<Block> {
|
|
472
|
+
const seen = new Set<Block>();
|
|
473
|
+
for (const stack = [from]; stack.length > 0;) {
|
|
474
|
+
const x = stack.pop()!;
|
|
475
|
+
if (seen.has(x)) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
seen.add(x);
|
|
479
|
+
if (!stop.has(x)) {
|
|
480
|
+
stack.push(...successorsOf(x).filter((s) => !dom.get(x)!.has(s)));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return seen;
|
|
484
|
+
}
|
|
485
|
+
|
|
293
486
|
/** Predecessor map for the whole function's CFG. */
|
|
294
487
|
export function predecessors(fn: Fn): Map<Block, Block[]> {
|
|
295
488
|
const preds = new Map<Block, Block[]>();
|
|
@@ -307,7 +500,7 @@ export function predecessors(fn: Fn): Map<Block, Block[]> {
|
|
|
307
500
|
/** Forward dominators (iterative data-flow). dom(b) = {b} ∪ ⋂ dom(preds).
|
|
308
501
|
*
|
|
309
502
|
* A CFG fact, so it lives beside `predecessors` it is built on rather than with either consumer:
|
|
310
|
-
* `verify` needs it to check def-dominates-use, `
|
|
503
|
+
* `verify` needs it to check def-dominates-use, `ir/loops.ts` to tell a back-edge from a
|
|
311
504
|
* forward one, and `raise/latch.ts` to tell a latch from a preheader. */
|
|
312
505
|
|
|
313
506
|
export function dominators(fn: Fn): Map<Block, Set<Block>> {
|
|
@@ -412,7 +605,7 @@ export function forwardingTarget(b: Block): Block {
|
|
|
412
605
|
* a CFG fact a parameter for one caller's extra edges — so it stays where it is, and what this
|
|
413
606
|
* paragraph buys is that the next reader looking for a fourth copy knows which of the two the
|
|
414
607
|
* third one is. */
|
|
415
|
-
export function mergeClasses(fn: Fn): Map<Value, readonly Value[]> {
|
|
608
|
+
export function mergeClasses(fn: Pick<Fn, 'blocks'>): Map<Value, readonly Value[]> {
|
|
416
609
|
const parent = new Map<Value, Value>();
|
|
417
610
|
const find = (v: Value): Value => {
|
|
418
611
|
let r = v;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// asmlift IR — the access discipline: which ops the recompile must run where, and as often as, the
|
|
2
|
+
// asm ran them, and the questions every pass asks about that.
|
|
3
|
+
//
|
|
4
|
+
// Two kinds of fact meet here. The registry's flags (ir/opcodes.ts `OpSig`: `effects`, `reads`,
|
|
5
|
+
// `traps`) are facts about an OPCODE. A PLACEMENT is a fact about one OP, decided once by the pass
|
|
6
|
+
// that knew it: `call` is its own opcode; `helper` is the stamp `runtime-helpers.ts` `helperOp` puts
|
|
7
|
+
// on a value op the asm computed by calling a runtime routine; `device` is the `volatile` a frontend
|
|
8
|
+
// puts on a memory access, decided by two pins — the device pin (frontend/device-pins.ts), for an
|
|
9
|
+
// access of a device register, and the dead-read pin (frontend/dead-reads.ts), for a read at ANY
|
|
10
|
+
// address whose value the lifted function never uses; `declared` is the
|
|
11
|
+
// stamp raise/declared-volatile.ts puts on an access of an object the symbol map declares volatile —
|
|
12
|
+
// a read only where the compiler could have made it of a `volatile` — as the lift is made and again
|
|
13
|
+
// when structuring starts. Every question below takes the op, so a pass
|
|
14
|
+
// asks one question instead of re-deriving the combination, and `PLACEMENT_ANSWERS` is the one
|
|
15
|
+
// place a placement's answers are written, the attrs a rebuilt op carries it by among them.
|
|
16
|
+
//
|
|
17
|
+
// A new placement is three edits: its `Placement` member with a `PLACEMENT_ANSWERS` row, the
|
|
18
|
+
// `placedAt` clause that reads its stamp, and its key in contracts.ts `assertEffectsPreserved`,
|
|
19
|
+
// whose switch over `placedAt` does not compile until the member is keyed or answered as uncounted.
|
|
20
|
+
//
|
|
21
|
+
// NAMED RESIDUE. The sites below ask a registry question where a placement could change the answer,
|
|
22
|
+
// and keep the registry's answer. DELIBERATE: the placement does not bear on what the site asks.
|
|
23
|
+
// GAP: it does, and some input gets a wrong answer.
|
|
24
|
+
//
|
|
25
|
+
// `speculationUnsafe` answers no for a plain read — DELIBERATE, argued at the function — and for
|
|
26
|
+
// a trapping divide — GAP: raise/shortcircuit.ts hoists one the structurer may name above its guard.
|
|
27
|
+
// A QUALIFIED read is one `qualified` answers yes for.
|
|
28
|
+
// Two qualified reads commuting (`orderSensitive` without `effectful` on either side):
|
|
29
|
+
// pattern/engine.ts `reordersUnsequenced` — DELIBERATE: the structurer's barrier scan names the
|
|
30
|
+
// first of two qualified reads the fold would join in one expression.
|
|
31
|
+
// structure/hazards.ts `movesPast` — DELIBERATE: the analysis names a qualified read another
|
|
32
|
+
// qualified read stands between it and its render, so the scan never weighs two.
|
|
33
|
+
// `effectful` as the barrier a read may not cross, without `isBarrier`'s helper and qualified-read
|
|
34
|
+
// clauses (structure/analysis.ts):
|
|
35
|
+
// structure/analysis.ts `writeBetween`, `standsOnMovableRead`, the multi-render rule — DELIBERATE:
|
|
36
|
+
// each asks about a memory write, and a qualified read writes nothing.
|
|
37
|
+
// The `call` placement alone, in structure/analysis.ts:
|
|
38
|
+
// `callPos` — DELIBERATE: it asks which values live across the registers a call clobbers.
|
|
39
|
+
// `readCone`, `isHomeableDef`, `copyInterdependentValues` — DELIBERATE: a qualified read is a
|
|
40
|
+
// `load` there, which each already stops at.
|
|
41
|
+
// `effectful` as block purity — structure.ts's exit ownership, preheader claim, header purity,
|
|
42
|
+
// copied `ret` target, `ends` and latch effect roots; structure/hazards.ts `testSkipsAnEffect`;
|
|
43
|
+
// structure/redundant-test.ts `testRereadsOnly` — DELIBERATE: a qualified read renders with its
|
|
44
|
+
// block, once on each path that runs the block, or declines upstream.
|
|
45
|
+
// raise/divpow2.ts's bias arm — GAP: the arm is deleted, and a dead qualified read in it with it.
|
|
46
|
+
// And three accesses the deciders leave unplaced, where a placement would change an answer — GAP:
|
|
47
|
+
// a device store a pinned read of the same cell re-reads in a loop, under the per-object policy
|
|
48
|
+
// (frontend/device-pins.ts's header argues it);
|
|
49
|
+
// a read of a map-declared volatile object through a base that reaches no name — a pointer
|
|
50
|
+
// walked along the object in a loop, a join of two different addresses in it
|
|
51
|
+
// (raise/declared-volatile.ts);
|
|
52
|
+
// a read the stamp cannot place by byte — a runtime index, an `aload` — of an object the map
|
|
53
|
+
// qualifies only some members of, which may reach a plain member (symbols.ts
|
|
54
|
+
// `declaresVolatile`).
|
|
55
|
+
import type { Block, Op, Value } from './core';
|
|
56
|
+
import { MEM_BASE_OPS, type OpSig, opSig } from './opcodes';
|
|
57
|
+
|
|
58
|
+
/** Why an op must run where the asm ran it. */
|
|
59
|
+
export type Placement = 'call' | 'helper' | 'device' | 'declared';
|
|
60
|
+
|
|
61
|
+
/** Where a qualified access's spelling carries its `volatile`: on a cast of its own address, or
|
|
62
|
+
* through the declaration of the object it names. */
|
|
63
|
+
export type Qualifier = 'cast' | 'declaration';
|
|
64
|
+
|
|
65
|
+
/** The attr raise/declared-volatile.ts stamps a `declared` access with. */
|
|
66
|
+
export const DECLARED_VOLATILE = 'declaredVolatile';
|
|
67
|
+
|
|
68
|
+
interface PlacementAnswers {
|
|
69
|
+
/** spelled even when nothing reads the result */
|
|
70
|
+
keptWhenDead: boolean;
|
|
71
|
+
/** never run on a path that did not run it */
|
|
72
|
+
speculationUnsafe: boolean;
|
|
73
|
+
/** each render is one more execution, so it renders once on every path the asm ran it on */
|
|
74
|
+
counted: boolean;
|
|
75
|
+
/** an access whose spelling must carry `volatile`, and where it carries it */
|
|
76
|
+
qualifiedBy: Qualifier | null;
|
|
77
|
+
/** the attrs that put this placement on an op a pass builds to stand for `from` */
|
|
78
|
+
carried: (from: Op) => Op['attrs'];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const PLACEMENT_ANSWERS: Readonly<Record<Placement, PlacementAnswers>> = {
|
|
82
|
+
// The registry's `effects` flag gives a call the first two answers on its own, and its opcode is
|
|
83
|
+
// its placement, which an op built to stand for it does not inherit.
|
|
84
|
+
call: { keptWhenDead: true, speculationUnsafe: true, counted: true, qualifiedBy: null, carried: () => ({}) },
|
|
85
|
+
// The value the helper computes is pure: nothing observes a divide nobody reads, a fold that
|
|
86
|
+
// moves one under a guard drops the stamp (`forgetHelperPlacement`), and agbcc computes one
|
|
87
|
+
// spelled twice once (`a / n + a / n` is one `bl __divsi3`).
|
|
88
|
+
helper: {
|
|
89
|
+
keptWhenDead: false,
|
|
90
|
+
speculationUnsafe: false,
|
|
91
|
+
counted: false,
|
|
92
|
+
qualifiedBy: null,
|
|
93
|
+
carried: (from) => ({ helper: from.attrs.helper }),
|
|
94
|
+
},
|
|
95
|
+
// The access is what is observable, not the value it yields, so its spelling is qualified.
|
|
96
|
+
device: {
|
|
97
|
+
keptWhenDead: true,
|
|
98
|
+
speculationUnsafe: true,
|
|
99
|
+
counted: true,
|
|
100
|
+
qualifiedBy: 'cast',
|
|
101
|
+
carried: () => ({ volatile: true }),
|
|
102
|
+
},
|
|
103
|
+
declared: {
|
|
104
|
+
keptWhenDead: true,
|
|
105
|
+
speculationUnsafe: true,
|
|
106
|
+
counted: true,
|
|
107
|
+
qualifiedBy: 'declaration',
|
|
108
|
+
carried: () => ({ [DECLARED_VOLATILE]: true }),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Every placement stamp an op carries, the one that places it first. An op can carry two: a read
|
|
113
|
+
* of an object the map declares volatile is `declared` and, kept by the lift's own pin (frontend/
|
|
114
|
+
* dead-reads.ts, frontend/device-pins.ts), `device` too.
|
|
115
|
+
*
|
|
116
|
+
* A declaration that qualifies the object places the access before a lift's own stamp does: both
|
|
117
|
+
* say the access is `volatile`, and the declaration is the spelling the source used, so the
|
|
118
|
+
* access counts and spells as the object's (raise/declared-volatile.ts re-derives it per map, and
|
|
119
|
+
* under a map that declares nothing the lift's stamp places it again — which is why a rebuild
|
|
120
|
+
* carries both, `carryDiscipline`). */
|
|
121
|
+
function stampsOf(op: Op): Placement[] {
|
|
122
|
+
if (op.opcode === 'call') {
|
|
123
|
+
return ['call'];
|
|
124
|
+
}
|
|
125
|
+
const stamps: Placement[] = [];
|
|
126
|
+
if (MEM_BASE_OPS.has(op.opcode) && op.attrs[DECLARED_VOLATILE] === true) {
|
|
127
|
+
stamps.push('declared');
|
|
128
|
+
}
|
|
129
|
+
if (MEM_BASE_OPS.has(op.opcode) && op.attrs.volatile === true) {
|
|
130
|
+
stamps.push('device');
|
|
131
|
+
}
|
|
132
|
+
// An `opaque` carries `helper` too, naming the call nothing could fold (`refuseUnmodelledHelpers`);
|
|
133
|
+
// it is no value op, and its `effects` flag already places it.
|
|
134
|
+
if (op.opcode !== 'opaque' && typeof op.attrs.helper === 'string') {
|
|
135
|
+
stamps.push('helper');
|
|
136
|
+
}
|
|
137
|
+
return stamps;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The placement an op carries, or null for an op that may render wherever its value is used. */
|
|
141
|
+
export function placedAt(op: Op): Placement | null {
|
|
142
|
+
return stampsOf(op)[0] ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const answered = (op: Op, q: 'keptWhenDead' | 'speculationUnsafe' | 'counted'): boolean => {
|
|
146
|
+
const p = placedAt(op);
|
|
147
|
+
return p !== null && PLACEMENT_ANSWERS[p][q];
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Where this access's spelling carries the `volatile` the recompile needs to make it where, and as
|
|
151
|
+
* often as, the asm did — or null for an op whose spelling needs none. A spelling through the
|
|
152
|
+
* declaration needs a cast's qualifier too wherever it reaches the object through a cast, since a
|
|
153
|
+
* cast's pointee drops the declaration's qualifiers (l3/volstore.ts `castOffDeclaration`). */
|
|
154
|
+
export function qualifiedBy(op: Op): Qualifier | null {
|
|
155
|
+
const p = placedAt(op);
|
|
156
|
+
return p === null ? null : PLACEMENT_ANSWERS[p].qualifiedBy;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Must this access's spelling carry `volatile`? */
|
|
160
|
+
export function qualified(op: Op): boolean {
|
|
161
|
+
return qualifiedBy(op) !== null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const sigOf = (op: Op): OpSig | undefined => opSig(op.opcode);
|
|
165
|
+
|
|
166
|
+
/** Does each render of this op execute it — a call, or a qualified access? Such an op is spelled
|
|
167
|
+
* once on every path the asm ran it on, at the position it ran: named where a second use, an edge
|
|
168
|
+
* or another block would render it again or elsewhere. */
|
|
169
|
+
export function counted(op: Op): boolean {
|
|
170
|
+
return answered(op, 'counted');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Has an observable side effect: a memory write, a call, an unmodelled instruction. The question a
|
|
174
|
+
* block-purity test asks, and the write a memory read may not cross. */
|
|
175
|
+
export function effectful(op: Op): boolean {
|
|
176
|
+
return sigOf(op)?.effects === true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** May a dead result be deleted? Registered, no effect, not control flow, and not an op whose
|
|
180
|
+
* placement keeps it. A memory read whose value the RAW lift never uses is never asked: the
|
|
181
|
+
* frontend stamps it `volatile` first (frontend/dead-reads.ts), because a compiler deletes every
|
|
182
|
+
* plain read nobody uses, so one still in the target was qualified. What reaches this question
|
|
183
|
+
* plain is a read only asmlift's own rewrites orphaned, or one that pass's gates refused. */
|
|
184
|
+
export function deletableWhenDead(op: Op): boolean {
|
|
185
|
+
const sig = sigOf(op);
|
|
186
|
+
return !!sig && !sig.effects && !sig.terminator && !answered(op, 'keptWhenDead');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** THE DEAD OPS of these blocks: each op `deletableWhenDead` answers yes for whose single result no
|
|
190
|
+
* op outside the set reads, as an operand or as a successor's argument, to a fixed point. The ONE
|
|
191
|
+
* definition of dead: pattern/engine.ts `dce` deletes exactly this set, and the dead-read pin
|
|
192
|
+
* (frontend/dead-reads.ts) asks it of the raw lift before anything is deleted, so the pin's "dead"
|
|
193
|
+
* cannot drift from what the pass would delete.
|
|
194
|
+
*
|
|
195
|
+
* `usesOperands` says whether an op's operands are uses at all. Every op's are, for `dce`; the pin
|
|
196
|
+
* passes the one exception a declaration makes: the `ret` of a function its own prototype declares
|
|
197
|
+
* void, whose operand the structurer suppresses (structure/analysis.ts `indexUses`). */
|
|
198
|
+
export function deadOps(blocks: readonly Block[], usesOperands: (op: Op) => boolean = () => true): Set<Op> {
|
|
199
|
+
const dead = new Set<Op>();
|
|
200
|
+
let changed = true;
|
|
201
|
+
while (changed) {
|
|
202
|
+
changed = false;
|
|
203
|
+
const used = new Set<Value>();
|
|
204
|
+
for (const b of blocks) {
|
|
205
|
+
for (const op of b.ops) {
|
|
206
|
+
if (dead.has(op)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (usesOperands(op)) {
|
|
210
|
+
for (const o of op.operands) {
|
|
211
|
+
used.add(o);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const s of op.successors) {
|
|
215
|
+
for (const a of s.args) {
|
|
216
|
+
used.add(a);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const b of blocks) {
|
|
222
|
+
for (const op of b.ops) {
|
|
223
|
+
if (!dead.has(op) && deletableWhenDead(op) && op.results.length === 1 && !used.has(op.results[0])) {
|
|
224
|
+
dead.add(op);
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return dead;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Must the structurer still SPELL this op when nothing consumes its result — an effect, or a
|
|
234
|
+
* memory read?
|
|
235
|
+
*
|
|
236
|
+
* The read half points the opposite way from `deletableWhenDead`, and both are right. That one is
|
|
237
|
+
* the C claim: nothing observes a load nobody reads. This one is the COMPILER claim: an optimizing
|
|
238
|
+
* compiler deletes every dead read it is allowed to delete, so one still in the target is evidence
|
|
239
|
+
* the source's access was `volatile`. A yes here only says the op may not be dropped silently;
|
|
240
|
+
* whether the read was `volatile` is decided once, by the dead-read pin (frontend/dead-reads.ts),
|
|
241
|
+
* and the caller spells a read only where it is `qualified` (structure.ts `volatileQualifiable`),
|
|
242
|
+
* dropping one the pin left plain.
|
|
243
|
+
*
|
|
244
|
+
* Its own function and not `orderSensitive`, though the two agree on every registered opcode: they
|
|
245
|
+
* ask different questions, and a registry change that splits them is a decision to make at each. */
|
|
246
|
+
export function spelledWhenDead(op: Op): boolean {
|
|
247
|
+
const sig = sigOf(op);
|
|
248
|
+
return !!sig && (!!sig.effects || !!sig.reads);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** May this op NOT be speculated — run on a path that did not run it? An effect, or an op whose
|
|
252
|
+
* placement forbids it.
|
|
253
|
+
*
|
|
254
|
+
* A plain memory read answers no, and that is the answer worth arguing. The consumer is
|
|
255
|
+
* raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
|
|
256
|
+
* inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short circuit
|
|
257
|
+
* re-guards it. Answering yes for plain reads refuses every connective whose arm reads memory,
|
|
258
|
+
* which branch-shortcircuit.test.ts's `refusals` pin. A qualified read is the read the re-guard
|
|
259
|
+
* argument does not cover — it moves an access, not a value — so its placement answers yes.
|
|
260
|
+
*
|
|
261
|
+
* The trapping divides answer no as well, and there the re-guard argument does NOT carry: a hoisted
|
|
262
|
+
* `sdiv` the structurer NAMES becomes an unconditional statement. `reevalUnsafe` answers yes for
|
|
263
|
+
* them, so the pre-update sink is not exposed to it.
|
|
264
|
+
*
|
|
265
|
+
* THE EXEMPTION IS NOT TRANSFERABLE. A consumer asking "would gcc have speculated this arm above a
|
|
266
|
+
* compare", where nothing re-guards anything, must refuse single-load arms (`gcc/jump.c:483`'s
|
|
267
|
+
* `! may_trap_p`); `raise/narrowlocal.ts` asks `reevalUnsafe`, and asking this instead loses
|
|
268
|
+
* `synthetic:mergeldcast:agbcc` its byte match. Ask this only where a C-level re-guard at the new
|
|
269
|
+
* point actually holds. */
|
|
270
|
+
export function speculationUnsafe(op: Op): boolean {
|
|
271
|
+
return effectful(op) || answered(op, 'speculationUnsafe');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Does this op's answer depend on WHERE it runs on one path? An effect (its order against other
|
|
275
|
+
* effects is observable) or a memory read (it answers whichever stores ran before it). */
|
|
276
|
+
export function orderSensitive(op: Op): boolean {
|
|
277
|
+
const sig = sigOf(op);
|
|
278
|
+
return !!sig && (!!sig.effects || !!sig.reads);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** May this op NOT be re-evaluated at another program point — order-sensitive, or trapping? The trap
|
|
282
|
+
* half separates it from `orderSensitive`: it matters only when the new point can be reached on a
|
|
283
|
+
* path the old one was not. A collapsed switch re-renders a test block's ops at their uses, each
|
|
284
|
+
* dominated by the def, so on a subset of the original paths — a narrowing, never a speculation —
|
|
285
|
+
* and asks `orderSensitive` instead. */
|
|
286
|
+
export function reevalUnsafe(op: Op): boolean {
|
|
287
|
+
const sig = sigOf(op);
|
|
288
|
+
return !!sig && (!!sig.effects || !!sig.reads || !!sig.traps);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Drop the `helper` placement from an op a pass moves out of the block the asm called it in, and
|
|
292
|
+
* return the op. The stamp is read as WHERE the call ran, so an op moved elsewhere must stop
|
|
293
|
+
* claiming it and renders as the pure value it computes. raise/shortcircuit.ts hoists a `&&`/`||`
|
|
294
|
+
* guarded arm's body above the branch, and there C's short circuit re-guards the op at its use:
|
|
295
|
+
* agbcc's `if (a > 0 && k / n != 0)` runs the `bl __divsi3` under the first compare, and named at
|
|
296
|
+
* its hoisted def it would divide on the path the `&&` skips. */
|
|
297
|
+
export function forgetHelperPlacement(op: Op): Op {
|
|
298
|
+
if (placedAt(op) === 'helper') {
|
|
299
|
+
const { helper: _, ...rest } = op.attrs;
|
|
300
|
+
op.attrs = rest;
|
|
301
|
+
}
|
|
302
|
+
return op;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** `attrs`, plus every placement stamp of `from`, for an op a pass builds to stand for `from`: a
|
|
306
|
+
* memory access keeps its `volatile` and its `declared` stamp, a value op its `helper`. Every op
|
|
307
|
+
* rebuilder carries the stamps through here, so a placement decided once is not lost to a rebuild
|
|
308
|
+
* that forgot to copy it. EVERY stamp, not only the one that places the op: a read carrying both
|
|
309
|
+
* `declared` and `device` that kept only `declared` would be left plain — and deleted as dead — by
|
|
310
|
+
* a later stamp that no longer finds the declaration. */
|
|
311
|
+
export function carryDiscipline(from: Op, attrs: Op['attrs']): Op['attrs'] {
|
|
312
|
+
return stampsOf(from).reduce((acc, p) => ({ ...acc, ...PLACEMENT_ANSWERS[p].carried(from) }), attrs);
|
|
313
|
+
}
|