@asmlift/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
package/src/raise/offsetnames.ts
CHANGED
|
@@ -100,12 +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
|
-
|
|
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.
|
|
109
|
+
import { constDisplacementOf } from '../ir/alias';
|
|
110
|
+
import { type Block, type Fn, type Op, type Value, defOpMap, successorsOf } from '../ir/core';
|
|
104
111
|
import { verify } from '../ir/verify';
|
|
105
112
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
106
113
|
import { dce } from '../pattern/engine';
|
|
107
114
|
import { type SymbolInfo, type SymbolMap, accessSignedness, lookupSymbol } from '../symbols';
|
|
108
|
-
import { type StampBehaviors, stampDeclaredVolatileUnder } from './declared-volatile';
|
|
115
|
+
import { type StampBehaviors, joinIncoming, stampDeclaredVolatileUnder } from './declared-volatile';
|
|
116
|
+
import { RaiseUnsupportedError } from './errors';
|
|
109
117
|
|
|
110
118
|
/** One `add`/`sub` whose address the gates judge. Built only for an op that already resolves to a
|
|
111
119
|
* named base plus a NON-ZERO constant — a zero offset names the base itself, which the pool path
|
|
@@ -230,8 +238,10 @@ export const OFFSET_NAME_GATES: readonly Gate<OffsetAddress>[] = [
|
|
|
230
238
|
// The mirror of `base-address-ambiguous`, and it is not the same question asked twice: that one
|
|
231
239
|
// is one NAME the map places at two addresses, this one is one ADDRESS the map gives several
|
|
232
240
|
// names — aliases, or typed views of one RAM region, which `SymbolMap`'s `SymbolInfo[]` exists
|
|
233
|
-
// to carry. `lookupSymbol` answers with
|
|
234
|
-
//
|
|
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.
|
|
235
245
|
{
|
|
236
246
|
id: 'target-address-ambiguous',
|
|
237
247
|
why: 'the map gives the walked-to address several names that do not agree, so which one this spells is arbitrary',
|
|
@@ -317,39 +327,22 @@ function addressesByName(symbols: SymbolMap): Map<string, { addr: number; info:
|
|
|
317
327
|
}
|
|
318
328
|
|
|
319
329
|
/** The named base an address value stands on, following `add`/`sub` chains through constants —
|
|
320
|
-
* the `adds r1,#12` walk is a chain of them, and each link is its own candidate.
|
|
321
|
-
*
|
|
322
|
-
* induction never
|
|
330
|
+
* the `adds r1,#12` walk is a chain of them, and each link is its own candidate. The chain is
|
|
331
|
+
* ir/alias.ts `constDisplacementOf`, the reading raise/structs.ts shares; it stops at anything
|
|
332
|
+
* else, block parameters included (they have no def, so a loop-carried pointer induction never
|
|
333
|
+
* resolves). */
|
|
323
334
|
function namedBase(v: Value, defOf: Map<Value, Op>): { sym: string; code: boolean; offset: number } | null {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return { sym: d.attrs.sym as string, code: d.attrs.code === true, offset: 0 };
|
|
330
|
-
}
|
|
331
|
-
if (d.opcode !== 'add' && d.opcode !== 'sub') {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
const [a, b] = d.operands;
|
|
335
|
-
const ka = defOf.get(a)?.opcode === 'const' ? (defOf.get(a)!.attrs.value as number) : null;
|
|
336
|
-
const kb = defOf.get(b)?.opcode === 'const' ? (defOf.get(b)!.attrs.value as number) : null;
|
|
337
|
-
// `sub` is not commutative: only `base - K` is an offset off the base.
|
|
338
|
-
if (kb !== null) {
|
|
339
|
-
const base = namedBase(a, defOf);
|
|
340
|
-
return base && { ...base, offset: base.offset + (d.opcode === 'sub' ? -kb : kb) };
|
|
341
|
-
}
|
|
342
|
-
if (ka !== null && d.opcode === 'add') {
|
|
343
|
-
const base = namedBase(b, defOf);
|
|
344
|
-
return base && { ...base, offset: base.offset + ka };
|
|
345
|
-
}
|
|
346
|
-
return null;
|
|
335
|
+
const disp = constDisplacementOf(defOf, v);
|
|
336
|
+
const d = defOf.get(disp?.root ?? v);
|
|
337
|
+
return d?.opcode === 'gaddr'
|
|
338
|
+
? { sym: d.attrs.sym as string, code: d.attrs.code === true, offset: disp?.k ?? 0 }
|
|
339
|
+
: null;
|
|
347
340
|
}
|
|
348
341
|
|
|
349
342
|
/** Every `add`/`sub` in `fn` that resolves to a named base plus a non-zero constant, with the
|
|
350
343
|
* context the gates judge. Shared by the rewrite and by `offsetNameRefusals` so the two cannot
|
|
351
344
|
* disagree about which sites exist. */
|
|
352
|
-
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 }[] {
|
|
353
346
|
const defOf = defOpMap(fn);
|
|
354
347
|
const byName = addressesByName(symbols);
|
|
355
348
|
const written = new Set<Value>();
|
|
@@ -404,7 +397,7 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
|
|
|
404
397
|
}
|
|
405
398
|
}
|
|
406
399
|
}
|
|
407
|
-
const out: { op: Op; sym: string; addr: OffsetAddress }[] = [];
|
|
400
|
+
const out: { op: Op; sym: string; addr: OffsetAddress; at: number }[] = [];
|
|
408
401
|
for (const b of fn.blocks) {
|
|
409
402
|
for (const op of b.ops) {
|
|
410
403
|
if (op.opcode !== 'add' && op.opcode !== 'sub') {
|
|
@@ -426,6 +419,7 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
|
|
|
426
419
|
out.push({
|
|
427
420
|
op,
|
|
428
421
|
sym: base.sym,
|
|
422
|
+
at: at.addr + base.offset,
|
|
429
423
|
addr: {
|
|
430
424
|
base: at.info,
|
|
431
425
|
ambiguous: at.twice,
|
|
@@ -444,15 +438,400 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
|
|
|
444
438
|
return out;
|
|
445
439
|
}
|
|
446
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
|
+
|
|
447
823
|
/** Rewrite every admitted site into the `gaddr` the map names, in place. Returns THE NAMES it
|
|
448
824
|
* spelled, in block order — what a caller has to be told, because a name this pass produced does
|
|
449
825
|
* not rest on the map alone the way a pool-loaded one does: it rests on `addr(base) + K` landing
|
|
450
826
|
* on the map entry it names. `DecompileResult.walkedNames` and the trace's `stage:offsetnames`
|
|
451
|
-
* 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. */
|
|
452
831
|
export function nameOffsetAddresses(
|
|
453
832
|
fn: Fn,
|
|
454
833
|
symbols: SymbolMap,
|
|
455
|
-
behaviors:
|
|
834
|
+
behaviors: OffsetNameBehaviors,
|
|
456
835
|
gates: readonly Gate<OffsetAddress>[] = OFFSET_NAME_GATES,
|
|
457
836
|
): string[] {
|
|
458
837
|
// Every site is judged against the PRE-REWRITE defs, so a chain's later links resolve through
|
|
@@ -461,7 +840,7 @@ export function nameOffsetAddresses(
|
|
|
461
840
|
for (const s of admitted) {
|
|
462
841
|
s.op.opcode = 'gaddr';
|
|
463
842
|
s.op.operands = [];
|
|
464
|
-
s.op.attrs = { sym: s.addr.target!.name };
|
|
843
|
+
s.op.attrs = { sym: s.addr.target!.name, fromMap: true, mapAddr: s.at >>> 0 };
|
|
465
844
|
}
|
|
466
845
|
if (admitted.length > 0) {
|
|
467
846
|
// An access now names another object, so whether it reads one the map declares volatile is
|
|
@@ -470,6 +849,13 @@ export function nameOffsetAddresses(
|
|
|
470
849
|
dce(fn); // the constants and the walked-off base the rewrite just orphaned
|
|
471
850
|
verify(fn);
|
|
472
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
|
+
}
|
|
473
859
|
return admitted.map((s) => s.addr.target!.name);
|
|
474
860
|
}
|
|
475
861
|
|
|
@@ -504,7 +890,7 @@ export function offsetNameRefusals(
|
|
|
504
890
|
* pass up, which three hand-written calls do not — so `offset-names.test.ts` supplies it by
|
|
505
891
|
* wrapping this record and asserting all three reach it. */
|
|
506
892
|
export const OFFSET_NAME_PASS: {
|
|
507
|
-
run: (fn: Fn, symbols: SymbolMap, behaviors:
|
|
893
|
+
run: (fn: Fn, symbols: SymbolMap, behaviors: OffsetNameBehaviors) => string[];
|
|
508
894
|
} = {
|
|
509
895
|
run: (fn, symbols, behaviors) => nameOffsetAddresses(fn, symbols, behaviors),
|
|
510
896
|
};
|
package/src/raise/pairparams.ts
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// 64-bit value; it is a word this function uses as a word, which is what a 64-bit argument passed
|
|
15
15
|
// to a helper and separately tested would look like. Fusing it would delete a use.
|
|
16
16
|
import { Fn, Op, Value } from '../ir/core';
|
|
17
|
+
import { intWidth } from '../ir/types';
|
|
18
|
+
import { type FnProto, declaredWidth } from '../proto';
|
|
19
|
+
import { RaiseUnsupportedError } from './errors';
|
|
17
20
|
|
|
18
21
|
/** Count every operand and edge-argument occurrence of each value. Occurrences, not sites: a value
|
|
19
22
|
* passed to `f(x, x)` is used twice, and a rule that said "used once" about it would be wrong. */
|
|
@@ -79,3 +82,33 @@ export function fuseParamPairs(fn: Fn): boolean {
|
|
|
79
82
|
}
|
|
80
83
|
return changed;
|
|
81
84
|
}
|
|
85
|
+
|
|
86
|
+
/** A FUNCTION DECLARED WITH AN INTEGER PARAMETER WIDER THAN A REGISTER, whose lift reads that
|
|
87
|
+
* parameter's words apart, refuses — on every frontend, since this reads only the entry block
|
|
88
|
+
* `fuseParamPairs` leaves. The return's twin is pipeline.ts `refuseNarrowReturnsOfWideDeclaration`.
|
|
89
|
+
*
|
|
90
|
+
* The pair is fused only where the lift built its `concat` (a helper call, a carry chain); a body
|
|
91
|
+
* that tests or combines the two words itself (`return a != 0;` is `or` of the halves on every
|
|
92
|
+
* compiler) leaves two word parameters, and the lift prints `s32 f(s32 a0, s32 a1)` against the
|
|
93
|
+
* caller's own `int f(long long a)` — a candidate the compiler rejects as a redeclaration. It is
|
|
94
|
+
* correct C under the ABI, and the wrong function for the declaration in hand. Counting is
|
|
95
|
+
* enough: every fused parameter is wider than a register and only `fuseParamPairs` makes one, so
|
|
96
|
+
* fewer of them than the declaration names means a declared one was left in halves. A
|
|
97
|
+
* declaration the caller does not state (a context's own entry is left out,
|
|
98
|
+
* proto-context.ts `withContextPrototypes`) asks nothing here. */
|
|
99
|
+
export function refuseSplitWideParams(fn: Fn, self: FnProto | undefined): void {
|
|
100
|
+
const declared = Array.isArray(self?.params) ? self.params : [];
|
|
101
|
+
const wide = declared.filter((t) => (declaredWidth(t) ?? 0) > 32);
|
|
102
|
+
if (wide.length === 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const fused = fn.blocks[0].params.filter((p) => (intWidth(p.type) ?? 0) > 32).length;
|
|
106
|
+
if (fused < wide.length) {
|
|
107
|
+
const at = declared.findIndex((t) => (declaredWidth(t) ?? 0) > 32);
|
|
108
|
+
throw new RaiseUnsupportedError(
|
|
109
|
+
`cannot lift '${fn.name}': its declaration takes parameter ${at + 1} as '${declared[at]}', and this lift ` +
|
|
110
|
+
'reads the two halves of a 64-bit value as two word parameters (a 64-bit parameter is one only where the ' +
|
|
111
|
+
'lift pairs its halves)',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|