@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/raise/offsetnames.ts
CHANGED
|
@@ -100,13 +100,20 @@
|
|
|
100
100
|
// the map spells two ways at either end, a width the map does not state, a width that disagrees
|
|
101
101
|
// with the access, a store onto a `const` name — refuse NOTHING here and are pinned by
|
|
102
102
|
// `offset-names.test.ts` alone.
|
|
103
|
+
//
|
|
104
|
+
// AND IT IS WHERE A LIFT'S NAMES BECOME FINAL, so it is also where `cellSpelledTwice` asks whether
|
|
105
|
+
// any byte is now reached under two spellings — the premise `ir/alias.ts` reads two names as two
|
|
106
|
+
// objects on — and declines the function when one is. A refusal above is one way to get there (a
|
|
107
|
+
// walk left as `base + K` past the base's end spells its neighbour's byte through the base's
|
|
108
|
+
// name), not the only one, which is why the question is asked of the accesses and not of the gates.
|
|
103
109
|
import { constDisplacementOf } from '../ir/alias';
|
|
104
|
-
import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
|
|
110
|
+
import { type Block, type Fn, type Op, type Value, defOpMap, successorsOf } from '../ir/core';
|
|
105
111
|
import { verify } from '../ir/verify';
|
|
106
112
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
107
113
|
import { dce } from '../pattern/engine';
|
|
108
114
|
import { type SymbolInfo, type SymbolMap, accessSignedness, lookupSymbol } from '../symbols';
|
|
109
|
-
import { type StampBehaviors, stampDeclaredVolatileUnder } from './declared-volatile';
|
|
115
|
+
import { type StampBehaviors, joinIncoming, stampDeclaredVolatileUnder } from './declared-volatile';
|
|
116
|
+
import { RaiseUnsupportedError } from './errors';
|
|
110
117
|
|
|
111
118
|
/** One `add`/`sub` whose address the gates judge. Built only for an op that already resolves to a
|
|
112
119
|
* named base plus a NON-ZERO constant — a zero offset names the base itself, which the pool path
|
|
@@ -231,8 +238,10 @@ export const OFFSET_NAME_GATES: readonly Gate<OffsetAddress>[] = [
|
|
|
231
238
|
// The mirror of `base-address-ambiguous`, and it is not the same question asked twice: that one
|
|
232
239
|
// is one NAME the map places at two addresses, this one is one ADDRESS the map gives several
|
|
233
240
|
// names — aliases, or typed views of one RAM region, which `SymbolMap`'s `SymbolInfo[]` exists
|
|
234
|
-
// to carry. `lookupSymbol` answers with
|
|
235
|
-
//
|
|
241
|
+
// to carry. `lookupSymbol` answers with ONE of them — `objectAt`'s per-address pick, usually the
|
|
242
|
+
// first, the sized object behind an unsized data marker otherwise — so the width the two access
|
|
243
|
+
// rules check and the `const` the store rule checks would otherwise be read off whichever alias
|
|
244
|
+
// that pick lands on. Passing this gate means every alias agrees with the first on all of them.
|
|
236
245
|
{
|
|
237
246
|
id: 'target-address-ambiguous',
|
|
238
247
|
why: 'the map gives the walked-to address several names that do not agree, so which one this spells is arbitrary',
|
|
@@ -333,7 +342,7 @@ function namedBase(v: Value, defOf: Map<Value, Op>): { sym: string; code: boolea
|
|
|
333
342
|
/** Every `add`/`sub` in `fn` that resolves to a named base plus a non-zero constant, with the
|
|
334
343
|
* context the gates judge. Shared by the rewrite and by `offsetNameRefusals` so the two cannot
|
|
335
344
|
* disagree about which sites exist. */
|
|
336
|
-
function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: OffsetAddress }[] {
|
|
345
|
+
function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: OffsetAddress; at: number }[] {
|
|
337
346
|
const defOf = defOpMap(fn);
|
|
338
347
|
const byName = addressesByName(symbols);
|
|
339
348
|
const written = new Set<Value>();
|
|
@@ -388,7 +397,7 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
|
|
|
388
397
|
}
|
|
389
398
|
}
|
|
390
399
|
}
|
|
391
|
-
const out: { op: Op; sym: string; addr: OffsetAddress }[] = [];
|
|
400
|
+
const out: { op: Op; sym: string; addr: OffsetAddress; at: number }[] = [];
|
|
392
401
|
for (const b of fn.blocks) {
|
|
393
402
|
for (const op of b.ops) {
|
|
394
403
|
if (op.opcode !== 'add' && op.opcode !== 'sub') {
|
|
@@ -410,6 +419,7 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
|
|
|
410
419
|
out.push({
|
|
411
420
|
op,
|
|
412
421
|
sym: base.sym,
|
|
422
|
+
at: at.addr + base.offset,
|
|
413
423
|
addr: {
|
|
414
424
|
base: at.info,
|
|
415
425
|
ambiguous: at.twice,
|
|
@@ -428,15 +438,400 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
|
|
|
428
438
|
return out;
|
|
429
439
|
}
|
|
430
440
|
|
|
441
|
+
/** Every address the map carries a name at, and whether the name is a MACRO — an address-cast
|
|
442
|
+
* macro expands to the numeric literal, so to the compiler it is the same spelling as a raw
|
|
443
|
+
* constant address, not an object of its own. Cached per map (rebuilt when its entry count moves),
|
|
444
|
+
* because the pass runs once per lift setting of every row. */
|
|
445
|
+
interface Places {
|
|
446
|
+
readonly addrs: number[];
|
|
447
|
+
macro: boolean;
|
|
448
|
+
}
|
|
449
|
+
const placesCache = new WeakMap<SymbolMap, { entries: number; places: Map<string, Places> }>();
|
|
450
|
+
|
|
451
|
+
function placesByName(symbols: SymbolMap): Map<string, Places> {
|
|
452
|
+
const cached = placesCache.get(symbols);
|
|
453
|
+
if (cached && cached.entries === symbols.size) {
|
|
454
|
+
return cached.places;
|
|
455
|
+
}
|
|
456
|
+
const places = new Map<string, Places>();
|
|
457
|
+
for (const [addr, infos] of symbols) {
|
|
458
|
+
for (const info of infos) {
|
|
459
|
+
const p = places.get(info.name) ?? places.set(info.name, { addrs: [], macro: false }).get(info.name)!;
|
|
460
|
+
if (!p.addrs.includes(addr)) {
|
|
461
|
+
p.addrs.push(addr);
|
|
462
|
+
}
|
|
463
|
+
p.macro ||= info.macroBody !== undefined;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
placesCache.set(symbols, { entries: symbols.size, places });
|
|
467
|
+
return places;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** What one access's address may stand for: the SPELLING the emitted C reaches it through (a
|
|
471
|
+
* global's name, or `null` for a numeric address — a raw constant or a macro), the OBJECT
|
|
472
|
+
* addresses that spelling may denote, and the constant displacement off them. */
|
|
473
|
+
interface Spelled {
|
|
474
|
+
readonly spelling: string | null;
|
|
475
|
+
readonly objects: readonly number[];
|
|
476
|
+
readonly k: number;
|
|
477
|
+
/** the name is asmlift's reading of the map (`gaddr`'s `fromMap`), not the asm's own spelling —
|
|
478
|
+
* read by ONE CELL PER SPELLING only; whether two spellings forward is the compiler's fact */
|
|
479
|
+
readonly fromMap: boolean;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** One access whose bytes the map lets this pass place, under ONE of the spellings its address may
|
|
483
|
+
* stand for (an address that arrives through a join may stand for several, one entry each). */
|
|
484
|
+
interface PlacedAccess extends Spelled {
|
|
485
|
+
/** the access, so a pair is never one op against itself */
|
|
486
|
+
readonly op: Op;
|
|
487
|
+
readonly ranges: readonly (readonly [lo: number, hi: number])[];
|
|
488
|
+
readonly writes: boolean;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** The 32-bit integer `v` denotes when every leaf under it is a constant, or null. Integer
|
|
492
|
+
* arithmetic over constants, and a join whose every in-edge brings one value, fold, because the C
|
|
493
|
+
* compiler folds them in the emitted C whatever this pipeline's own constant pass does: an index
|
|
494
|
+
* built in two steps (`movs r2, #2; adds r2, #3`), or `lsls r2, #0`, is `[5]` to agbcc.
|
|
495
|
+
* Memoised per call of {@link cellSpelledTwice}; a value met again on its own walk (a loop-carried
|
|
496
|
+
* join) does not fold. */
|
|
497
|
+
function constantFolder(defOf: Map<Value, Op>, incoming: Map<Value, Value[]>): (v: Value) => number | null {
|
|
498
|
+
const memo = new Map<Value, number | null>();
|
|
499
|
+
const onWalk = new Set<Value>();
|
|
500
|
+
const fold = (v: Value): number | null => {
|
|
501
|
+
const known = memo.get(v);
|
|
502
|
+
if (known !== undefined || memo.has(v)) {
|
|
503
|
+
return known ?? null;
|
|
504
|
+
}
|
|
505
|
+
if (onWalk.has(v)) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
onWalk.add(v);
|
|
509
|
+
const out = foldOnce(v);
|
|
510
|
+
onWalk.delete(v);
|
|
511
|
+
memo.set(v, out);
|
|
512
|
+
return out;
|
|
513
|
+
};
|
|
514
|
+
const foldOnce = (v: Value): number | null => {
|
|
515
|
+
const d = defOf.get(v);
|
|
516
|
+
if (d === undefined) {
|
|
517
|
+
const ins = [...new Set(incoming.get(v) ?? [])].filter((a) => a !== v);
|
|
518
|
+
const each = ins.map(fold);
|
|
519
|
+
return each.length > 0 && each.every((x) => x !== null && x === each[0]) ? each[0] : null;
|
|
520
|
+
}
|
|
521
|
+
if (d.opcode === 'const') {
|
|
522
|
+
return typeof d.attrs.value === 'number' ? d.attrs.value >> 0 : null;
|
|
523
|
+
}
|
|
524
|
+
const a = d.operands.length > 0 ? fold(d.operands[0]) : null;
|
|
525
|
+
if (a === null) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
if (d.operands.length === 1) {
|
|
529
|
+
const imm = d.attrs.imm;
|
|
530
|
+
const w = d.attrs.width;
|
|
531
|
+
switch (d.opcode) {
|
|
532
|
+
case 'not':
|
|
533
|
+
return ~a;
|
|
534
|
+
case 'neg':
|
|
535
|
+
return -a >> 0;
|
|
536
|
+
case 'zext':
|
|
537
|
+
return typeof w === 'number' && w < 32 ? (a & ((1 << w) - 1)) >>> 0 : a;
|
|
538
|
+
case 'sext':
|
|
539
|
+
return typeof w === 'number' && w < 32 ? (a << (32 - w)) >> (32 - w) : a;
|
|
540
|
+
}
|
|
541
|
+
if (typeof imm !== 'number') {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
return shift(d.opcode, a, imm);
|
|
545
|
+
}
|
|
546
|
+
const b = d.operands.length === 2 ? fold(d.operands[1]) : null;
|
|
547
|
+
if (b === null) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
switch (d.opcode) {
|
|
551
|
+
case 'add':
|
|
552
|
+
return (a + b) >> 0;
|
|
553
|
+
case 'sub':
|
|
554
|
+
return (a - b) >> 0;
|
|
555
|
+
case 'mul':
|
|
556
|
+
return Math.imul(a, b);
|
|
557
|
+
case 'and':
|
|
558
|
+
return a & b;
|
|
559
|
+
case 'or':
|
|
560
|
+
return a | b;
|
|
561
|
+
case 'xor':
|
|
562
|
+
return a ^ b;
|
|
563
|
+
}
|
|
564
|
+
return shift(d.opcode, a, b);
|
|
565
|
+
};
|
|
566
|
+
const shift = (opcode: string, a: number, n: number): number | null => {
|
|
567
|
+
if (n < 0 || n > 31) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
switch (opcode) {
|
|
571
|
+
case 'shl':
|
|
572
|
+
return a << n;
|
|
573
|
+
case 'shr_u':
|
|
574
|
+
return (a >>> n) >> 0;
|
|
575
|
+
case 'shr_s':
|
|
576
|
+
return a >> n;
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
};
|
|
580
|
+
return fold;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** The spellings an address stands for, through defs and joins: a literal (or a literal plus
|
|
584
|
+
* constants) is a numeric address, a `gaddr` plus constants its name — at the one map address
|
|
585
|
+
* the name was read at (`mapAddr`), or at every address the map places it at — and a block
|
|
586
|
+
* parameter every in-edge's value, so an address the raising tower will merge across a join (two
|
|
587
|
+
* pool loads of one word, a load and its `adds #0` copy) is placed as the merge spells it. An
|
|
588
|
+
* in-edge that passes the parameter back unchanged adds nothing; one that passes it MOVED (a
|
|
589
|
+
* pointer stepped round a loop) reaches bytes no constant names. Null where any reaching value
|
|
590
|
+
* is not placed: a parameter, a frame slot, a runtime index, a name the map does not hold. Every
|
|
591
|
+
* constant here is read through `fold` ({@link constantFolder}), as the C compiler will read it. */
|
|
592
|
+
function spellingsOf(
|
|
593
|
+
v: Value,
|
|
594
|
+
defOf: Map<Value, Op>,
|
|
595
|
+
incoming: Map<Value, Value[]>,
|
|
596
|
+
places: Map<string, Places>,
|
|
597
|
+
fold: (v: Value) => number | null,
|
|
598
|
+
): Spelled[] | null {
|
|
599
|
+
// A parameter's answer is memoised per displacement once it is known not to lean on a parameter
|
|
600
|
+
// still being walked (`open`), so a chain of joins is walked once, not once per path through it.
|
|
601
|
+
const memo = new Map<Value, Map<number, Spelled[] | null>>();
|
|
602
|
+
const onWalk = new Map<Value, number>();
|
|
603
|
+
let open = false;
|
|
604
|
+
const walk = (x: Value, k: number): Spelled[] | null => {
|
|
605
|
+
// a literal however it was built, a join of one literal included
|
|
606
|
+
const literal = fold(x);
|
|
607
|
+
if (literal !== null) {
|
|
608
|
+
return [{ spelling: null, objects: [(literal + k) >>> 0], k: 0, fromMap: false }];
|
|
609
|
+
}
|
|
610
|
+
const disp = constDisplacementOf(defOf, x, fold);
|
|
611
|
+
const root = disp?.root ?? x;
|
|
612
|
+
const kk = k + (disp?.k ?? 0);
|
|
613
|
+
const d = defOf.get(root);
|
|
614
|
+
if (d?.opcode === 'gaddr') {
|
|
615
|
+
const sym = d.attrs.sym as string;
|
|
616
|
+
const at = places.get(sym);
|
|
617
|
+
if (at === undefined) {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
const objects = typeof d.attrs.mapAddr === 'number' ? [d.attrs.mapAddr] : at.addrs;
|
|
621
|
+
return [{ spelling: at.macro ? null : sym, objects, k: kk, fromMap: d.attrs.fromMap === true }];
|
|
622
|
+
}
|
|
623
|
+
const ins = d === undefined ? incoming.get(root) : undefined;
|
|
624
|
+
if (ins === undefined) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
const known = memo.get(root)?.get(kk);
|
|
628
|
+
if (known !== undefined) {
|
|
629
|
+
return known;
|
|
630
|
+
}
|
|
631
|
+
const seenAt = onWalk.get(root);
|
|
632
|
+
if (seenAt !== undefined) {
|
|
633
|
+
open = true;
|
|
634
|
+
return seenAt === kk ? [] : null;
|
|
635
|
+
}
|
|
636
|
+
const outerOpen = open;
|
|
637
|
+
open = false;
|
|
638
|
+
onWalk.set(root, kk);
|
|
639
|
+
let out: Spelled[] | null = [];
|
|
640
|
+
for (const a of new Set(ins)) {
|
|
641
|
+
const each = walk(a, kk);
|
|
642
|
+
if (each === null) {
|
|
643
|
+
out = null;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
out.push(...each);
|
|
647
|
+
}
|
|
648
|
+
onWalk.delete(root);
|
|
649
|
+
if (!open) {
|
|
650
|
+
(memo.get(root) ?? memo.set(root, new Map()).get(root)!).set(kk, out);
|
|
651
|
+
}
|
|
652
|
+
open ||= outerOpen;
|
|
653
|
+
return out;
|
|
654
|
+
};
|
|
655
|
+
const all = walk(v, 0);
|
|
656
|
+
return all === null || all.length === 0 ? null : all;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Every spelling one memory access may be placed under, with the byte range at each base that
|
|
660
|
+
* spelling may denote; empty where its address is not placed ({@link spellingsOf}). */
|
|
661
|
+
function placeAccess(
|
|
662
|
+
op: Op,
|
|
663
|
+
defOf: Map<Value, Op>,
|
|
664
|
+
incoming: Map<Value, Value[]>,
|
|
665
|
+
places: Map<string, Places>,
|
|
666
|
+
fold: (v: Value) => number | null,
|
|
667
|
+
): PlacedAccess[] {
|
|
668
|
+
let off: number;
|
|
669
|
+
let width: number;
|
|
670
|
+
if (op.opcode === 'load' || op.opcode === 'store') {
|
|
671
|
+
off = op.attrs.off as number;
|
|
672
|
+
width = op.attrs.width as number;
|
|
673
|
+
} else if (op.opcode === 'aload' || op.opcode === 'astore') {
|
|
674
|
+
const index = fold(op.operands[1]);
|
|
675
|
+
if (index === null) {
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
width = op.attrs.elemSize as number;
|
|
679
|
+
off = index * width;
|
|
680
|
+
} else {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
const writes = op.opcode === 'store' || op.opcode === 'astore';
|
|
684
|
+
// A literal is compared as the UNSIGNED address the map keys by: MIPS and PPC materialize
|
|
685
|
+
// `0x80100000` as a negative 32-bit constant (`spellingsOf` takes `>>> 0`).
|
|
686
|
+
return (spellingsOf(op.operands[0], defOf, incoming, places, fold) ?? []).map((sp) => ({
|
|
687
|
+
...sp,
|
|
688
|
+
op,
|
|
689
|
+
writes,
|
|
690
|
+
ranges: sp.objects.map((a) => [a + sp.k + off, a + sp.k + off + width] as const),
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** The behaviours the pass reads: the declared-volatile stamp's, and whether this compiler RELOADS
|
|
695
|
+
* a cell stored through one spelling and read through another (`compilerBehaviors`
|
|
696
|
+
* `reloadsAcrossSpellings`; absent ⇒ it forwards). */
|
|
697
|
+
export type OffsetNameBehaviors = StampBehaviors & {
|
|
698
|
+
readonly reloadsAcrossSpellings?: boolean;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
/** ONE SPELLING PER CELL, AND ONE CELL PER SPELLING — the premise `ir/alias.ts` reads names on:
|
|
702
|
+
* the first pair of this function's accesses that breaks it, or null when none does.
|
|
703
|
+
*
|
|
704
|
+
* ONE SPELLING PER CELL. Two different names are two objects to every C compiler, and so are a
|
|
705
|
+
* name and a numeric address: agbcc forwards a value stored through one past a store made
|
|
706
|
+
* through the other, where the machine code reads memory back (`sRandCount = 5; ((u8
|
|
707
|
+
* *)&sUnknown)[4] = 7; return sRandCount;` compiles to `mov r0, #5`). Several places decide a
|
|
708
|
+
* spelling — the pool promotion's per-address pick and its interior attribution, this pass's
|
|
709
|
+
* walk and the refusals that leave a walk as `base + K`, the raw constant where the map holds
|
|
710
|
+
* nothing — and none of them sees the others' accesses. So the question is asked once, after the
|
|
711
|
+
* last of them, over the BYTES each access touches rather than over the map's extents: a map that
|
|
712
|
+
* lays one object over another is harmless until two accesses spell one byte two ways, and an
|
|
713
|
+
* access walked past an object's declared end, or a raw constant beside an unsized name, reaches
|
|
714
|
+
* a byte twice with no overlap in the map at all. A pair declines when one of the two is a store
|
|
715
|
+
* (two LOADS leave no value to forward) and the compiler forwards across two spellings.
|
|
716
|
+
*
|
|
717
|
+
* Which compilers forward is a compiler fact, not a property of who chose the spelling. agbcc and
|
|
718
|
+
* gcc 2.7.2 forward, so an asm that went back to memory proves the source was NOT those two
|
|
719
|
+
* spellings, and every such pair declines. IDO 7.1 reloads (`reloadsAcrossSpellings`) across a
|
|
720
|
+
* name and a literal, two externs, two statics and an array element alike — but only a read that
|
|
721
|
+
* FOLLOWS the store: it treats two names as two objects, so a read of one placed BEFORE a byte
|
|
722
|
+
* store through the other is sunk past it (`v0 = D_80100004; ((u8 *)&D_80100000)[5] = 7; return
|
|
723
|
+
* v0;` is `sb t6, 5(at); jr ra; lw v0, 0(v1)`, the word read with byte 5 already 7). So the pair
|
|
724
|
+
* lifts there only in an order the compiler is measured to keep ({@link keptInOrder}): both in
|
|
725
|
+
* one block outside any loop, the store first — whether the asm spelled both (two relocations, a
|
|
726
|
+
* relocation and a literal) or asmlift chose one off the map (a promoted pool word, an interior
|
|
727
|
+
* attribution, a named walk: `gaddr`'s `fromMap`). Any other order declines.
|
|
728
|
+
*
|
|
729
|
+
* ONE CELL PER SPELLING. A name the map places at several addresses (one file static per unit)
|
|
730
|
+
* read off the map at two different ones is two objects the C would spell as one, which the
|
|
731
|
+
* compiler forwards across or reads once (`sFiller = 5; return sFiller;` where the asm reads
|
|
732
|
+
* another static's cell). It declines whenever one of the two is a map read — two relocations of
|
|
733
|
+
* one name are the asm's own one object — with or without a store.
|
|
734
|
+
*
|
|
735
|
+
* An address is placed through defs and joins ({@link spellingsOf}); one that reaches an access
|
|
736
|
+
* under several spellings (`c ? &a : &b`) is placed under each, so a pair is asked of every one.
|
|
737
|
+
* What this cannot place it cannot enforce: a lift with no map, or a name the map does not hold,
|
|
738
|
+
* has no address to compare, and an address the compiler cannot place either — a pointer
|
|
739
|
+
* parameter, a frame slot, a runtime index — is one it assumes may alias (agbcc reloads
|
|
740
|
+
* `sRandCount` after `((u8 *)&sUnknown)[a0] = 7`). */
|
|
741
|
+
export function cellSpelledTwice(fn: Fn, symbols: SymbolMap, reloadsAcrossSpellings = false): string | null {
|
|
742
|
+
const keptInOrder = reloadsAcrossSpellings ? keptInOrderOn(fn) : (): boolean => false;
|
|
743
|
+
const defOf = defOpMap(fn);
|
|
744
|
+
const incoming = joinIncoming(fn);
|
|
745
|
+
const places = placesByName(symbols);
|
|
746
|
+
const fold = constantFolder(defOf, incoming);
|
|
747
|
+
const placed: PlacedAccess[] = [];
|
|
748
|
+
for (const b of fn.blocks) {
|
|
749
|
+
for (const op of b.ops) {
|
|
750
|
+
placed.push(...placeAccess(op, defOf, incoming, places, fold));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const spell = (s: string | null): string => (s === null ? 'a numeric address' : `'${s}'`);
|
|
754
|
+
for (const [i, x] of placed.entries()) {
|
|
755
|
+
for (const y of placed.slice(i + 1)) {
|
|
756
|
+
if (x.op === y.op) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (x.spelling !== null && x.spelling === y.spelling) {
|
|
760
|
+
const oneObject = x.objects.length === 1 && y.objects.length === 1 && x.objects[0] === y.objects[0];
|
|
761
|
+
const several = (places.get(x.spelling)?.addrs.length ?? 0) > 1;
|
|
762
|
+
if (several && (x.fromMap || y.fromMap) && !oneObject) {
|
|
763
|
+
return `'${x.spelling}' names two of the cells the map places it at`;
|
|
764
|
+
}
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (x.spelling === y.spelling || !(x.writes || y.writes)) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (keptInOrder(x.op, y.op)) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
for (const [xl, xh] of x.ranges) {
|
|
774
|
+
for (const [yl, yh] of y.ranges) {
|
|
775
|
+
if (xl < yh && yl < xh) {
|
|
776
|
+
return `byte 0x${Math.max(xl, yl).toString(16)} is reached as ${spell(x.spelling)} and as ${spell(y.spelling)}`;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Whether a compiler that reloads across two spellings (`reloadsAcrossSpellings`) keeps the pair
|
|
786
|
+
* `a`, `b` (at least one a store) in the order the asm has, so the C reads back what the asm read.
|
|
787
|
+
* MEASURED on IDO 7.1 -O2: a store then a read through another name reloads, and two stores keep
|
|
788
|
+
* their order; a read then a store through another name does NOT — the read is sunk past the
|
|
789
|
+
* store. So the pair holds only when both sit in ONE block, the store first (or both stores), and
|
|
790
|
+
* that block is on no cycle: across blocks, or in a loop whose next iteration reads before this
|
|
791
|
+
* one's store, nothing measured says the read stays where the asm put it. */
|
|
792
|
+
function keptInOrderOn(fn: Fn): (a: Op, b: Op) => boolean {
|
|
793
|
+
const at = new Map<Op, { block: Block; index: number }>();
|
|
794
|
+
for (const block of fn.blocks) {
|
|
795
|
+
block.ops.forEach((op, index) => at.set(op, { block, index }));
|
|
796
|
+
}
|
|
797
|
+
const onCycle = (start: Block): boolean => {
|
|
798
|
+
const seen = new Set<Block>();
|
|
799
|
+
const work = successorsOf(start);
|
|
800
|
+
for (let b = work.pop(); b !== undefined; b = work.pop()) {
|
|
801
|
+
if (b === start) {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
if (!seen.has(b)) {
|
|
805
|
+
seen.add(b);
|
|
806
|
+
work.push(...successorsOf(b));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
};
|
|
811
|
+
const isStore = (op: Op): boolean => op.opcode === 'store' || op.opcode === 'astore';
|
|
812
|
+
return (a, b) => {
|
|
813
|
+
const pa = at.get(a);
|
|
814
|
+
const pb = at.get(b);
|
|
815
|
+
if (pa === undefined || pb === undefined || pa.block !== pb.block || onCycle(pa.block)) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
// one of the two is a store, so the pair is in order exactly when the first one is
|
|
819
|
+
return isStore(pa.index < pb.index ? a : b);
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
431
823
|
/** Rewrite every admitted site into the `gaddr` the map names, in place. Returns THE NAMES it
|
|
432
824
|
* spelled, in block order — what a caller has to be told, because a name this pass produced does
|
|
433
825
|
* not rest on the map alone the way a pool-loaded one does: it rests on `addr(base) + K` landing
|
|
434
826
|
* on the map entry it names. `DecompileResult.walkedNames` and the trace's `stage:offsetnames`
|
|
435
|
-
* carry it out; nothing else can tell from a result that a walk was named.
|
|
827
|
+
* carry it out; nothing else can tell from a result that a walk was named.
|
|
828
|
+
*
|
|
829
|
+
* It is also where the lift's names become FINAL, so it declines a function whose accesses reach
|
|
830
|
+
* one byte under two spellings ({@link cellSpelledTwice}), whichever pass named them. */
|
|
436
831
|
export function nameOffsetAddresses(
|
|
437
832
|
fn: Fn,
|
|
438
833
|
symbols: SymbolMap,
|
|
439
|
-
behaviors:
|
|
834
|
+
behaviors: OffsetNameBehaviors,
|
|
440
835
|
gates: readonly Gate<OffsetAddress>[] = OFFSET_NAME_GATES,
|
|
441
836
|
): string[] {
|
|
442
837
|
// Every site is judged against the PRE-REWRITE defs, so a chain's later links resolve through
|
|
@@ -445,7 +840,7 @@ export function nameOffsetAddresses(
|
|
|
445
840
|
for (const s of admitted) {
|
|
446
841
|
s.op.opcode = 'gaddr';
|
|
447
842
|
s.op.operands = [];
|
|
448
|
-
s.op.attrs = { sym: s.addr.target!.name };
|
|
843
|
+
s.op.attrs = { sym: s.addr.target!.name, fromMap: true, mapAddr: s.at >>> 0 };
|
|
449
844
|
}
|
|
450
845
|
if (admitted.length > 0) {
|
|
451
846
|
// An access now names another object, so whether it reads one the map declares volatile is
|
|
@@ -454,6 +849,13 @@ export function nameOffsetAddresses(
|
|
|
454
849
|
dce(fn); // the constants and the walked-off base the rewrite just orphaned
|
|
455
850
|
verify(fn);
|
|
456
851
|
}
|
|
852
|
+
const twice = cellSpelledTwice(fn, symbols, behaviors.reloadsAcrossSpellings === true);
|
|
853
|
+
if (twice !== null) {
|
|
854
|
+
throw new RaiseUnsupportedError(
|
|
855
|
+
`cannot lift '${fn.name}': ${twice} in one function — C the compiler would read as ` +
|
|
856
|
+
`${twice.startsWith('byte') ? 'two objects, forwarding a store through one past the other' : 'one object'}`,
|
|
857
|
+
);
|
|
858
|
+
}
|
|
457
859
|
return admitted.map((s) => s.addr.target!.name);
|
|
458
860
|
}
|
|
459
861
|
|
|
@@ -488,7 +890,7 @@ export function offsetNameRefusals(
|
|
|
488
890
|
* pass up, which three hand-written calls do not — so `offset-names.test.ts` supplies it by
|
|
489
891
|
* wrapping this record and asserting all three reach it. */
|
|
490
892
|
export const OFFSET_NAME_PASS: {
|
|
491
|
-
run: (fn: Fn, symbols: SymbolMap, behaviors:
|
|
893
|
+
run: (fn: Fn, symbols: SymbolMap, behaviors: OffsetNameBehaviors) => string[];
|
|
492
894
|
} = {
|
|
493
895
|
run: (fn, symbols, behaviors) => nameOffsetAddresses(fn, symbols, behaviors),
|
|
494
896
|
};
|
|
@@ -18,7 +18,7 @@ import type { TargetDescription } from '../target';
|
|
|
18
18
|
import { readDeclaredMembers } from './aggmembers';
|
|
19
19
|
import { recognizeArrays } from './arrays';
|
|
20
20
|
import { recognizeConsts } from './const';
|
|
21
|
-
import { recognizeDivPow2 } from './divpow2';
|
|
21
|
+
import { recognizeDivPow2, recognizeSremPow2 } from './divpow2';
|
|
22
22
|
import {
|
|
23
23
|
type PoolOrder,
|
|
24
24
|
type ScaleRecord,
|
|
@@ -109,10 +109,10 @@ export interface PreRecoveryPass {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
|
|
112
|
-
* address-numbering → const-materialize → magic-division → pow2-division →
|
|
113
|
-
* scaled-extension → store-sign → array-legalize → truncated-load → struct-array →
|
|
114
|
-
* struct-pointer → short-circuit → branch-short-circuit → narrow-reads →
|
|
115
|
-
* parameter-width → scaled-extension-restore.
|
|
112
|
+
* address-numbering → const-materialize → magic-division → pow2-division → pow2-remainder →
|
|
113
|
+
* soft-division → scaled-extension → store-sign → array-legalize → truncated-load → struct-array →
|
|
114
|
+
* member-array → struct-pointer → short-circuit → branch-short-circuit → narrow-reads →
|
|
115
|
+
* narrow-local → parameter-width → scaled-extension-restore.
|
|
116
116
|
* See each recognizer's file for the rationale. */
|
|
117
117
|
export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
118
118
|
// FIRST: collapsing duplicate address definitions removes block params every later recognizer
|
|
@@ -141,6 +141,9 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
141
141
|
// ending in `br`. Verified by running it before and after each of them — same result. It sits
|
|
142
142
|
// beside magicdiv so that a reader looking for division recovery finds both together.
|
|
143
143
|
{ id: 'divpow2', run: recognizeDivPow2, dce: true },
|
|
144
|
+
// …and its remainder sibling, IDO's two-test diamond (raise/divpow2.ts `recognizeSremPow2`): no
|
|
145
|
+
// other pass reads a three-edge merge whose last arm takes a power of two off the mask.
|
|
146
|
+
{ id: 'srempow2', run: recognizeSremPow2, dce: true },
|
|
144
147
|
{
|
|
145
148
|
id: 'softdiv',
|
|
146
149
|
run: (fn, _self, _opts, target) => recognizeSoftDiv(fn, target),
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// asmlift — the CFG folds that run last before structuring, after return-sinking:
|
|
2
|
-
// loop's edges into a shape a loop spelling owns, and
|
|
3
|
-
//
|
|
1
|
+
// asmlift — the CFG folds that run last before structuring, after return-sinking: the loop folds
|
|
2
|
+
// rewrite a loop's edges into a shape a loop spelling owns, and the last one takes a parameter off a
|
|
3
|
+
// switch's join (a case fallen into, or the switch's exit). Each is its own stage in the trace, so a
|
|
4
|
+
// re-spelled function names the fold that moved it.
|
|
4
5
|
//
|
|
5
|
-
// A MUTABLE RECORD, not a sum of
|
|
6
|
+
// A MUTABLE RECORD, not a sum of calls, for the reason `PRE_RECOVERY_PASSES` is one: it is the
|
|
6
7
|
// caller-side seam a process outside core swaps to census a fold's gate table (`pnpm bench gates`,
|
|
7
8
|
// apps/benchmark/src/run/gate-census.ts). A static import binding cannot be swapped.
|
|
8
9
|
import { Fn } from '../ir/core';
|
|
9
10
|
import { foldEmptyLatches, foldExitTrampolines } from './latch';
|
|
10
11
|
import { rerollPeeledTests } from './reroll';
|
|
12
|
+
import { recomputeDerivedParams, recomputeRedundantParams } from './unpre';
|
|
11
13
|
import { unthreadLoopExits } from './unthread';
|
|
12
14
|
|
|
13
15
|
export interface PreStructureFold {
|
|
@@ -29,7 +31,13 @@ export interface PreStructureFold {
|
|
|
29
31
|
* trampoline fold reshapes, and sits ahead of the re-roll so a loop it gives one way out is one
|
|
30
32
|
* the re-roll may then read. That place is a precaution: `synthetic:thrbreak:agbcc` and the
|
|
31
33
|
* pointer walk unthread.ts names lift the same C wherever the fold sits, so `unthread.test.ts`
|
|
32
|
-
* pins its position, not a lift that needs it.
|
|
34
|
+
* pins its position, not a lift that needs it. The two parameter recomputes run last, the
|
|
35
|
+
* derived one (`derivedparam`) just ahead of `unpre`, because that is the one place each was
|
|
36
|
+
* measured, and `unpre.test.ts` pins the places, not a lift that needs them. Both MOVE EDGES: a
|
|
37
|
+
* block either one's orphan sweep leaves holding only its `br` is threaded out of the CFG
|
|
38
|
+
* (`threadEmptied`, raise/unpre.ts), its in-edges retargeted and the block removed, so `unpre`
|
|
39
|
+
* reads the CFG `derivedparam` left, and a loop fold moved after either would read a reshaped one
|
|
40
|
+
* it was never measured on. */
|
|
33
41
|
export const PRE_STRUCTURE_FOLDS: PreStructureFold[] = [
|
|
34
42
|
{
|
|
35
43
|
id: 'latchfold',
|
|
@@ -51,4 +59,15 @@ export const PRE_STRUCTURE_FOLDS: PreStructureFold[] = [
|
|
|
51
59
|
title: 'Peeled-test re-rolling (a loop test peeled into the entry becomes the first iteration)',
|
|
52
60
|
run: (fn) => rerollPeeledTests(fn),
|
|
53
61
|
},
|
|
62
|
+
{
|
|
63
|
+
id: 'derivedparam',
|
|
64
|
+
title:
|
|
65
|
+
"Derived-parameter recomputing (a parameter every edge hands one pure op of a sibling's argument becomes that op)",
|
|
66
|
+
run: (fn) => recomputeDerivedParams(fn),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'unpre',
|
|
70
|
+
title: 'Redundant-parameter recomputing (a parameter every edge hands one pure expression becomes it)',
|
|
71
|
+
run: (fn) => recomputeRedundantParams(fn),
|
|
72
|
+
},
|
|
54
73
|
];
|
package/src/raise/reroll.ts
CHANGED
|
@@ -49,7 +49,7 @@ import { analyzeLoops } from '../ir/loops';
|
|
|
49
49
|
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
50
50
|
import { intWidth } from '../ir/types';
|
|
51
51
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
52
|
-
import {
|
|
52
|
+
import { evalIntPair, isIntPairOpcode } from './const';
|
|
53
53
|
|
|
54
54
|
/** One loop whose shape admits a re-roll, and the facts its admission reads. */
|
|
55
55
|
export interface PeeledTest {
|
|
@@ -266,7 +266,7 @@ export interface FoldedTopTest {
|
|
|
266
266
|
/** the exit test on the entry values: `stays` (provably not taken), `leaves`, `guarded` (not a
|
|
267
267
|
* constant, but the test of the guard the loop is entered under, on the same values and leaving
|
|
268
268
|
* for the same exit), or `unknown` (not a constant: a memory read, a call, a parameter, an opcode
|
|
269
|
-
* `
|
|
269
|
+
* `evalIntPair` does not fold) */
|
|
270
270
|
atEntry: 'stays' | 'leaves' | 'guarded' | 'unknown';
|
|
271
271
|
}
|
|
272
272
|
|
|
@@ -293,25 +293,6 @@ export const TOP_TEST_GATES: readonly Gate<FoldedTopTest>[] = [
|
|
|
293
293
|
},
|
|
294
294
|
];
|
|
295
295
|
|
|
296
|
-
/** Integer opcodes `evalConst` folds, as int32; `add` and `or` through raise/const.ts's `foldConstPair`. */
|
|
297
|
-
const EVAL: Readonly<Record<string, (a: number, b: number) => number>> = {
|
|
298
|
-
add: (a, b) => foldConstPair('add', a, b)!,
|
|
299
|
-
or: (a, b) => foldConstPair('or', a, b)!,
|
|
300
|
-
sub: (a, b) => (a - b) | 0,
|
|
301
|
-
and: (a, b) => a & b,
|
|
302
|
-
xor: (a, b) => a ^ b,
|
|
303
|
-
icmp_eq: (a, b) => +(a === b),
|
|
304
|
-
icmp_ne: (a, b) => +(a !== b),
|
|
305
|
-
icmp_slt: (a, b) => +(a < b),
|
|
306
|
-
icmp_sle: (a, b) => +(a <= b),
|
|
307
|
-
icmp_sgt: (a, b) => +(a > b),
|
|
308
|
-
icmp_sge: (a, b) => +(a >= b),
|
|
309
|
-
icmp_ult: (a, b) => +(a >>> 0 < b >>> 0),
|
|
310
|
-
icmp_ule: (a, b) => +(a >>> 0 <= b >>> 0),
|
|
311
|
-
icmp_ugt: (a, b) => +(a >>> 0 > b >>> 0),
|
|
312
|
-
icmp_uge: (a, b) => +(a >>> 0 >= b >>> 0),
|
|
313
|
-
};
|
|
314
|
-
|
|
315
296
|
/** RESTORE THE TOP TEST THE COMPILER FOLDED AWAY. A loop `rerollOne` just rotated is now
|
|
316
297
|
*
|
|
317
298
|
* E: br T(A) T(q): test(q) ? X : H H: …; B = next(q); exit(B) ? Y : T(B)
|
|
@@ -387,13 +368,12 @@ function restoreTopTest(
|
|
|
387
368
|
if (d.opcode === 'const') {
|
|
388
369
|
return typeof d.attrs.value === 'number' ? d.attrs.value | 0 : null;
|
|
389
370
|
}
|
|
390
|
-
|
|
391
|
-
if (!f || d.operands.length !== 2 || !(moved.has(d) || E.ops.includes(d))) {
|
|
371
|
+
if (!isIntPairOpcode(d.opcode) || d.operands.length !== 2 || !(moved.has(d) || E.ops.includes(d))) {
|
|
392
372
|
return null;
|
|
393
373
|
}
|
|
394
374
|
const x = evalConst(d.operands[0]);
|
|
395
375
|
const y = evalConst(d.operands[1]);
|
|
396
|
-
return x === null || y === null ? null :
|
|
376
|
+
return x === null || y === null ? null : evalIntPair(d.opcode, x, y);
|
|
397
377
|
};
|
|
398
378
|
const cond = evalConst(hTerm.operands[0]);
|
|
399
379
|
const guard = cond === null ? entryGuard(fn, preds, defs, { E, H, exit, back, B, A, moved }) : null;
|