@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/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 `bareArrayIndex`, 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,8 +344,44 @@ 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
|
+
//
|
|
356
|
+
// `label` names a switch-scoped `break` a case-end `goto` lands on (`L: break;`, see `goto`).
|
|
357
|
+
| { k: 'break'; ofSwitch?: true; label?: string }
|
|
300
358
|
| { k: 'continue' }
|
|
359
|
+
// `goto <label>;` — L3 HOLDS TWO GOTOS, both narrow (docs/asmlift-101.md §2.7).
|
|
360
|
+
//
|
|
361
|
+
// A CASE-LABEL goto jumps to a case label of the switch it sits in, written where two or more arms
|
|
362
|
+
// enter one sibling arm's body and only one of them can reach it by falling through (`case 1: if
|
|
363
|
+
// (c) continue; goto L; case 2: … case 3: L: …`). `label` names the target arm's
|
|
364
|
+
// `SwitchCase.label`. It is the LAST statement of a case or default body, at that body's top
|
|
365
|
+
// level, and its label is a case of THAT switch — so no loop or nested switch stands between the
|
|
366
|
+
// jump and its target, and the jump runs exactly what a fall-through into that arm would run.
|
|
367
|
+
//
|
|
368
|
+
// A CASE-END goto is the next kind, `caseEndGoto`. Both are checked by `caseGotoViolation`
|
|
369
|
+
// (contracts.ts `assertCaseGotosNarrow`, which every structured and respelled tree passes). Any
|
|
370
|
+
// other goto is not representable. A goto ends its path like `break` and `continue`. SGI/IDO
|
|
371
|
+
// Pascal has no goto into a `case-of` arm, so its backend loud-fails it.
|
|
372
|
+
| { k: 'goto'; label: string }
|
|
373
|
+
// `goto <label>;` — THE CASE-END GOTO, the second goto L3 holds, a jump to the end of the arm it
|
|
374
|
+
// sits in: `label` names a labelled switch-scoped `break` (`L: break;`) that is an element of a
|
|
375
|
+
// statement list enclosing the goto, later in that list than the statement holding it, with no
|
|
376
|
+
// switch between the two and no loop around the label inside the arm — so the jump leaves every
|
|
377
|
+
// loop it sits in and then the switch, and runs nothing on the way. It is the source's `for (j…)
|
|
378
|
+
// { if (hit) { …; goto L; } } k(i); L: break;`, which agbcc threads past the loop around the
|
|
379
|
+
// switch (jump.c `follow_jumps`). Its own kind, not a flag on `goto`, so every pass that reads a
|
|
380
|
+
// `goto` as the jump into a case arm (`switchArmSuccessors`, `switchJumpsBack`) never meets one,
|
|
381
|
+
// and every exhaustive switch over `Stmt` decides it. It ends its path, and leaves the innermost
|
|
382
|
+
// switch around it. Written only by l3/caseendgoto.ts. SGI/IDO Pascal has no `break`, so its
|
|
383
|
+
// backend loud-fails it.
|
|
384
|
+
| { k: 'caseEndGoto'; label: string }
|
|
301
385
|
// A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
|
|
302
386
|
// a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
|
|
303
387
|
// is emitted after `defaultAt` of them, or after all of them when that is absent.
|
|
@@ -342,6 +426,10 @@ export interface SwitchCase {
|
|
|
342
426
|
values: number[];
|
|
343
427
|
body: Stmt[];
|
|
344
428
|
fallsThrough: boolean;
|
|
429
|
+
/** The C label printed after this arm's `case` labels, present only where a `goto` of this same
|
|
430
|
+
* switch names it (see the `goto` statement). A pass that merges, drops or reorders arms keeps it
|
|
431
|
+
* on the arm it labels. */
|
|
432
|
+
label?: string;
|
|
345
433
|
}
|
|
346
434
|
|
|
347
435
|
export interface SFn {
|
|
@@ -399,6 +487,13 @@ export interface SFn {
|
|
|
399
487
|
* one are deliberately left unstamped even where the offset is in hand — see the refusal at
|
|
400
488
|
* the structurer's build site for the measurement that decided it. */
|
|
401
489
|
slots?: number[];
|
|
490
|
+
/** The declaration's initializer, element by element, null where the source left one out
|
|
491
|
+
* (`SImage.init`) — present only on the copy the C-family emitter prints, where
|
|
492
|
+
* l3/constinit.ts folded the block copy of the compiler's anonymous constant image into it
|
|
493
|
+
* (`u8 sp0[16] = { … };`). Never on a tree a pass sees. */
|
|
494
|
+
init?: (number | InitAddress | null)[];
|
|
495
|
+
/** that initializer is spelled as a string literal — the image was written as one */
|
|
496
|
+
initString?: true;
|
|
402
497
|
}[];
|
|
403
498
|
/** project globals referenced with a known declaration shape (symbol map) — typed for the
|
|
404
499
|
* legalization env (exprCType) but NEVER declared by a backend: the project's own headers
|
|
@@ -408,6 +503,11 @@ export interface SFn {
|
|
|
408
503
|
* the top of its body ahead of the locals. Unlike `globals`, the backend declares these: no
|
|
409
504
|
* header does, because the source defined them here. */
|
|
410
505
|
statics?: SStatic[];
|
|
506
|
+
/** The compiler's own constant images this function copies a local's initializer from
|
|
507
|
+
* (structure/local-statics.ts), under labels the source never wrote. Not statics: no declaration
|
|
508
|
+
* may spell one, so the C-family emitter folds each into the initializer of the local its copy
|
|
509
|
+
* fills (l3/constinit.ts), and a backend that cannot refuses. */
|
|
510
|
+
images?: SImage[];
|
|
411
511
|
retType: IrType;
|
|
412
512
|
body: Stmt[];
|
|
413
513
|
/** Struct types this function's fields reference, declared above it by the backend. Empty
|
|
@@ -426,31 +526,64 @@ export interface SFn {
|
|
|
426
526
|
* and `l3/slotorder.ts` is the identity — never "ascending by default". */
|
|
427
527
|
slotOrder?: 'ascending' | 'descending';
|
|
428
528
|
/** 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
|
|
529
|
+
* what an argument converts to. Read by the C-family printer where `dialect` says C++ or
|
|
530
|
+
* `strictConversions` is set, either of which refuses three conversions C makes implicitly
|
|
531
|
+
* (backend/cfamily.ts `argConversion`), and by the pointer-cell conversions in every dialect
|
|
532
|
+
* (l3/ptrcell.ts). */
|
|
432
533
|
declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
|
|
433
534
|
/** Each declared callee's return type as the project spells it, never printed: whether a call a
|
|
434
535
|
* pointer cell is compared with returns a pointer (l3/ptrcell.ts). */
|
|
435
536
|
declaredReturns?: Readonly<Record<string, string>>;
|
|
436
537
|
/** The emitted C is compiled AS C++. An emission policy like `slotOrder`. */
|
|
437
538
|
dialect?: 'c++';
|
|
539
|
+
/** The compiler parses a designated element in an initializer (`[8] = 2`) — a fact of its
|
|
540
|
+
* dialect, carried from `compilerBehaviors.designatedInitializers`. An emission policy: the
|
|
541
|
+
* C-family printer spells an initializer's left-out run by index where present, as zeros where
|
|
542
|
+
* not. */
|
|
543
|
+
designators?: true;
|
|
544
|
+
/** The compiler refuses C's implicit pointer conversions in C as well, as C++ does — carried from
|
|
545
|
+
* `compilerBehaviors.strictPointerConversions`. An emission policy: the C-family printer casts an
|
|
546
|
+
* argument to its declared parameter (`declaredArgs`), and a pointer of another pointee written
|
|
547
|
+
* into a pointer slot, wherever it would for C++. */
|
|
548
|
+
strictConversions?: true;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** An initial element that is an ADDRESS CONSTANT (ir/core.ts `LocalObject.relocs`): `symbol`'s
|
|
552
|
+
* address plus `addend` bytes, held in the element's integer type — a global's, which a
|
|
553
|
+
* declaration outside the function names. */
|
|
554
|
+
export interface InitAddress {
|
|
555
|
+
symbol: string;
|
|
556
|
+
addend: number;
|
|
438
557
|
}
|
|
439
558
|
|
|
440
559
|
/** 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
|
-
*
|
|
560
|
+
* the initial elements in order, absent for a static with no initializer (the target's bss); an
|
|
561
|
+
* element the target relocates is the {@link InitAddress} it holds. `align` is an alignment in
|
|
562
|
+
* bytes wider than the type's own, which the definition must state. */
|
|
443
563
|
export interface SStatic {
|
|
444
564
|
name: string;
|
|
445
565
|
type: IrType;
|
|
446
566
|
const?: true;
|
|
447
567
|
align?: number;
|
|
448
|
-
init?: number[];
|
|
568
|
+
init?: (number | InitAddress)[];
|
|
449
569
|
/** the initializer is spelled as a string literal — a byte array the compiler aligns as a
|
|
450
570
|
* string (structure/local-statics.ts) */
|
|
451
571
|
string?: true;
|
|
452
572
|
}
|
|
453
573
|
|
|
574
|
+
/** One constant image of the compiler's own (ir/core.ts `LocalObject.anonymous`), named by its
|
|
575
|
+
* label: an integer array typed by the directives that wrote it, and its initializer element by
|
|
576
|
+
* element — null where the source left one out (padding in the image, which C supplies as zero),
|
|
577
|
+
* the {@link InitAddress} an element the target relocates holds, and ending at the last one it
|
|
578
|
+
* spelled, so it may be shorter than `type`. */
|
|
579
|
+
export interface SImage {
|
|
580
|
+
name: string;
|
|
581
|
+
type: IrType;
|
|
582
|
+
init: (number | InitAddress | null)[];
|
|
583
|
+
/** the image was written as a string literal (`.ascii`) */
|
|
584
|
+
string?: true;
|
|
585
|
+
}
|
|
586
|
+
|
|
454
587
|
/** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
|
|
455
588
|
* type but lives in the neutral AST so a backend can print `struct N { ... };`. */
|
|
456
589
|
export interface StructType {
|
|
@@ -471,6 +604,19 @@ export interface LanguageBackend {
|
|
|
471
604
|
* cannot print it turns a whole function that used to decompile into a loud stub. The
|
|
472
605
|
* structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
|
|
473
606
|
readonly spellsSwitchFallthrough: boolean;
|
|
607
|
+
/** Can this language spell a `break` that leaves a `switch` arm before its end
|
|
608
|
+
* (`{ k: 'break', ofSwitch: true }`)? C and C++ can; Pascal has no `break` at all, and its
|
|
609
|
+
* backend loud-fails the node. Asked for the same reason as `spellsSwitchFallthrough`: a
|
|
610
|
+
* closed arm with a mid-arm exit (`case 10: if (a) { if (b) break; g(1); } h(2); break;`) also
|
|
611
|
+
* spells without one, by copying the code the arm meets at into each path, which Pascal prints.
|
|
612
|
+
* The structurer reads it through `StructureOptions.spellSwitchBreak`. */
|
|
613
|
+
readonly spellsSwitchBreak: boolean;
|
|
614
|
+
/** Can this language spell a `return` before the end of the function? C and C++ can; Pascal
|
|
615
|
+
* cannot, and its backend loud-fails one. Asked for the same reason again: the join past early
|
|
616
|
+
* returns ends each arm that leaves early in a `return`, and the same branch also spells by
|
|
617
|
+
* copying the code below it into each arm (the return-tail copies), which Pascal prints. The
|
|
618
|
+
* structurer reads it through `StructureOptions.spellEarlyReturns`. */
|
|
619
|
+
readonly spellsEarlyReturn: boolean;
|
|
474
620
|
emit(fn: SFn): string;
|
|
475
621
|
// Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
|
|
476
622
|
// Used by the annotate-mode stub path to carry the failure reason + the original asm
|
|
@@ -560,6 +706,10 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
560
706
|
(typeof a.fn === 'string' || typeof bb.fn === 'string' ? a.fn === bb.fn : exprEquals(a.fn, bb.fn)) &&
|
|
561
707
|
(a.wide64 ?? false) === (bb.wide64 ?? false) &&
|
|
562
708
|
(a.sret === undefined ? bb.sret === undefined : bb.sret !== undefined && typeEquals(a.sret, bb.sret)) &&
|
|
709
|
+
(a.returns === undefined
|
|
710
|
+
? bb.returns === undefined
|
|
711
|
+
: bb.returns !== undefined && typeEquals(a.returns, bb.returns)) &&
|
|
712
|
+
(a.byValue ?? []).map((x) => x.at).join() === (bb.byValue ?? []).map((x) => x.at).join() &&
|
|
563
713
|
a.args.length === bb.args.length &&
|
|
564
714
|
a.args.every((x, i) => exprEquals(x, bb.args[i]))
|
|
565
715
|
);
|
|
@@ -636,6 +786,60 @@ export function stmtsEqual(a: readonly Stmt[], b: readonly Stmt[]): boolean {
|
|
|
636
786
|
return a.length === b.length && a.every((s, i) => stmtEquals(s, b[i]));
|
|
637
787
|
}
|
|
638
788
|
|
|
789
|
+
/** The node a merge KEEPS when it found `a` and `b` equal ({@link exprEquals}) and drops one: `a`,
|
|
790
|
+
* with every compare carrying `unsignedCmp` if it did in EITHER. Equality ignores the evidence
|
|
791
|
+
* because two compares that print alike are one expression to C; but when one of them was lowered
|
|
792
|
+
* from an `icmp_u` and the other from a signed compare, the kept node now stands for both paths,
|
|
793
|
+
* and the finished-tree judgment (l3/typing.ts `signedOverUnsignedCompare`) can only see the
|
|
794
|
+
* unsigned one's obligation if the node still says so. Keeping `a` bare would publish a signed
|
|
795
|
+
* `x < y` on the path that compared unsigned (an slt arm and an sltu arm merged by l3/tailmerge.ts
|
|
796
|
+
* under `/signed-cmp`). Precondition: `exprEquals(a, b)`, so the two trees walk in step. */
|
|
797
|
+
export function unionEvidence(a: Expr, b: Expr): Expr {
|
|
798
|
+
// `mapExprChildren` calls `f` in `exprChildren` order (an object literal evaluates its
|
|
799
|
+
// properties in source order), so the i-th call pairs with b's i-th child. Not by identity: one
|
|
800
|
+
// child object may sit in two positions, each with its own counterpart in `b`.
|
|
801
|
+
const bs = exprChildren(b);
|
|
802
|
+
let i = 0;
|
|
803
|
+
const out = mapExprChildren(a, (c) => unionEvidence(c, bs[i++]));
|
|
804
|
+
return out.k === 'bin' && b.k === 'bin' && b.unsignedCmp === true && out.unsignedCmp !== true
|
|
805
|
+
? { ...out, unsignedCmp: true }
|
|
806
|
+
: out;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** {@link unionEvidence} over two statements {@link stmtEquals} found equal. */
|
|
810
|
+
export function unionStmtEvidence(a: Stmt, b: Stmt): Stmt {
|
|
811
|
+
switch (a.k) {
|
|
812
|
+
case 'assign':
|
|
813
|
+
return b.k === 'assign' ? { ...a, value: unionEvidence(a.value, b.value) } : a;
|
|
814
|
+
case 'store':
|
|
815
|
+
return b.k === 'store'
|
|
816
|
+
? { ...a, lval: unionEvidence(a.lval, b.lval), value: unionEvidence(a.value, b.value) }
|
|
817
|
+
: a;
|
|
818
|
+
case 'exprstmt':
|
|
819
|
+
return b.k === 'exprstmt' ? { ...a, value: unionEvidence(a.value, b.value) } : a;
|
|
820
|
+
case 'return':
|
|
821
|
+
return b.k === 'return' && a.value !== undefined && b.value !== undefined
|
|
822
|
+
? { ...a, value: unionEvidence(a.value, b.value) }
|
|
823
|
+
: a;
|
|
824
|
+
case 'if':
|
|
825
|
+
return b.k === 'if'
|
|
826
|
+
? {
|
|
827
|
+
...a,
|
|
828
|
+
cond: unionEvidence(a.cond, b.cond),
|
|
829
|
+
then: unionStmtsEvidence(a.then, b.then),
|
|
830
|
+
else: unionStmtsEvidence(a.else, b.else),
|
|
831
|
+
}
|
|
832
|
+
: a;
|
|
833
|
+
default:
|
|
834
|
+
return a;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** {@link unionStmtEvidence} over two lists {@link stmtsEqual} found equal. */
|
|
839
|
+
export function unionStmtsEvidence(a: readonly Stmt[], b: readonly Stmt[]): Stmt[] {
|
|
840
|
+
return a.map((s, i) => unionStmtEvidence(s, b[i]));
|
|
841
|
+
}
|
|
842
|
+
|
|
639
843
|
/** THE spelling of an `opaque`'s gap reason, in one place: `structure.ts` writes it into the
|
|
640
844
|
* marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
|
|
641
845
|
* classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
|
|
@@ -730,6 +934,8 @@ export function stmtExprs(s: Stmt): Expr[] {
|
|
|
730
934
|
return [s.scrutinee];
|
|
731
935
|
case 'break':
|
|
732
936
|
case 'continue':
|
|
937
|
+
case 'goto':
|
|
938
|
+
case 'caseEndGoto':
|
|
733
939
|
return [];
|
|
734
940
|
}
|
|
735
941
|
}
|
|
@@ -769,6 +975,8 @@ export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
|
|
|
769
975
|
};
|
|
770
976
|
case 'break':
|
|
771
977
|
case 'continue':
|
|
978
|
+
case 'goto':
|
|
979
|
+
case 'caseEndGoto':
|
|
772
980
|
return s;
|
|
773
981
|
}
|
|
774
982
|
}
|
|
@@ -875,6 +1083,44 @@ function* subterms(e: Expr): Generator<Expr> {
|
|
|
875
1083
|
}
|
|
876
1084
|
}
|
|
877
1085
|
|
|
1086
|
+
export type Assign = Extract<Stmt, { k: 'assign' }>;
|
|
1087
|
+
|
|
1088
|
+
/** Every `assign` in the tree, at any nesting. ORDER-FREE: its callers ask what a name is ever
|
|
1089
|
+
* assigned, never which assignment comes first, so a switch default's position does not matter. */
|
|
1090
|
+
export function assignsIn(body: readonly Stmt[]): Assign[] {
|
|
1091
|
+
const out: Assign[] = [];
|
|
1092
|
+
const visit = (s: Stmt): void => {
|
|
1093
|
+
if (s.k === 'assign') {
|
|
1094
|
+
out.push(s);
|
|
1095
|
+
}
|
|
1096
|
+
stmtChildren(s).forEach(visit);
|
|
1097
|
+
};
|
|
1098
|
+
body.forEach(visit);
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** Grow `seed` over assignments to a fixpoint: an assignment adds its target when `propagates`
|
|
1103
|
+
* says its value carries what the set holds so far. A FLOW-INSENSITIVE taint, which is what each
|
|
1104
|
+
* caller wants — the question is what a name may EVER hold, so one assignment anywhere is
|
|
1105
|
+
* enough. Callers own the seed and the rule; this owns only the iteration. */
|
|
1106
|
+
export function taintOverAssigns(
|
|
1107
|
+
assigns: readonly Assign[],
|
|
1108
|
+
seed: Iterable<string>,
|
|
1109
|
+
propagates: (a: Assign, tainted: ReadonlySet<string>) => boolean,
|
|
1110
|
+
): Set<string> {
|
|
1111
|
+
const tainted = new Set(seed);
|
|
1112
|
+
for (let grew = true; grew;) {
|
|
1113
|
+
grew = false;
|
|
1114
|
+
for (const a of assigns) {
|
|
1115
|
+
if (!tainted.has(a.name) && propagates(a, tainted)) {
|
|
1116
|
+
tainted.add(a.name);
|
|
1117
|
+
grew = true;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return tainted;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
878
1124
|
/** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
|
|
879
1125
|
* splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
|
|
880
1126
|
* init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
|
|
@@ -887,6 +1133,8 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
887
1133
|
case 'return':
|
|
888
1134
|
case 'break':
|
|
889
1135
|
case 'continue':
|
|
1136
|
+
case 'goto':
|
|
1137
|
+
case 'caseEndGoto':
|
|
890
1138
|
return [];
|
|
891
1139
|
case 'if':
|
|
892
1140
|
return [...s.then, ...s.else];
|
|
@@ -937,10 +1185,254 @@ export function isLoop(s: Stmt): s is Loop {
|
|
|
937
1185
|
case 'return':
|
|
938
1186
|
case 'break':
|
|
939
1187
|
case 'continue':
|
|
1188
|
+
case 'goto':
|
|
1189
|
+
case 'caseEndGoto':
|
|
940
1190
|
return false;
|
|
941
1191
|
}
|
|
942
1192
|
}
|
|
943
1193
|
|
|
1194
|
+
/** Why `body` breaks the two forms a `goto` may take in L3 (the `goto` statement's contract), or
|
|
1195
|
+
* null when every `goto` in it keeps one. A case-label goto is the LAST statement of a case or
|
|
1196
|
+
* default body, at that body's top level, naming a label one case of THAT switch carries. A
|
|
1197
|
+
* case-end goto (`caseEndGoto`) names a labelled switch-scoped `break` later in a statement list that
|
|
1198
|
+
* encloses it, inside the same switch arm, with no switch between them; ifs and loops may stand
|
|
1199
|
+
* between, but no loop may stand between the labelled `break` and its arm. Labels are unique
|
|
1200
|
+
* across the function, case labels and `break` labels together. Any other placement is refused:
|
|
1201
|
+
* a case-label goto under an `if`, in a loop body, in a nested switch's arm aimed at the outer
|
|
1202
|
+
* switch or before another statement; a case-end goto in a nested switch, in another arm, outside
|
|
1203
|
+
* the switch, or after its label; a labelled `break` that is not the switch's or sits in a loop.
|
|
1204
|
+
*
|
|
1205
|
+
* Shared by contracts.ts `assertCaseGotosNarrow`, which every structured and respelled tree
|
|
1206
|
+
* passes, and the C-family printer, which asks it of every function it prints: a pass that moves
|
|
1207
|
+
* a statement across an arm boundary is caught at the one place the program is spelled. */
|
|
1208
|
+
export function caseGotoViolation(body: readonly Stmt[]): string | null {
|
|
1209
|
+
const seen = new Set<string>();
|
|
1210
|
+
const claim = (label: string): string | null => {
|
|
1211
|
+
if (seen.has(label)) {
|
|
1212
|
+
return `the label \`${label}\` is carried by two switch arms`;
|
|
1213
|
+
}
|
|
1214
|
+
seen.add(label);
|
|
1215
|
+
return null;
|
|
1216
|
+
};
|
|
1217
|
+
/** `labels`: the case labels a case-label goto here may name (null outside an arm's top level's
|
|
1218
|
+
* reach); `ends`: the `break` labels a case-end goto here may name; `inArm`: inside a switch arm
|
|
1219
|
+
* with no loop entered since it began, where a labelled `break` may sit. */
|
|
1220
|
+
type Scope = { labels: ReadonlySet<string> | null; armTop: boolean; ends: ReadonlySet<string>; inArm: boolean };
|
|
1221
|
+
const walk = (list: readonly Stmt[], scope: Scope): string | null => {
|
|
1222
|
+
// `after[i]`: the `break` labels later in this list than `i`, beside the enclosing ones — built
|
|
1223
|
+
// only for a list holding one, so the walk allocates nothing per statement on a goto-free tree
|
|
1224
|
+
const after = endsAfter(list, scope.ends);
|
|
1225
|
+
for (const [i, s] of list.entries()) {
|
|
1226
|
+
if (s.k === 'caseEndGoto') {
|
|
1227
|
+
if (!scope.ends.has(s.label)) {
|
|
1228
|
+
return `\`goto ${s.label};\` names no labelled \`break;\` that ends the switch arm it sits in`;
|
|
1229
|
+
}
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (s.k === 'goto') {
|
|
1233
|
+
if (scope.labels === null || !scope.armTop || i !== list.length - 1) {
|
|
1234
|
+
return `\`goto ${s.label};\` is not the last statement of a switch arm's own body`;
|
|
1235
|
+
}
|
|
1236
|
+
if (!scope.labels.has(s.label)) {
|
|
1237
|
+
return `\`goto ${s.label};\` names no case label of the switch it sits in`;
|
|
1238
|
+
}
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (s.k === 'break' && s.label !== undefined) {
|
|
1242
|
+
if (s.ofSwitch !== true || !scope.inArm) {
|
|
1243
|
+
return `the label \`${s.label}\` is not on a \`break;\` that leaves a switch arm`;
|
|
1244
|
+
}
|
|
1245
|
+
const why = claim(s.label);
|
|
1246
|
+
if (why !== null) {
|
|
1247
|
+
return why;
|
|
1248
|
+
}
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (s.k === 'switch') {
|
|
1252
|
+
const own = new Set<string>();
|
|
1253
|
+
for (const c of s.cases) {
|
|
1254
|
+
if (c.label !== undefined) {
|
|
1255
|
+
const why = claim(c.label);
|
|
1256
|
+
if (why !== null) {
|
|
1257
|
+
return why;
|
|
1258
|
+
}
|
|
1259
|
+
own.add(c.label);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
for (const arm of [...s.cases.map((c) => c.body), s.default ?? []]) {
|
|
1263
|
+
const why = walk(arm, { labels: own, armTop: true, ends: NO_ENDS, inArm: true });
|
|
1264
|
+
if (why !== null) {
|
|
1265
|
+
return why;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
// The `break` labels later in this list are the ends a goto inside `s` may reach. Inside a
|
|
1271
|
+
// loop the switch's case labels are out of reach (a case-label goto there would leave the
|
|
1272
|
+
// loop) and no `break;` leaves the arm.
|
|
1273
|
+
const ends = after?.[i] ?? scope.ends;
|
|
1274
|
+
const loop = isLoop(s);
|
|
1275
|
+
for (const inner of stmtLists(s)) {
|
|
1276
|
+
const why = walk(inner, {
|
|
1277
|
+
labels: loop ? null : scope.labels,
|
|
1278
|
+
armTop: false,
|
|
1279
|
+
ends,
|
|
1280
|
+
inArm: scope.inArm && !loop,
|
|
1281
|
+
});
|
|
1282
|
+
if (why !== null) {
|
|
1283
|
+
return why;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (s.k === 'for') {
|
|
1287
|
+
const why = walk([s.init, s.inc], { labels: null, armTop: false, ends: NO_ENDS, inArm: false });
|
|
1288
|
+
if (why !== null) {
|
|
1289
|
+
return why;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
};
|
|
1295
|
+
return walk(body, { labels: null, armTop: false, ends: NO_ENDS, inArm: false });
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const NO_ENDS: ReadonlySet<string> = new Set();
|
|
1299
|
+
|
|
1300
|
+
/** For each index of `list`, `outer` and the labels of the labelled `break`s that are elements of
|
|
1301
|
+
* `list` after it; undefined where `list` holds none, so every index reads `outer`. */
|
|
1302
|
+
function endsAfter(list: readonly Stmt[], outer: ReadonlySet<string>): ReadonlySet<string>[] | undefined {
|
|
1303
|
+
if (!list.some((s) => s.k === 'break' && s.label !== undefined)) {
|
|
1304
|
+
return undefined;
|
|
1305
|
+
}
|
|
1306
|
+
const out: ReadonlySet<string>[] = new Array(list.length);
|
|
1307
|
+
let later: ReadonlySet<string> = outer;
|
|
1308
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
1309
|
+
out[i] = later;
|
|
1310
|
+
const s = list[i];
|
|
1311
|
+
if (s.k === 'break' && s.label !== undefined) {
|
|
1312
|
+
later = new Set([...later, s.label]);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return out;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/** Does a case-end `goto` in `list`, at any depth, name a labelled `break` that `list` does not
|
|
1319
|
+
* hold — so the jump leaves `list`, and every loop of it the goto sits in? A pass asking whether a
|
|
1320
|
+
* loop body leaves its loop asks this beside its `break`s (l3/reindex.ts `hasLoopExit`). */
|
|
1321
|
+
export function caseEndGotoLeaves(list: readonly Stmt[]): boolean {
|
|
1322
|
+
const held = new Set<string>();
|
|
1323
|
+
const named: string[] = [];
|
|
1324
|
+
const visit = (s: Stmt): void => {
|
|
1325
|
+
if (s.k === 'break' && s.label !== undefined) {
|
|
1326
|
+
held.add(s.label);
|
|
1327
|
+
} else if (s.k === 'caseEndGoto') {
|
|
1328
|
+
named.push(s.label);
|
|
1329
|
+
}
|
|
1330
|
+
stmtChildren(s).forEach(visit);
|
|
1331
|
+
};
|
|
1332
|
+
list.forEach(visit);
|
|
1333
|
+
return named.some((l) => !held.has(l));
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** Can control fall off the END of `list`? False only where every path through it is provably
|
|
1337
|
+
* left first — by `return`, `continue`, a loop's `break`, a `goto`, a switch-scoped `break` or a
|
|
1338
|
+
* case-end `goto` (which leave the arm they sit in), or an `if` or `switch` none of whose paths
|
|
1339
|
+
* reaches its own end. A loop is read as reaching its end (a decided exit is not looked for), so
|
|
1340
|
+
* the answer errs toward "reaches", which is the side a caller appending a jump after the list
|
|
1341
|
+
* can afford: the jump it then writes is one no path runs, dead code and never a path cut. */
|
|
1342
|
+
export function reachesEnd(list: readonly Stmt[]): boolean {
|
|
1343
|
+
// `end`: some path falls off the list's last statement; `leaves`: some path takes a
|
|
1344
|
+
// switch-scoped `break` out of it, which ends the innermost switch around it.
|
|
1345
|
+
const flow = (ss: readonly Stmt[]): { end: boolean; leaves: boolean } => {
|
|
1346
|
+
let leaves = false;
|
|
1347
|
+
for (const s of ss) {
|
|
1348
|
+
switch (s.k) {
|
|
1349
|
+
case 'return':
|
|
1350
|
+
case 'continue':
|
|
1351
|
+
case 'goto':
|
|
1352
|
+
return { end: false, leaves };
|
|
1353
|
+
// a case-end goto leaves the innermost switch around it, as its labelled `break;` does
|
|
1354
|
+
case 'caseEndGoto':
|
|
1355
|
+
return { end: false, leaves: true };
|
|
1356
|
+
case 'break':
|
|
1357
|
+
return { end: false, leaves: leaves || s.ofSwitch === true };
|
|
1358
|
+
case 'if': {
|
|
1359
|
+
const t = flow(s.then);
|
|
1360
|
+
const e = flow(s.else);
|
|
1361
|
+
leaves ||= t.leaves || e.leaves;
|
|
1362
|
+
if (!t.end && !e.end) {
|
|
1363
|
+
return { end: false, leaves };
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
case 'switch': {
|
|
1368
|
+
// Its end is reached by a `break` out of an arm, by an arm that neither falls nor jumps
|
|
1369
|
+
// on and reaches its own end, or — with no `default` — by a value no case names.
|
|
1370
|
+
const next = switchArmSuccessors(s);
|
|
1371
|
+
const ends =
|
|
1372
|
+
s.default === undefined ||
|
|
1373
|
+
[...s.cases.map((c) => c.body), s.default].some((body, i) => {
|
|
1374
|
+
const f = flow(body);
|
|
1375
|
+
return f.leaves || (f.end && next[i] === undefined);
|
|
1376
|
+
});
|
|
1377
|
+
if (!ends) {
|
|
1378
|
+
return { end: false, leaves };
|
|
1379
|
+
}
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
case 'while':
|
|
1383
|
+
case 'dowhile':
|
|
1384
|
+
case 'for':
|
|
1385
|
+
case 'assign':
|
|
1386
|
+
case 'store':
|
|
1387
|
+
case 'exprstmt':
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return { end: true, leaves };
|
|
1392
|
+
};
|
|
1393
|
+
return flow(list).end;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/** For each arm of a `switch` — its cases in order, then its `default` at index `cases.length` —
|
|
1397
|
+
* the arm control runs on into from that arm's end, or undefined where it leaves the switch: the
|
|
1398
|
+
* arm a trailing `goto` names (its last top-level statement, `caseGotoViolation`), else the next
|
|
1399
|
+
* arm where the case falls through (the last case into the `default`, or out where there is
|
|
1400
|
+
* none). The one reading of "arm `i` runs on into arm `j`" that contracts.ts's effect count and
|
|
1401
|
+
* l3/coalesce.ts's arm-disjointness both follow, transitively. */
|
|
1402
|
+
export function switchArmSuccessors(sw: Extract<Stmt, { k: 'switch' }>): (number | undefined)[] {
|
|
1403
|
+
const labelled = new Map(sw.cases.flatMap((c, i) => (c.label === undefined ? [] : [[c.label, i] as const])));
|
|
1404
|
+
const jumpOf = (body: readonly Stmt[]): number | undefined => {
|
|
1405
|
+
const last = body[body.length - 1];
|
|
1406
|
+
return last?.k === 'goto' ? labelled.get(last.label) : undefined;
|
|
1407
|
+
};
|
|
1408
|
+
const last = sw.cases.length;
|
|
1409
|
+
return [
|
|
1410
|
+
...sw.cases.map(
|
|
1411
|
+
(c, i) => jumpOf(c.body) ?? (c.fallsThrough && (i + 1 < last || sw.default !== undefined) ? i + 1 : undefined),
|
|
1412
|
+
),
|
|
1413
|
+
sw.default === undefined ? undefined : jumpOf(sw.default),
|
|
1414
|
+
];
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/** Does a case-label `goto` of this switch land on an arm LAID OUT AT OR ABOVE the arm it ends —
|
|
1418
|
+
* a BACK EDGE with no loop statement around it? Then the arm it lands on runs AFTER the jumping
|
|
1419
|
+
* arm, so preorder stops being execution order inside the switch's own range, exactly as it does
|
|
1420
|
+
* inside a loop body; a pass that reads preorder as liveness must count such a switch with the
|
|
1421
|
+
* loops (l3/coalesce.ts `spans`). Positions are the layout {@link stmtChildren} walks: the
|
|
1422
|
+
* `default` sits at `defaultAt`. A fall-through only ever runs on DOWNWARD, so only the jumps are
|
|
1423
|
+
* asked. */
|
|
1424
|
+
export function switchJumpsBack(sw: Extract<Stmt, { k: 'switch' }>): boolean {
|
|
1425
|
+
const at = sw.defaultAt ?? sw.cases.length;
|
|
1426
|
+
const laidOut = (arm: number): number => (arm === sw.cases.length ? at : arm < at ? arm : arm + 1);
|
|
1427
|
+
const labelled = new Map(sw.cases.flatMap((c, i) => (c.label === undefined ? [] : [[c.label, i] as const])));
|
|
1428
|
+
const bodies = [...sw.cases.map((c) => c.body), sw.default ?? []];
|
|
1429
|
+
return bodies.some((body, i) => {
|
|
1430
|
+
const last = body[body.length - 1];
|
|
1431
|
+
const to = last?.k === 'goto' ? labelled.get(last.label) : undefined;
|
|
1432
|
+
return to !== undefined && laidOut(to) <= laidOut(i);
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
944
1436
|
/** The nested statement LISTS of a statement — the SCOPES it opens.
|
|
945
1437
|
*
|
|
946
1438
|
* Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
|
|
@@ -968,6 +1460,8 @@ export function stmtLists(s: Stmt): Stmt[][] {
|
|
|
968
1460
|
case 'return':
|
|
969
1461
|
case 'break':
|
|
970
1462
|
case 'continue':
|
|
1463
|
+
case 'goto':
|
|
1464
|
+
case 'caseEndGoto':
|
|
971
1465
|
return [];
|
|
972
1466
|
}
|
|
973
1467
|
}
|
|
@@ -997,6 +1491,8 @@ export function mapStmtLists(s: Stmt, f: (list: Stmt[]) => Stmt[]): Stmt {
|
|
|
997
1491
|
case 'return':
|
|
998
1492
|
case 'break':
|
|
999
1493
|
case 'continue':
|
|
1494
|
+
case 'goto':
|
|
1495
|
+
case 'caseEndGoto':
|
|
1000
1496
|
return s;
|
|
1001
1497
|
}
|
|
1002
1498
|
}
|
|
@@ -1077,8 +1573,76 @@ export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
|
|
|
1077
1573
|
'<=': '>',
|
|
1078
1574
|
'==': '!=',
|
|
1079
1575
|
'!=': '==',
|
|
1576
|
+
// C's float `==` is false on a NaN and `!=` true, so these two are each other's negation; the
|
|
1577
|
+
// ordered four have none in the operator vocabulary (the BinOp note).
|
|
1578
|
+
'f==': 'f!=',
|
|
1579
|
+
'f!=': 'f==',
|
|
1080
1580
|
};
|
|
1081
1581
|
|
|
1582
|
+
/** The compares under `e`'s connectives whose runtime helper names the relation the source spelled
|
|
1583
|
+
* (`helperRel`), each answering whether it spells that relation now. `negateCond` flips every one
|
|
1584
|
+
* of them at once, through De Morgan. */
|
|
1585
|
+
function helperRelLeaves(e: Expr, out: boolean[] = []): boolean[] {
|
|
1586
|
+
if (e.k === 'bin' && (e.op === '&&' || e.op === '||')) {
|
|
1587
|
+
helperRelLeaves(e.l, out);
|
|
1588
|
+
helperRelLeaves(e.r, out);
|
|
1589
|
+
} else if (e.k === 'bin' && e.helperRel !== undefined) {
|
|
1590
|
+
out.push(e.op === e.helperRel);
|
|
1591
|
+
}
|
|
1592
|
+
return out;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/** Whether `e` spells, at every compare of it whose runtime helper names the relation the source
|
|
1596
|
+
* spelled, that relation — so that negating `e` recompiles to the other helper at each of them:
|
|
1597
|
+
* the relation's twin and its `!` alike, since gcc folds `!(a == b)` into `a != b`. An arm swap
|
|
1598
|
+
* that would negate it keeps `e` and its arm order instead. */
|
|
1599
|
+
export function keepsHelperRel(e: Expr): boolean {
|
|
1600
|
+
const leaves = helperRelLeaves(e);
|
|
1601
|
+
return leaves.length > 0 && leaves.every((own) => own);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/** Whether negating `e` puts every such compare back on its helper's relation: the swap a pass
|
|
1605
|
+
* would otherwise leave alone is the one that spells the helpers. A condition mixing the two has
|
|
1606
|
+
* neither answer, and no arm order spells it. */
|
|
1607
|
+
export function regainsHelperRel(e: Expr): boolean {
|
|
1608
|
+
const leaves = helperRelLeaves(e);
|
|
1609
|
+
return leaves.length > 0 && leaves.every((own) => !own);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/** A loop's stay condition `P && S` whose compares under `S` are all negated off their helper's
|
|
1613
|
+
* relation, and whose `P` negates none: `{ head: P, guard: !S }`, which the loop spells
|
|
1614
|
+
* `while (P) { if (!S) break; … }` — the same tests in the same order, calling the helpers the
|
|
1615
|
+
* source called. `while (P && S)` has no spelling of that `S` that calls the helper (gcc folds
|
|
1616
|
+
* `!(a == b)` into `a != b`); the break guard is how the source wrote it before the branch fold
|
|
1617
|
+
* joined the two tests. Null when the condition already spells every helper's relation, or when no
|
|
1618
|
+
* split of its `&&` chain separates the two kinds of compare. */
|
|
1619
|
+
export function splitHelperBreak(cond: Expr): { head: Expr; guard: Expr } | null {
|
|
1620
|
+
if (keepsHelperRel(cond) || helperRelLeaves(cond).length === 0) {
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
const conj: Expr[] = [];
|
|
1624
|
+
const flatten = (e: Expr): void => {
|
|
1625
|
+
if (e.k === 'bin' && e.op === '&&') {
|
|
1626
|
+
flatten(e.l);
|
|
1627
|
+
flatten(e.r);
|
|
1628
|
+
} else {
|
|
1629
|
+
conj.push(e);
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
flatten(cond);
|
|
1633
|
+
const and = (xs: Expr[]): Expr => xs.reduce((l, r) => ({ k: 'bin', op: '&&', l, r }));
|
|
1634
|
+
// The longest prefix that negates no helper leaf; the rest must negate every one it holds.
|
|
1635
|
+
let k = 0;
|
|
1636
|
+
while (k < conj.length && helperRelLeaves(conj[k]).every((own) => own)) {
|
|
1637
|
+
k++;
|
|
1638
|
+
}
|
|
1639
|
+
if (k === 0 || k === conj.length) {
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
const rest = and(conj.slice(k));
|
|
1643
|
+
return regainsHelperRel(rest) ? { head: and(conj.slice(0, k)), guard: negateCond(rest) } : null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1082
1646
|
export function negateCond(e: Expr): Expr {
|
|
1083
1647
|
if (e.k === 'bin') {
|
|
1084
1648
|
const flipped = NEGATE_REL[e.op];
|