@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/raise/latch.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// A loop whose back-edge carries a register copy — `add r3, r0, #0` then `b .L6` — lifts to a block
|
|
4
4
|
// holding nothing but that branch, because SSA construction turns the copy into an EDGE ARGUMENT.
|
|
5
5
|
// The loop is unchanged; its latch has just become a separate empty block between the exit test and
|
|
6
|
-
// the header. Loop DISCOVERY still finds the loop — `
|
|
6
|
+
// the header. Loop DISCOVERY still finds the loop — `ir/loops.ts` reads the same back-edge —
|
|
7
7
|
// but the do-while emitter requires the latch to end in a `cond_br`, an empty one ends in `br`, and
|
|
8
8
|
// the test is at the bottom so the `while` form is unavailable too. The back-edge survives to the
|
|
9
9
|
// `onStack` refusal and the whole function declines with "unrecovered back-edge".
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
// `if` (declining loud on the hazards). The guard therefore survives the fold either way; what the
|
|
25
25
|
// gate preserves is the SHAPE — a forward trampoline is not a latch, and folding one re-casts an
|
|
26
26
|
// unrelated branch as a loop guard — which is the `target-dominates` entry's own note below.
|
|
27
|
-
import { Fn, dominators, foldWriteOrder } from '../ir/core';
|
|
27
|
+
import { Block, Fn, dominators, foldWriteOrder, predecessors, reachForward, successorsOf } from '../ir/core';
|
|
28
|
+
import { effectful } from '../ir/discipline';
|
|
29
|
+
import { analyzeLoops } from '../ir/loops';
|
|
28
30
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
29
31
|
|
|
30
32
|
/** What the gates below judge: one candidate block and the block its `br` goes to. */
|
|
31
|
-
interface LatchCandidate {
|
|
33
|
+
export interface LatchCandidate {
|
|
32
34
|
block: Fn['blocks'][number];
|
|
33
35
|
target: Fn['blocks'][number];
|
|
34
36
|
dominatesBlock: boolean;
|
|
@@ -80,9 +82,9 @@ export const LATCH_GATES: readonly Gate<LatchCandidate>[] = [
|
|
|
80
82
|
* SUCCESSOR changes, so an edge that was not a back-edge becomes one. (Dominator sets themselves
|
|
81
83
|
* only ever shrink — removing a block removes it from every set it was in.)
|
|
82
84
|
*
|
|
83
|
-
* A predecessor that already branches to the target ends up with two edges into one block
|
|
84
|
-
*
|
|
85
|
-
*
|
|
85
|
+
* A predecessor that already branches to the target ends up with two edges into one block, each
|
|
86
|
+
* carrying its own arguments. Where that predecessor has no other edge the loop has no exit, and
|
|
87
|
+
* loop recovery spells it `while (1)` with each edge's own update copy.
|
|
86
88
|
*/
|
|
87
89
|
export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[] = LATCH_GATES): number {
|
|
88
90
|
let folded = 0;
|
|
@@ -124,3 +126,157 @@ export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[]
|
|
|
124
126
|
folded++;
|
|
125
127
|
}
|
|
126
128
|
}
|
|
129
|
+
|
|
130
|
+
// ── EMPTY EXIT TRAMPOLINES ──────────────────────────────────────────────────────────────────────
|
|
131
|
+
//
|
|
132
|
+
// The same empty block on a loop's way OUT. `for (…) { if (t[i] == k) { r = i; break; } }` keeps
|
|
133
|
+
// `r` in a register the break copies into (`add r2, r1, #0`), and the copy becomes an edge argument
|
|
134
|
+
// of a block that holds nothing but its `br` to the code after the loop — beside a second exit edge
|
|
135
|
+
// (the loop's own test failing) that reaches that code directly. The loop then has two exit targets
|
|
136
|
+
// and none of them is a `return`, so every loop form refuses it, and the back-edge declines loud at
|
|
137
|
+
// `onStack`. Which exit edge carries the block is the compiler's choice: agbcc puts it on the break
|
|
138
|
+
// (`synthetic:break_value`) or on the latch's own exit with the break going direct (kleod
|
|
139
|
+
// `sub_0801E664`).
|
|
140
|
+
//
|
|
141
|
+
// The second exit need not land on the target itself. ido, gcc2.7.2kmc and mwcc lay an `else` the
|
|
142
|
+
// loop sits beside between the loop and its join — `if (f) { for (..) if (a(i) == k) { id = i + 100;
|
|
143
|
+
// break; } } else id = f + 26; out(id);` (`synthetic:break_elsetail`) — so the loop's own exit is an
|
|
144
|
+
// empty `b`/`j` over the `else`, and the break reaches the join only through the code its arm runs.
|
|
145
|
+
// Folding the block there leaves the loop two exit targets all the same — the break arm's head is
|
|
146
|
+
// one. What it buys is that the loop's own exit becomes the join the break arm runs on into, which
|
|
147
|
+
// is the exit a `break` that runs code has to leave for (`codeBreakArm`'s region must reach it). So
|
|
148
|
+
// the scope gate asks whether another exit edge's target REACHES the target, going forward and
|
|
149
|
+
// outside the loop's body.
|
|
150
|
+
//
|
|
151
|
+
// Splicing it out re-points each edge into it at its target with its own arguments, and the loop
|
|
152
|
+
// exits to one block: a `break` that carries its copies, which the structurer already spells. The
|
|
153
|
+
// arguments move soundly for the reason the latch's do: the block has no params and one op, so
|
|
154
|
+
// every value its `br` passes dominates it, and therefore the end of each of its predecessors.
|
|
155
|
+
|
|
156
|
+
/** One candidate trampoline: the block, where its `br` goes, and the loops it may leave. */
|
|
157
|
+
export interface ExitTrampoline {
|
|
158
|
+
block: Block;
|
|
159
|
+
target: Block;
|
|
160
|
+
/** the predecessors of `block` */
|
|
161
|
+
preds: readonly Block[];
|
|
162
|
+
/** does some loop hold every predecessor of `block` but not `block` itself, and leave by another
|
|
163
|
+
* edge for `target` as well — landing on it, or on a block that reaches it forward without
|
|
164
|
+
* re-entering the loop (`reachForward`)? */
|
|
165
|
+
sharedExit: boolean;
|
|
166
|
+
/** does every path from `target` reach a `ret` without an effect on the way? */
|
|
167
|
+
pureReturn: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const EXIT_TRAMPOLINE_GATES: readonly Gate<ExitTrampoline>[] = [
|
|
171
|
+
{
|
|
172
|
+
id: 'trampoline-has-params',
|
|
173
|
+
why: 'a param is a join: the br args may read it, and they would not exist at a predecessor',
|
|
174
|
+
// the fold then hands a predecessor a value it does not dominate, which `verify` refuses loud
|
|
175
|
+
sound: false,
|
|
176
|
+
rejects: (c) => c.block.params.length > 0,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'trampoline-does-work',
|
|
180
|
+
why: 'the sole terminator must be an unconditional `br`: folding a `cond_br` block discards its second successor',
|
|
181
|
+
sound: true,
|
|
182
|
+
guardedBy: 'latch.test.ts: an exit block whose sole op is a cond_br is never folded',
|
|
183
|
+
rejects: (c) => c.block.ops[0]?.opcode !== 'br',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'trampoline-self-branch',
|
|
187
|
+
why: 'a block branching to itself is an infinite loop rather than a trampoline',
|
|
188
|
+
sound: false,
|
|
189
|
+
rejects: (c) => c.target === c.block,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
// The SCOPE of the fold, not its soundness: an exit edge's empty block is harmless to the
|
|
193
|
+
// structurer wherever it is the loop's one way out, and folding it there would only re-spell
|
|
194
|
+
// functions that already lift.
|
|
195
|
+
id: 'trampoline-not-a-second-exit',
|
|
196
|
+
why: 'only a loop that also leaves for the same target by another edge, or for a block that runs on into it, gains from the fold an exit every edge out can leave for',
|
|
197
|
+
sound: false,
|
|
198
|
+
rejects: (c) => !c.sharedExit,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
// A target that only returns already lifts: the structurer takes the edge through the
|
|
202
|
+
// trampoline as an early `return` and copies the pure tail into it. Kept that way, so the fold
|
|
203
|
+
// re-spells no function that lifted before it.
|
|
204
|
+
id: 'trampoline-returns-already',
|
|
205
|
+
why: 'an exit that only returns is an early `return` arm, which the structurer spells without the fold',
|
|
206
|
+
sound: false,
|
|
207
|
+
rejects: (c) => c.pureReturn,
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
/** Every path from `b` ends in a `ret` and runs no effect on the way. */
|
|
212
|
+
export function returnsPurely(b: Block): boolean {
|
|
213
|
+
const seen = new Set<Block>();
|
|
214
|
+
for (const stack = [b]; stack.length > 0;) {
|
|
215
|
+
const x = stack.pop()!;
|
|
216
|
+
if (seen.has(x)) {
|
|
217
|
+
continue; // a join two paths share, or a cycle — either way judged on its first visit
|
|
218
|
+
}
|
|
219
|
+
seen.add(x);
|
|
220
|
+
if (x.ops.some(effectful)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const t = x.ops[x.ops.length - 1];
|
|
224
|
+
if (t?.opcode === 'ret') {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (t?.opcode !== 'br' && t?.opcode !== 'cond_br') {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
stack.push(...successorsOf(x));
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Splice out every empty EXIT TRAMPOLINE the gates above admit; returns how many were removed.
|
|
236
|
+
* Iterated, as the latch fold is: one splice can make another block's exit edges shared. */
|
|
237
|
+
export function foldExitTrampolines(fn: Fn, gates: readonly Gate<ExitTrampoline>[] = EXIT_TRAMPOLINE_GATES): number {
|
|
238
|
+
let folded = 0;
|
|
239
|
+
for (;;) {
|
|
240
|
+
const preds = predecessors(fn);
|
|
241
|
+
const dom = dominators(fn);
|
|
242
|
+
const loops = [...analyzeLoops(fn, dom).byHeader.values()];
|
|
243
|
+
const found = fn.blocks.find((b) => {
|
|
244
|
+
const target = b.ops[0]?.successors[0]?.block;
|
|
245
|
+
if (target === undefined) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const into = preds.get(b) ?? [];
|
|
249
|
+
const sharedExit =
|
|
250
|
+
into.length > 0 &&
|
|
251
|
+
loops.some(
|
|
252
|
+
(nl) =>
|
|
253
|
+
!nl.body.has(b) &&
|
|
254
|
+
into.every((p) => nl.body.has(p)) &&
|
|
255
|
+
nl.exitEdges.some((e) => e.to !== b && reachForward(e.to, dom, nl.body).has(target)),
|
|
256
|
+
);
|
|
257
|
+
return (
|
|
258
|
+
firstRejection(gates, { block: b, target, preds: into, sharedExit, pureReturn: returnsPurely(target) }) === null
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
if (!found) {
|
|
262
|
+
return folded;
|
|
263
|
+
}
|
|
264
|
+
const onward = found.ops[0].successors[0];
|
|
265
|
+
for (const b of fn.blocks) {
|
|
266
|
+
let repointed = false;
|
|
267
|
+
for (const op of b.ops) {
|
|
268
|
+
op.successors.forEach((s, i) => {
|
|
269
|
+
if (s.block === found) {
|
|
270
|
+
op.successors[i] = { block: onward.block, args: [...onward.args] };
|
|
271
|
+
repointed = true;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (repointed) {
|
|
276
|
+
foldWriteOrder(fn.writeOrder, found, b);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
fn.blocks = fn.blocks.filter((b) => b !== found);
|
|
280
|
+
folded++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
|
|
72
72
|
import { type IrType, type StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
73
73
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
74
|
+
import { storeSigned } from './storesign';
|
|
74
75
|
import { firstFreeStructIndex } from './structs';
|
|
75
76
|
|
|
76
77
|
/** One variable-index access, resolved onto (base value, member offset). */
|
|
@@ -260,7 +261,9 @@ function stripExt(v: Value, defs: Map<Value, Op>): Value {
|
|
|
260
261
|
}
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
/** `add(x, const)` read as (other operand, constant), or null. The add is commutative.
|
|
264
|
+
/** `add(x, const)` read as (other operand, constant), or null. The add is commutative. ONE link, not
|
|
265
|
+
* a chain, and no `sub`: the shapes this pass reads are a single `add` of a scaled index or of a
|
|
266
|
+
* member offset. A chain of constant displacements is ir/alias.ts `constDisplacementOf`. */
|
|
264
267
|
function constAddend(op: Op, defs: Map<Value, Op>): { of: Value; k: number } | null {
|
|
265
268
|
if (op.opcode !== 'add' || op.operands.length !== 2) {
|
|
266
269
|
return null;
|
|
@@ -389,7 +392,7 @@ function accessesByBase(fn: Fn, defs: Map<Value, Op>): Map<Value, MemberAccess[]
|
|
|
389
392
|
op,
|
|
390
393
|
off: add === null ? 0 : add.k,
|
|
391
394
|
elemSize,
|
|
392
|
-
signed: isLoad ? (op.attrs.signed as boolean) : elemSize
|
|
395
|
+
signed: isLoad ? (op.attrs.signed as boolean) : storeSigned(op, elemSize),
|
|
393
396
|
isLoad,
|
|
394
397
|
index: op.operands[1],
|
|
395
398
|
addr: add === null ? null : op.operands[0],
|
|
@@ -583,8 +586,11 @@ export function recognizeMemberArrays(
|
|
|
583
586
|
a.op.operands[0] = g.base;
|
|
584
587
|
a.op.attrs.memberOff = a.off;
|
|
585
588
|
if (!a.isLoad) {
|
|
586
|
-
//
|
|
587
|
-
//
|
|
589
|
+
// the member's declaration decides how the index node types, so the store spells through
|
|
590
|
+
// the same declaration the load does, over any stamp raise/storesign.ts left. No probed
|
|
591
|
+
// input reaches here with a stamp that disagrees: agbcc lifts a signed narrow constant as
|
|
592
|
+
// `neg`/`shl`, and the MIPS shapes (`p->t[i] = -1`, `-104` beside a u16 read) take a flat
|
|
593
|
+
// cast. One that does should keep its stamp, and the index node cast the cell to it.
|
|
588
594
|
a.op.attrs.signed = g.members.find((m) => m.off === a.off)!.signed;
|
|
589
595
|
}
|
|
590
596
|
}
|
package/src/raise/narrow.ts
CHANGED
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
// A signed and an unsigned right shift of the same value by the same amount differ only in the bits
|
|
24
24
|
// the shift brings in, so `%134` is `sext(%133, 32 - 16)` — the same rewrite, and the reason this
|
|
25
25
|
// pass matches the pair rather than either extension on its own.
|
|
26
|
+
//
|
|
27
|
+
// And a third, where the variable is carried SHIFTED and never extended at all: an `s8` walked down
|
|
28
|
+
// a list, `i = buf[i].next`, is kept as `next << 24` and the test reads it back with `asr #24`.
|
|
29
|
+
// The cast idiom folds that `asr` of the `lsl` into a `sext` of the load, which is the same bits
|
|
30
|
+
// and no longer reads the carried value:
|
|
31
|
+
//
|
|
32
|
+
// %41 = shl %40 {imm=24} what `i` holds next iteration, shifted
|
|
33
|
+
// %42 = sext %40 {8} the loop test
|
|
34
|
+
//
|
|
35
|
+
// `sext(v, 32 - k) === shr_s(shl(v, k), k)`, so `%42` is re-rooted as `shr_s %41 {k}`.
|
|
26
36
|
import { Fn, Op, Value, defOpMap } from '../ir/core';
|
|
27
37
|
import { CAST_WIDTHS, Opcode } from '../ir/opcodes';
|
|
28
38
|
|
|
@@ -41,8 +51,8 @@ const domainOf = (op: Op, low: Opcode, high: Opcode): { key: string; width: numb
|
|
|
41
51
|
return null;
|
|
42
52
|
};
|
|
43
53
|
|
|
44
|
-
/** Re-root a sign extension on the co-existing zero extension of the same bits
|
|
45
|
-
* of rewrites. */
|
|
54
|
+
/** Re-root a sign extension on the co-existing zero extension of the same bits, or on the carried
|
|
55
|
+
* left shift that holds them. Returns the number of rewrites. */
|
|
46
56
|
export function rerootNarrowReads(fn: Fn): number {
|
|
47
57
|
// LOOP-CARRIED edge arguments only. The rewrite is sound for ANY co-existing pair — it only
|
|
48
58
|
// re-associates two extensions of the same bits — so this gate is about what it BUYS, not about
|
|
@@ -87,6 +97,8 @@ export function rerootNarrowReads(fn: Fn): number {
|
|
|
87
97
|
// else this could be a use before a def. That is the shape the idiom emits — one increment,
|
|
88
98
|
// both extensions of it, in the block that computes it.
|
|
89
99
|
const unsigned = new Map<Value, Map<string, Value>>();
|
|
100
|
+
// …and the carried left shifts, by operand and then by amount, under the same two rules
|
|
101
|
+
const shifted = new Map<Value, Map<number, Value>>();
|
|
90
102
|
for (const op of b.ops) {
|
|
91
103
|
const u = domainOf(op, 'zext', 'shr_u');
|
|
92
104
|
if (u !== null && carried.has(op.results[0])) {
|
|
@@ -95,6 +107,21 @@ export function rerootNarrowReads(fn: Fn): number {
|
|
|
95
107
|
unsigned.set(op.operands[0], byDomain);
|
|
96
108
|
continue;
|
|
97
109
|
}
|
|
110
|
+
if (op.opcode === 'shl' && typeof op.attrs.imm === 'number' && carried.has(op.results[0])) {
|
|
111
|
+
const byAmount = shifted.get(op.operands[0]) ?? new Map<number, Value>();
|
|
112
|
+
byAmount.set(op.attrs.imm, op.results[0]);
|
|
113
|
+
shifted.set(op.operands[0], byAmount);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const k = 32 - (op.attrs.width as number);
|
|
117
|
+
const sh = op.opcode === 'sext' && CAST_WIDTHS.has(32 - k) ? shifted.get(op.operands[0])?.get(k) : undefined;
|
|
118
|
+
if (sh !== undefined) {
|
|
119
|
+
op.opcode = 'shr_s';
|
|
120
|
+
op.operands = [sh];
|
|
121
|
+
op.attrs = { imm: k };
|
|
122
|
+
rewritten++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
98
125
|
const sgn = domainOf(op, 'sext', 'shr_s');
|
|
99
126
|
if (sgn === null) {
|
|
100
127
|
continue;
|
package/src/raise/narrowlocal.ts
CHANGED
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
// u16 v; … *out = v; zext16 4 0 MATCH
|
|
59
59
|
// s32 v; … *out = (u16)v; zext16 0 MATCH 4
|
|
60
60
|
//
|
|
61
|
+
// A THIRD SPELLING of rows three and four is the `/escape-home` variation's
|
|
62
|
+
// (structure/analysis.ts `escapingExtensions`): keep `s32 v` and give the truncation a NAME where
|
|
63
|
+
// it happened, `t = (u16)v`. It is not an alternative to this pass — it judges an extension's
|
|
64
|
+
// result where this one judges a block PARAMETER — and it is a ranked candidate where this is a
|
|
65
|
+
// decision, so neither can take the other's input.
|
|
66
|
+
//
|
|
61
67
|
// Rows one and two are DECIDABLE by the carrier's own readers: a `zext_w` read by a `sext_w` is the
|
|
62
68
|
// write-back truncation followed by the declaration's own sign extension, and no cast on a wide
|
|
63
69
|
// local writes that pair. Rows three and four are the same IR in this pass's READER vocabulary —
|
|
@@ -131,7 +137,8 @@
|
|
|
131
137
|
// admits), and `runPreRecovery` reads the shape ahead of the pass that manufactures it —
|
|
132
138
|
// `mergeShapes`.
|
|
133
139
|
import { type Block, type Fn, type Op, type Value, defOpMap, predecessors, replaceAllUsesWith } from '../ir/core';
|
|
134
|
-
import {
|
|
140
|
+
import { reevalUnsafe } from '../ir/discipline';
|
|
141
|
+
import { CAST_WIDTHS } from '../ir/opcodes';
|
|
135
142
|
import { T } from '../ir/types';
|
|
136
143
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
137
144
|
|
|
@@ -350,8 +357,8 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
|
|
|
350
357
|
* • a MEMORY READ is one op and is NOT one speculatable SET. `gcc/rtlanal.c:1770-1771` sends a
|
|
351
358
|
* MEM to `rtx_addr_can_trap_p` and `:144-147` says an address held in a plain pseudo CAN trap,
|
|
352
359
|
* so `jump.c` never hoists a load and the diamond survives under BOTH spellings, carrying no
|
|
353
|
-
* information. `
|
|
354
|
-
* refuses the trapping divides too, at `rtlanal.c:1774-1784`); `
|
|
360
|
+
* information. `reevalUnsafe` answers this (effects, reads, or traps — `may_trap_p`
|
|
361
|
+
* refuses the trapping divides too, at `rtlanal.c:1774-1784`); `speculationUnsafe` does NOT,
|
|
355
362
|
* because it omits reads for `raise/shortcircuit.ts`, whose arm C's own `&&` re-guards, and
|
|
356
363
|
* that exemption does not transfer to speculation above a compare.
|
|
357
364
|
* • a CONSTANT is not free. `v = a + 0x12345` is a literal-pool `ldr` plus the `add` — two
|
|
@@ -382,9 +389,7 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
|
|
|
382
389
|
* clause in that table is `sound: false`), so the shared conservative predicate is the right one
|
|
383
390
|
* until a row asks for the cost model neither pass has. */
|
|
384
391
|
export function armIsOneSet(b: Block): boolean {
|
|
385
|
-
return (
|
|
386
|
-
!b.ops.some((op) => REEVAL_UNSAFE_OPS.has(op.opcode)) && b.ops.filter((op) => op.results.length > 0).length === 1
|
|
387
|
-
);
|
|
392
|
+
return !b.ops.some(reevalUnsafe) && b.ops.filter((op) => op.results.length > 0).length === 1;
|
|
388
393
|
}
|
|
389
394
|
|
|
390
395
|
/** The join shape of every block, read ONCE off the IR it is handed.
|