@asmlift/core 0.9.0 → 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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- 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 +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -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 +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- 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 +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- 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/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- 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 +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
package/src/l3/ast.ts
CHANGED
|
@@ -9,7 +9,21 @@ export type Expr =
|
|
|
9
9
|
// A `double` literal, carried as its IEEE bit pattern (ir/float-bits.ts) — a kind apart from the
|
|
10
10
|
// integer `const`, so no integer fold, range test or signedness reading ever takes it for one.
|
|
11
11
|
| { k: 'fconst'; bits: string }
|
|
12
|
-
|
|
12
|
+
// `unsignedCmp` is EVIDENCE, not spelling: an ordering compare (`<` `<=` `>` `>=`) lowered from an
|
|
13
|
+
// `icmp_u*`, which C spells with the same token as the signed one and decides from the operand
|
|
14
|
+
// types. The printer never reads it; `signedOverUnsignedCompare` (l3/typing.ts) does, over the
|
|
15
|
+
// FINISHED tree, so a pass that substitutes into a compare cannot re-spell it signed unseen.
|
|
16
|
+
// `exprEquals` ignores it, as it ignores `index.operandOff`: two nodes that print alike are
|
|
17
|
+
// alike to a CSE. A merge that keeps one of two equal nodes keeps it through `unionEvidence`, so
|
|
18
|
+
// the evidence of the one it drops is not lost with it.
|
|
19
|
+
//
|
|
20
|
+
// `helperRel` is evidence too: on a float `==`/`!=`, the relation the SOURCE spelled, where the
|
|
21
|
+
// compiler picked a runtime helper by it (runtime-helpers.ts `RuntimeHelper.relation` — agbcc's
|
|
22
|
+
// `__eqsf2` and `__nesf2`). It rides through `negateCond` unchanged, so a node whose `op` is not
|
|
23
|
+
// its `helperRel` is one a negation moved off the helper, and `keepsHelperRel` /
|
|
24
|
+
// `regainsHelperRel` below are what a pass choosing between `if (c) A else B` and
|
|
25
|
+
// `if (!c) B else A` asks. Ignored by `exprEquals` and by the printer, like `unsignedCmp`.
|
|
26
|
+
| { k: 'bin'; op: BinOp; l: Expr; r: Expr; unsignedCmp?: true; helperRel?: BinOp }
|
|
13
27
|
| { k: 'un'; op: '-' | '~' | '!' | 'f-'; e: Expr }
|
|
14
28
|
// A post-increment `name++` (`by` 1) or post-decrement `name--` (`by` -1): the value the local
|
|
15
29
|
// held BEFORE the update, with the update as a side effect. The only expression form here that
|
|
@@ -31,10 +45,10 @@ export type Expr =
|
|
|
31
45
|
// A C-style value cast `(T)e`. Tree-level producers: a width-narrowing cast `(u8)e` (the
|
|
32
46
|
// recovered form of a byte/half extend idiom — zext/sext IR ops), the STRUCT-pointer cast
|
|
33
47
|
// (structure.ts memAccess/arrayAccess struct paths — see the note on `field` below), and the
|
|
34
|
-
// integer legalization of a pointer operand under an operator C rejects (
|
|
35
|
-
// `3 & (s32)p`). Scalar deref casts are backend-owned — the
|
|
36
|
-
// them from the `index` node's width. Each backend spells the cast
|
|
37
|
-
// (C: `(u8)e`; Pascal: no spelling yet → fails loud).
|
|
48
|
+
// integer legalization of a pointer operand under an operator C rejects (the arithmetic table,
|
|
49
|
+
// structure/pointer-spelling.ts: `3 & (s32)p`). Scalar deref casts are backend-owned — the
|
|
50
|
+
// C-family printer synthesizes them from the `index` node's width. Each backend spells the cast
|
|
51
|
+
// in its own syntax (C: `(u8)e`; Pascal: no spelling yet → fails loud).
|
|
38
52
|
//
|
|
39
53
|
// `volatile` qualifies the POINTEE of a pointer cast (`(volatile u16 *)0x4000208`) — the one
|
|
40
54
|
// place asmlift can say "this access is to a volatile object" when there is no declaration to
|
|
@@ -62,13 +76,29 @@ export type Expr =
|
|
|
62
76
|
// `sret` is the type of a struct the callee returns BY VALUE through memory. `args[0]` is then the
|
|
63
77
|
// hidden pointer the frontend read from the first argument register — the address of the struct
|
|
64
78
|
// the call fills — and the rest are the declared arguments. The only C spelling is a statement,
|
|
65
|
-
// `dest = fn(rest)`, so such a call stands alone.
|
|
79
|
+
// `dest = fn(rest)`, so such a call stands alone. `returns` is the type of a struct the callee
|
|
80
|
+
// returns BY VALUE in registers, where the call is that value itself, `return mk(x);`
|
|
81
|
+
// (frontend/register-struct-call.ts); the declarations block prints its prototype by it.
|
|
82
|
+
//
|
|
83
|
+
// `byValue` names the arguments that are a struct passed BY VALUE — a local or a global spelled
|
|
84
|
+
// bare, `take(s)` — by their place in `args` and the declared parameter each is, with the struct
|
|
85
|
+
// the declaration passes
|
|
86
|
+
// (frontend/call-plan.ts `foldByValueArgs`); the declarations block defines each beside the
|
|
87
|
+
// callee's prototype.
|
|
66
88
|
//
|
|
67
89
|
// `fn` is the callee's NAME, or for an indirect call the EXPRESSION whose value is the address
|
|
68
90
|
// called (ir/core.ts `calleeValue`) — a child of the node like its arguments, listed first in
|
|
69
91
|
// `exprChildren`. That is no evaluation order: C leaves it unspecified, and agbcc loads the callee
|
|
70
92
|
// after the arguments.
|
|
71
|
-
| {
|
|
93
|
+
| {
|
|
94
|
+
k: 'call';
|
|
95
|
+
fn: string | Expr;
|
|
96
|
+
args: Expr[];
|
|
97
|
+
wide64?: true;
|
|
98
|
+
sret?: IrType;
|
|
99
|
+
returns?: IrType;
|
|
100
|
+
byValue?: readonly { readonly at: number; readonly param: number; readonly type: IrType }[];
|
|
101
|
+
}
|
|
72
102
|
// The ADDRESS of a named global, `&gSym` (agbcc pool `.word gSym`, frontend `gaddr` op). A
|
|
73
103
|
// DEREF of it collapses to the bare global: memAccess/arrayAccess spell `*(&gSym)` as `gSym`
|
|
74
104
|
// and `(&gSym)[i]` as `gSym[i]` (a global name decays to a pointer). Only a genuinely
|
|
@@ -192,6 +222,15 @@ export type Expr =
|
|
|
192
222
|
operandOff?: number;
|
|
193
223
|
baseOrdered?: true;
|
|
194
224
|
baseAdvanced?: number;
|
|
225
|
+
/** Spell element 0 as `base[0]`, never `*base`. The two are one lvalue to C and NOT to
|
|
226
|
+
* agbcc's alias analysis: `u.half[0]` is an ARRAY_REF of a union member, alias set 0
|
|
227
|
+
* (c-common.c `c_get_alias_set`), while `*u.half` dereferences the member's decay — an
|
|
228
|
+
* INDIRECT_REF typed by its pointee, which a compiled `*u.half = 1; *u.word = x; return
|
|
229
|
+
* *u.half;` returns stale (`mov r0, #0x1`) where the subscript reloads. Set on a view of a
|
|
230
|
+
* frame union (structure.ts `frameUnionAccess`), the one access that rests on that set,
|
|
231
|
+
* and on an element of a frame array, where it only keeps the element spelling at index 0
|
|
232
|
+
* (structure.ts `frameArrayElement`, l3/constinit.ts `asElement`). */
|
|
233
|
+
subscript?: true;
|
|
195
234
|
}
|
|
196
235
|
// A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
|
|
197
236
|
// pointer, so the byte offset resolves to a named field instead of a scaled array index).
|
|
@@ -226,9 +265,10 @@ export type Expr =
|
|
|
226
265
|
// The divides earned it second, on pokeemerald:GetAnchorCoord — `(u32)(coord * a1) / (u32)a0`
|
|
227
266
|
// standing beside two arithmetic shifts of the same values. Their only other channel is the operand
|
|
228
267
|
// TYPES, reached by flipping a declaration, and there that flip is unreachable and unsound at
|
|
229
|
-
// once: the divisor also feeds a signed compare, so the
|
|
230
|
-
// it, and forcing it anyway makes agbcc
|
|
231
|
-
// the only spelling that says "this
|
|
268
|
+
// once: the divisor also feeds a signed compare, so the unsigned-compare reconciliation
|
|
269
|
+
// (structure.ts unsignedCompareSpelling) correctly refuses it, and forcing it anyway makes agbcc
|
|
270
|
+
// delete the comparison as always-false. A per-operand pin is the only spelling that says "this
|
|
271
|
+
// division alone is unsigned".
|
|
232
272
|
//
|
|
233
273
|
// The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
|
|
234
274
|
// which side a compare was spelled from genuinely underdetermines — a signed spelling that
|
|
@@ -241,12 +281,20 @@ export type Expr =
|
|
|
241
281
|
// and `+`/`-` are the pointer walks, reassociations and constant folds of a dozen L3 passes. A float
|
|
242
282
|
// under any of those is a wrong program, so it gets operators none of them match — the same reason
|
|
243
283
|
// the IR has `fadd` rather than a float-typed `add`. The unary minus splits the same way (`f-` on
|
|
244
|
-
// `un`).
|
|
284
|
+
// `un`). The float COMPARES split for the same reason and one more: NaN makes them a partial order,
|
|
285
|
+
// so `negateCond` may flip `f==` and `f!=` into each other and no other — `!(a f> b)` is not
|
|
286
|
+
// `a f<= b`, and it prints as the `!` it is.
|
|
245
287
|
export type BinOp =
|
|
246
288
|
| 'f+'
|
|
247
289
|
| 'f-'
|
|
248
290
|
| 'f*'
|
|
249
291
|
| 'f/'
|
|
292
|
+
| 'f<'
|
|
293
|
+
| 'f<='
|
|
294
|
+
| 'f>'
|
|
295
|
+
| 'f>='
|
|
296
|
+
| 'f=='
|
|
297
|
+
| 'f!='
|
|
250
298
|
| '+'
|
|
251
299
|
| '-'
|
|
252
300
|
| '*'
|
|
@@ -296,7 +344,15 @@ export type Stmt =
|
|
|
296
344
|
// `break;` / `continue;` — a loop early-exit / next-iteration jump. Emitted ONLY when the target is
|
|
297
345
|
// the innermost enclosing loop (bare C break/continue cannot express a multi-level exit; a deeper
|
|
298
346
|
// target declines). SGI/IDO Pascal has neither, so its backend loud-fails them (like `field`/`cast`).
|
|
299
|
-
|
|
347
|
+
//
|
|
348
|
+
// `ofSwitch` makes the `break` the innermost enclosing SWITCH's instead: it leaves the arm it sits
|
|
349
|
+
// in for the code after the switch (`case 1: if (c) break; case 2: …`). Legal only inside a switch
|
|
350
|
+
// arm with no loop or switch between it and that arm — the producer's contract (structure.ts
|
|
351
|
+
// `switchBreakAt`). The C family prints both as `break;`, so its printer refuses each where C
|
|
352
|
+
// would bind it to the other kind — a plain one between `case` labels, this one in a loop body
|
|
353
|
+
// (backend/cfamily.ts `refuseMisboundBreak`). A pass that reads `break` as a loop exit must skip
|
|
354
|
+
// this one (l3/reindex.ts `hasLoopExit`).
|
|
355
|
+
| { k: 'break'; ofSwitch?: true }
|
|
300
356
|
| { k: 'continue' }
|
|
301
357
|
// A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
|
|
302
358
|
// a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
|
|
@@ -399,6 +455,13 @@ export interface SFn {
|
|
|
399
455
|
* one are deliberately left unstamped even where the offset is in hand — see the refusal at
|
|
400
456
|
* the structurer's build site for the measurement that decided it. */
|
|
401
457
|
slots?: number[];
|
|
458
|
+
/** The declaration's initializer, element by element, null where the source left one out
|
|
459
|
+
* (`SImage.init`) — present only on the copy the C-family emitter prints, where
|
|
460
|
+
* l3/constinit.ts folded the block copy of the compiler's anonymous constant image into it
|
|
461
|
+
* (`u8 sp0[16] = { … };`). Never on a tree a pass sees. */
|
|
462
|
+
init?: (number | InitAddress | null)[];
|
|
463
|
+
/** that initializer is spelled as a string literal — the image was written as one */
|
|
464
|
+
initString?: true;
|
|
402
465
|
}[];
|
|
403
466
|
/** project globals referenced with a known declaration shape (symbol map) — typed for the
|
|
404
467
|
* legalization env (exprCType) but NEVER declared by a backend: the project's own headers
|
|
@@ -408,6 +471,11 @@ export interface SFn {
|
|
|
408
471
|
* the top of its body ahead of the locals. Unlike `globals`, the backend declares these: no
|
|
409
472
|
* header does, because the source defined them here. */
|
|
410
473
|
statics?: SStatic[];
|
|
474
|
+
/** The compiler's own constant images this function copies a local's initializer from
|
|
475
|
+
* (structure/local-statics.ts), under labels the source never wrote. Not statics: no declaration
|
|
476
|
+
* may spell one, so the C-family emitter folds each into the initializer of the local its copy
|
|
477
|
+
* fills (l3/constinit.ts), and a backend that cannot refuses. */
|
|
478
|
+
images?: SImage[];
|
|
411
479
|
retType: IrType;
|
|
412
480
|
body: Stmt[];
|
|
413
481
|
/** Struct types this function's fields reference, declared above it by the backend. Empty
|
|
@@ -426,31 +494,64 @@ export interface SFn {
|
|
|
426
494
|
* and `l3/slotorder.ts` is the identity — never "ascending by default". */
|
|
427
495
|
slotOrder?: 'ascending' | 'descending';
|
|
428
496
|
/** Each declared callee's parameter types (an entry the printer cannot spell is `undefined`):
|
|
429
|
-
* what an argument converts to. Read by the C-family printer where `dialect` says C
|
|
430
|
-
* refuses three conversions C makes implicitly
|
|
431
|
-
* pointer-cell conversions in every dialect
|
|
497
|
+
* what an argument converts to. Read by the C-family printer where `dialect` says C++ or
|
|
498
|
+
* `strictConversions` is set, either of which refuses three conversions C makes implicitly
|
|
499
|
+
* (backend/cfamily.ts `argConversion`), and by the pointer-cell conversions in every dialect
|
|
500
|
+
* (l3/ptrcell.ts). */
|
|
432
501
|
declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
|
|
433
502
|
/** Each declared callee's return type as the project spells it, never printed: whether a call a
|
|
434
503
|
* pointer cell is compared with returns a pointer (l3/ptrcell.ts). */
|
|
435
504
|
declaredReturns?: Readonly<Record<string, string>>;
|
|
436
505
|
/** The emitted C is compiled AS C++. An emission policy like `slotOrder`. */
|
|
437
506
|
dialect?: 'c++';
|
|
507
|
+
/** The compiler parses a designated element in an initializer (`[8] = 2`) — a fact of its
|
|
508
|
+
* dialect, carried from `compilerBehaviors.designatedInitializers`. An emission policy: the
|
|
509
|
+
* C-family printer spells an initializer's left-out run by index where present, as zeros where
|
|
510
|
+
* not. */
|
|
511
|
+
designators?: true;
|
|
512
|
+
/** The compiler refuses C's implicit pointer conversions in C as well, as C++ does — carried from
|
|
513
|
+
* `compilerBehaviors.strictPointerConversions`. An emission policy: the C-family printer casts an
|
|
514
|
+
* argument to its declared parameter (`declaredArgs`), and a pointer of another pointee written
|
|
515
|
+
* into a pointer slot, wherever it would for C++. */
|
|
516
|
+
strictConversions?: true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** An initial element that is an ADDRESS CONSTANT (ir/core.ts `LocalObject.relocs`): `symbol`'s
|
|
520
|
+
* address plus `addend` bytes, held in the element's integer type — a global's, which a
|
|
521
|
+
* declaration outside the function names. */
|
|
522
|
+
export interface InitAddress {
|
|
523
|
+
symbol: string;
|
|
524
|
+
addend: number;
|
|
438
525
|
}
|
|
439
526
|
|
|
440
527
|
/** One function-scope static's definition: `static [const] T name[count] = { init };`. `init` is
|
|
441
|
-
* the initial elements in order, absent for a static with no initializer (the target's bss)
|
|
442
|
-
*
|
|
528
|
+
* the initial elements in order, absent for a static with no initializer (the target's bss); an
|
|
529
|
+
* element the target relocates is the {@link InitAddress} it holds. `align` is an alignment in
|
|
530
|
+
* bytes wider than the type's own, which the definition must state. */
|
|
443
531
|
export interface SStatic {
|
|
444
532
|
name: string;
|
|
445
533
|
type: IrType;
|
|
446
534
|
const?: true;
|
|
447
535
|
align?: number;
|
|
448
|
-
init?: number[];
|
|
536
|
+
init?: (number | InitAddress)[];
|
|
449
537
|
/** the initializer is spelled as a string literal — a byte array the compiler aligns as a
|
|
450
538
|
* string (structure/local-statics.ts) */
|
|
451
539
|
string?: true;
|
|
452
540
|
}
|
|
453
541
|
|
|
542
|
+
/** One constant image of the compiler's own (ir/core.ts `LocalObject.anonymous`), named by its
|
|
543
|
+
* label: an integer array typed by the directives that wrote it, and its initializer element by
|
|
544
|
+
* element — null where the source left one out (padding in the image, which C supplies as zero),
|
|
545
|
+
* the {@link InitAddress} an element the target relocates holds, and ending at the last one it
|
|
546
|
+
* spelled, so it may be shorter than `type`. */
|
|
547
|
+
export interface SImage {
|
|
548
|
+
name: string;
|
|
549
|
+
type: IrType;
|
|
550
|
+
init: (number | InitAddress | null)[];
|
|
551
|
+
/** the image was written as a string literal (`.ascii`) */
|
|
552
|
+
string?: true;
|
|
553
|
+
}
|
|
554
|
+
|
|
454
555
|
/** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
|
|
455
556
|
* type but lives in the neutral AST so a backend can print `struct N { ... };`. */
|
|
456
557
|
export interface StructType {
|
|
@@ -471,6 +572,13 @@ export interface LanguageBackend {
|
|
|
471
572
|
* cannot print it turns a whole function that used to decompile into a loud stub. The
|
|
472
573
|
* structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
|
|
473
574
|
readonly spellsSwitchFallthrough: boolean;
|
|
575
|
+
/** Can this language spell a `break` that leaves a `switch` arm before its end
|
|
576
|
+
* (`{ k: 'break', ofSwitch: true }`)? C and C++ can; Pascal has no `break` at all, and its
|
|
577
|
+
* backend loud-fails the node. Asked for the same reason as `spellsSwitchFallthrough`: a
|
|
578
|
+
* closed arm with a mid-arm exit (`case 10: if (a) { if (b) break; g(1); } h(2); break;`) also
|
|
579
|
+
* spells without one, by copying the code the arm meets at into each path, which Pascal prints.
|
|
580
|
+
* The structurer reads it through `StructureOptions.spellSwitchBreak`. */
|
|
581
|
+
readonly spellsSwitchBreak: boolean;
|
|
474
582
|
emit(fn: SFn): string;
|
|
475
583
|
// Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
|
|
476
584
|
// Used by the annotate-mode stub path to carry the failure reason + the original asm
|
|
@@ -560,6 +668,10 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
560
668
|
(typeof a.fn === 'string' || typeof bb.fn === 'string' ? a.fn === bb.fn : exprEquals(a.fn, bb.fn)) &&
|
|
561
669
|
(a.wide64 ?? false) === (bb.wide64 ?? false) &&
|
|
562
670
|
(a.sret === undefined ? bb.sret === undefined : bb.sret !== undefined && typeEquals(a.sret, bb.sret)) &&
|
|
671
|
+
(a.returns === undefined
|
|
672
|
+
? bb.returns === undefined
|
|
673
|
+
: bb.returns !== undefined && typeEquals(a.returns, bb.returns)) &&
|
|
674
|
+
(a.byValue ?? []).map((x) => x.at).join() === (bb.byValue ?? []).map((x) => x.at).join() &&
|
|
563
675
|
a.args.length === bb.args.length &&
|
|
564
676
|
a.args.every((x, i) => exprEquals(x, bb.args[i]))
|
|
565
677
|
);
|
|
@@ -636,6 +748,60 @@ export function stmtsEqual(a: readonly Stmt[], b: readonly Stmt[]): boolean {
|
|
|
636
748
|
return a.length === b.length && a.every((s, i) => stmtEquals(s, b[i]));
|
|
637
749
|
}
|
|
638
750
|
|
|
751
|
+
/** The node a merge KEEPS when it found `a` and `b` equal ({@link exprEquals}) and drops one: `a`,
|
|
752
|
+
* with every compare carrying `unsignedCmp` if it did in EITHER. Equality ignores the evidence
|
|
753
|
+
* because two compares that print alike are one expression to C; but when one of them was lowered
|
|
754
|
+
* from an `icmp_u` and the other from a signed compare, the kept node now stands for both paths,
|
|
755
|
+
* and the finished-tree judgment (l3/typing.ts `signedOverUnsignedCompare`) can only see the
|
|
756
|
+
* unsigned one's obligation if the node still says so. Keeping `a` bare would publish a signed
|
|
757
|
+
* `x < y` on the path that compared unsigned (an slt arm and an sltu arm merged by l3/tailmerge.ts
|
|
758
|
+
* under `/signed-cmp`). Precondition: `exprEquals(a, b)`, so the two trees walk in step. */
|
|
759
|
+
export function unionEvidence(a: Expr, b: Expr): Expr {
|
|
760
|
+
// `mapExprChildren` calls `f` in `exprChildren` order (an object literal evaluates its
|
|
761
|
+
// properties in source order), so the i-th call pairs with b's i-th child. Not by identity: one
|
|
762
|
+
// child object may sit in two positions, each with its own counterpart in `b`.
|
|
763
|
+
const bs = exprChildren(b);
|
|
764
|
+
let i = 0;
|
|
765
|
+
const out = mapExprChildren(a, (c) => unionEvidence(c, bs[i++]));
|
|
766
|
+
return out.k === 'bin' && b.k === 'bin' && b.unsignedCmp === true && out.unsignedCmp !== true
|
|
767
|
+
? { ...out, unsignedCmp: true }
|
|
768
|
+
: out;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** {@link unionEvidence} over two statements {@link stmtEquals} found equal. */
|
|
772
|
+
export function unionStmtEvidence(a: Stmt, b: Stmt): Stmt {
|
|
773
|
+
switch (a.k) {
|
|
774
|
+
case 'assign':
|
|
775
|
+
return b.k === 'assign' ? { ...a, value: unionEvidence(a.value, b.value) } : a;
|
|
776
|
+
case 'store':
|
|
777
|
+
return b.k === 'store'
|
|
778
|
+
? { ...a, lval: unionEvidence(a.lval, b.lval), value: unionEvidence(a.value, b.value) }
|
|
779
|
+
: a;
|
|
780
|
+
case 'exprstmt':
|
|
781
|
+
return b.k === 'exprstmt' ? { ...a, value: unionEvidence(a.value, b.value) } : a;
|
|
782
|
+
case 'return':
|
|
783
|
+
return b.k === 'return' && a.value !== undefined && b.value !== undefined
|
|
784
|
+
? { ...a, value: unionEvidence(a.value, b.value) }
|
|
785
|
+
: a;
|
|
786
|
+
case 'if':
|
|
787
|
+
return b.k === 'if'
|
|
788
|
+
? {
|
|
789
|
+
...a,
|
|
790
|
+
cond: unionEvidence(a.cond, b.cond),
|
|
791
|
+
then: unionStmtsEvidence(a.then, b.then),
|
|
792
|
+
else: unionStmtsEvidence(a.else, b.else),
|
|
793
|
+
}
|
|
794
|
+
: a;
|
|
795
|
+
default:
|
|
796
|
+
return a;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** {@link unionStmtEvidence} over two lists {@link stmtsEqual} found equal. */
|
|
801
|
+
export function unionStmtsEvidence(a: readonly Stmt[], b: readonly Stmt[]): Stmt[] {
|
|
802
|
+
return a.map((s, i) => unionStmtEvidence(s, b[i]));
|
|
803
|
+
}
|
|
804
|
+
|
|
639
805
|
/** THE spelling of an `opaque`'s gap reason, in one place: `structure.ts` writes it into the
|
|
640
806
|
* marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
|
|
641
807
|
* classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
|
|
@@ -875,6 +1041,44 @@ function* subterms(e: Expr): Generator<Expr> {
|
|
|
875
1041
|
}
|
|
876
1042
|
}
|
|
877
1043
|
|
|
1044
|
+
export type Assign = Extract<Stmt, { k: 'assign' }>;
|
|
1045
|
+
|
|
1046
|
+
/** Every `assign` in the tree, at any nesting. ORDER-FREE: its callers ask what a name is ever
|
|
1047
|
+
* assigned, never which assignment comes first, so a switch default's position does not matter. */
|
|
1048
|
+
export function assignsIn(body: readonly Stmt[]): Assign[] {
|
|
1049
|
+
const out: Assign[] = [];
|
|
1050
|
+
const visit = (s: Stmt): void => {
|
|
1051
|
+
if (s.k === 'assign') {
|
|
1052
|
+
out.push(s);
|
|
1053
|
+
}
|
|
1054
|
+
stmtChildren(s).forEach(visit);
|
|
1055
|
+
};
|
|
1056
|
+
body.forEach(visit);
|
|
1057
|
+
return out;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** Grow `seed` over assignments to a fixpoint: an assignment adds its target when `propagates`
|
|
1061
|
+
* says its value carries what the set holds so far. A FLOW-INSENSITIVE taint, which is what each
|
|
1062
|
+
* caller wants — the question is what a name may EVER hold, so one assignment anywhere is
|
|
1063
|
+
* enough. Callers own the seed and the rule; this owns only the iteration. */
|
|
1064
|
+
export function taintOverAssigns(
|
|
1065
|
+
assigns: readonly Assign[],
|
|
1066
|
+
seed: Iterable<string>,
|
|
1067
|
+
propagates: (a: Assign, tainted: ReadonlySet<string>) => boolean,
|
|
1068
|
+
): Set<string> {
|
|
1069
|
+
const tainted = new Set(seed);
|
|
1070
|
+
for (let grew = true; grew;) {
|
|
1071
|
+
grew = false;
|
|
1072
|
+
for (const a of assigns) {
|
|
1073
|
+
if (!tainted.has(a.name) && propagates(a, tainted)) {
|
|
1074
|
+
tainted.add(a.name);
|
|
1075
|
+
grew = true;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return tainted;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
878
1082
|
/** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
|
|
879
1083
|
* splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
|
|
880
1084
|
* init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
|
|
@@ -1077,8 +1281,76 @@ export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
|
|
|
1077
1281
|
'<=': '>',
|
|
1078
1282
|
'==': '!=',
|
|
1079
1283
|
'!=': '==',
|
|
1284
|
+
// C's float `==` is false on a NaN and `!=` true, so these two are each other's negation; the
|
|
1285
|
+
// ordered four have none in the operator vocabulary (the BinOp note).
|
|
1286
|
+
'f==': 'f!=',
|
|
1287
|
+
'f!=': 'f==',
|
|
1080
1288
|
};
|
|
1081
1289
|
|
|
1290
|
+
/** The compares under `e`'s connectives whose runtime helper names the relation the source spelled
|
|
1291
|
+
* (`helperRel`), each answering whether it spells that relation now. `negateCond` flips every one
|
|
1292
|
+
* of them at once, through De Morgan. */
|
|
1293
|
+
function helperRelLeaves(e: Expr, out: boolean[] = []): boolean[] {
|
|
1294
|
+
if (e.k === 'bin' && (e.op === '&&' || e.op === '||')) {
|
|
1295
|
+
helperRelLeaves(e.l, out);
|
|
1296
|
+
helperRelLeaves(e.r, out);
|
|
1297
|
+
} else if (e.k === 'bin' && e.helperRel !== undefined) {
|
|
1298
|
+
out.push(e.op === e.helperRel);
|
|
1299
|
+
}
|
|
1300
|
+
return out;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/** Whether `e` spells, at every compare of it whose runtime helper names the relation the source
|
|
1304
|
+
* spelled, that relation — so that negating `e` recompiles to the other helper at each of them:
|
|
1305
|
+
* the relation's twin and its `!` alike, since gcc folds `!(a == b)` into `a != b`. An arm swap
|
|
1306
|
+
* that would negate it keeps `e` and its arm order instead. */
|
|
1307
|
+
export function keepsHelperRel(e: Expr): boolean {
|
|
1308
|
+
const leaves = helperRelLeaves(e);
|
|
1309
|
+
return leaves.length > 0 && leaves.every((own) => own);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** Whether negating `e` puts every such compare back on its helper's relation: the swap a pass
|
|
1313
|
+
* would otherwise leave alone is the one that spells the helpers. A condition mixing the two has
|
|
1314
|
+
* neither answer, and no arm order spells it. */
|
|
1315
|
+
export function regainsHelperRel(e: Expr): boolean {
|
|
1316
|
+
const leaves = helperRelLeaves(e);
|
|
1317
|
+
return leaves.length > 0 && leaves.every((own) => !own);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/** A loop's stay condition `P && S` whose compares under `S` are all negated off their helper's
|
|
1321
|
+
* relation, and whose `P` negates none: `{ head: P, guard: !S }`, which the loop spells
|
|
1322
|
+
* `while (P) { if (!S) break; … }` — the same tests in the same order, calling the helpers the
|
|
1323
|
+
* source called. `while (P && S)` has no spelling of that `S` that calls the helper (gcc folds
|
|
1324
|
+
* `!(a == b)` into `a != b`); the break guard is how the source wrote it before the branch fold
|
|
1325
|
+
* joined the two tests. Null when the condition already spells every helper's relation, or when no
|
|
1326
|
+
* split of its `&&` chain separates the two kinds of compare. */
|
|
1327
|
+
export function splitHelperBreak(cond: Expr): { head: Expr; guard: Expr } | null {
|
|
1328
|
+
if (keepsHelperRel(cond) || helperRelLeaves(cond).length === 0) {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
const conj: Expr[] = [];
|
|
1332
|
+
const flatten = (e: Expr): void => {
|
|
1333
|
+
if (e.k === 'bin' && e.op === '&&') {
|
|
1334
|
+
flatten(e.l);
|
|
1335
|
+
flatten(e.r);
|
|
1336
|
+
} else {
|
|
1337
|
+
conj.push(e);
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
flatten(cond);
|
|
1341
|
+
const and = (xs: Expr[]): Expr => xs.reduce((l, r) => ({ k: 'bin', op: '&&', l, r }));
|
|
1342
|
+
// The longest prefix that negates no helper leaf; the rest must negate every one it holds.
|
|
1343
|
+
let k = 0;
|
|
1344
|
+
while (k < conj.length && helperRelLeaves(conj[k]).every((own) => own)) {
|
|
1345
|
+
k++;
|
|
1346
|
+
}
|
|
1347
|
+
if (k === 0 || k === conj.length) {
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
const rest = and(conj.slice(k));
|
|
1351
|
+
return regainsHelperRel(rest) ? { head: and(conj.slice(0, k)), guard: negateCond(rest) } : null;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1082
1354
|
export function negateCond(e: Expr): Expr {
|
|
1083
1355
|
if (e.k === 'bin') {
|
|
1084
1356
|
const flipped = NEGATE_REL[e.op];
|
package/src/l3/basecse.ts
CHANGED
|
@@ -248,15 +248,35 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
/** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local.
|
|
252
|
-
|
|
251
|
+
/** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local.
|
|
252
|
+
*
|
|
253
|
+
* A `volatile` cast over a hoisted LEAF keeps its cast and takes the local under it:
|
|
254
|
+
* `((volatile s32 *)0x40000D4)[2]` beside a hoisted `p0 = (s32 *)0x40000D4` becomes
|
|
255
|
+
* `((volatile s32 *)p0)[2]`. The cast is never hoisted itself (`isHoistableBase`), since the local
|
|
256
|
+
* would drop its qualifier; under it the local is the same address, so the access keeps its
|
|
257
|
+
* qualifier and its cell, and reads the register the compiler already holds the base in. Inline, a
|
|
258
|
+
* qualified access at a nonzero offset makes agbcc materialize a second literal with the offset
|
|
259
|
+
* folded in (`.word 0x40000dc` + `ldr r0, [r0]`) where the source read `[r4, #0x8]` off the base
|
|
260
|
+
* it had loaded — the `dmaRegs[2];` that ends a DMA macro. `leafLocal` maps a hoisted leaf to its
|
|
261
|
+
* local, whichever key hoisted it. */
|
|
262
|
+
function rewrite(e: Expr, localFor: Map<string, string>, leafLocal: ReadonlyMap<string, string>): Expr {
|
|
253
263
|
if (e.k === 'index' && isHoistableBase(e.base)) {
|
|
254
264
|
const nm = localFor.get(keyOf(e.base, e.width, e.signed));
|
|
255
265
|
if (nm) {
|
|
256
|
-
return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor) };
|
|
266
|
+
return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor, leafLocal) };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (e.k === 'index' && e.base.k === 'cast' && e.base.volatile === true && isHoistableLeaf(e.base.e)) {
|
|
270
|
+
const nm = leafLocal.get(leafId(e.base.e));
|
|
271
|
+
if (nm) {
|
|
272
|
+
return {
|
|
273
|
+
...e,
|
|
274
|
+
base: { ...e.base, e: { k: 'var', name: nm } },
|
|
275
|
+
idx: rewrite(e.idx, localFor, leafLocal),
|
|
276
|
+
};
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
|
-
return mapExprChildren(e, (c) => rewrite(c, localFor));
|
|
279
|
+
return mapExprChildren(e, (c) => rewrite(c, localFor, leafLocal));
|
|
260
280
|
}
|
|
261
281
|
|
|
262
282
|
/** One base under consideration, keyed as `(base, width, signedness)`. */
|
|
@@ -784,7 +804,15 @@ export function hoistBaseLocals(
|
|
|
784
804
|
});
|
|
785
805
|
}
|
|
786
806
|
|
|
787
|
-
const
|
|
807
|
+
const leafLocal = new Map<string, string>();
|
|
808
|
+
for (const k of hoisted) {
|
|
809
|
+
const b = meta.get(k)!.base;
|
|
810
|
+
const leaf = leafId(b.k === 'cast' ? b.e : b);
|
|
811
|
+
if (!leafLocal.has(leaf)) {
|
|
812
|
+
leafLocal.set(leaf, localFor.get(k)!);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const rewritten = sfn.body.map((s) => mapStmtExprs(s, (e) => rewrite(e, localFor, leafLocal)));
|
|
788
816
|
// The new inits join the tree's LEADING run of base inits rather than being prepended above it:
|
|
789
817
|
// when rank's /livebase re-runs this pass the head already carries the default run's, and
|
|
790
818
|
// blindly prepending would spell the new base's pool load above locals the compiler loads first.
|