@asmlift/core 0.6.0 → 0.7.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 +2 -2
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/contracts.ts +60 -11
- package/src/frontend/ssa.ts +1 -1
- package/src/frontend/thumb.ts +2 -2
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +8 -0
- package/src/ir/opcodes.ts +43 -7
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +4 -4
- package/src/l3/ast.ts +65 -21
- package/src/l3/basecse.ts +48 -28
- package/src/l3/coalesce.ts +9 -9
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +28 -25
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +13 -13
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/pattern/engine.ts +4 -4
- package/src/pipeline.ts +17 -5
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +24 -2
- package/src/raise/pre-recovery.ts +90 -25
- package/src/raise/retsink.ts +381 -15
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +4 -4
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1148 -803
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/structure.ts +1415 -200
- package/src/structure/switch-recover.ts +100 -7
- package/src/symbols.ts +127 -6
- package/src/target.ts +155 -35
- package/src/trace.ts +1 -1
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/README.md
CHANGED
|
@@ -85,8 +85,8 @@ injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
|
|
|
85
85
|
| `frontend/{thumb,mips,ppc}.ts` | ISA frontends: decode → CFG → L1 with **Braun-2013 block-arg SSA** (`ssa.ts`), incl. loops, calls (signature-driven arity), memory, jump tables. Shared scaffolding: `disasm.ts` (objdump parsing), `splat.ts` (Splat-dialect MIPS → objdump-shaped instrs), `format.ts` (input-format classification), `emit.ts` (per-block emitter kit + `switch_br`), `opaque.ts` (the unmodelled-op → loud-`opaque` contract), `errors.ts` (`FrontendUnsupportedError`; PPC's subclass), `registry.ts`, `asmdata.ts` (Regime-B jump-table side-table) |
|
|
86
86
|
| `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
|
|
87
87
|
| `raise/*.ts` | The pre-recovery recognizers, in ONE ordered list (`pre-recovery.ts`): const materialize → magic division (`magicdiv.ts`, Hacker's Delight inverse) → soft division → array legalize → struct-array → member-array (`memberarrays.ts`, a constant offset feeding a variable-index walk is a struct's ARRAY member) → struct-pointer → short-circuit → narrow-reads (`narrow.ts`) → narrow locals (`narrowlocal.ts`, a carrier read only through its own extension IS that width) → parameter width (`paramwidth.ts`, a parameter extended in the prologue is DECLARED at that width); plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `latch.ts` (empty-latch folding), `errors.ts` (`RaiseUnsupportedError`) |
|
|
88
|
-
| `structure/*.ts` | L2→L3 in six modules: `loops.ts` (natural-loop discovery, over `ir/core.ts`'s dominators), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `hazards.ts` (the checks a loop emitter runs before committing to a loop form), `namecoalesce.ts` (copy coalescing over the interference graph — the `/merge-names`
|
|
89
|
-
| `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked
|
|
88
|
+
| `structure/*.ts` | L2→L3 in six modules: `loops.ts` (natural-loop discovery, over `ir/core.ts`'s dominators), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `hazards.ts` (the checks a loop emitter runs before committing to a loop form), `namecoalesce.ts` (copy coalescing over the interference graph — the `/merge-names` variation), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
|
|
89
|
+
| `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked respell variations `regspell.ts` + `reindex.ts` + `argbase.ts` + `scopebase.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
|
|
90
90
|
| `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw) |
|
|
91
91
|
| `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
|
|
92
92
|
| `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly |
|
package/package.json
CHANGED
package/src/backend/cfamily.ts
CHANGED
|
@@ -80,10 +80,18 @@ export function cType(t: IrType): string {
|
|
|
80
80
|
|
|
81
81
|
/** Declare a name of a given type, C declarator rules: an array puts its length AFTER the name
|
|
82
82
|
* (`u8 _pad[4]`), a pointer binds its `*` to the declarator (`void *p`), everything else is
|
|
83
|
-
* the prefix `cType name`.
|
|
83
|
+
* the prefix `cType name`. A NESTED array spells every extent after the name in declaration
|
|
84
|
+
* order (`u8 unk8[6][8]`) — one declarator, not an element type that is itself an array, which
|
|
85
|
+
* C has no syntax for and `cType` marks ill-formed as a prefix. */
|
|
84
86
|
function cDeclare(t: IrType, name: string): string {
|
|
85
87
|
if (t.kind === 'array') {
|
|
86
|
-
|
|
88
|
+
const extents: number[] = [];
|
|
89
|
+
let e: IrType = t;
|
|
90
|
+
while (e.kind === 'array') {
|
|
91
|
+
extents.push(e.count);
|
|
92
|
+
e = e.elem;
|
|
93
|
+
}
|
|
94
|
+
return `${cType(e)} ${name}${extents.map((n) => `[${n}]`).join('')}`;
|
|
87
95
|
}
|
|
88
96
|
if (t.kind === 'ptr') {
|
|
89
97
|
return `${cType(t.to)} *${name}`;
|
|
@@ -136,7 +144,19 @@ export type LeafHook = (e: Expr, rec: (e: Expr, p: number) => string) => string
|
|
|
136
144
|
* -fprologue-bugfix`), and referring to a volatile object through a non-volatile lvalue is
|
|
137
145
|
* undefined behaviour (C99 6.7.3p5). */
|
|
138
146
|
function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Expr {
|
|
139
|
-
|
|
147
|
+
// `baseElem` states the element type the base's own DECLARATION gives it, where the type walk
|
|
148
|
+
// cannot reconstruct one (a map-declared array MEMBER — see l3/ast.ts). It is a FALLBACK, not an
|
|
149
|
+
// override, and the order is the whole guard: the walk reads the tree in front of it, so
|
|
150
|
+
// wherever it answers at all it is the better answer and the stated one is not consulted.
|
|
151
|
+
// Inverted (`declared ?? walked`), a stale statement would stand — `derefStrideOk` cannot catch
|
|
152
|
+
// one, because it tests the STATED type against the access width and never against the base.
|
|
153
|
+
// No pass carries a statement onto a foreign base today (each refuses a `field` base by its own
|
|
154
|
+
// predicate: basecse `isHoistableBase`, scopebase/argbase `eligible`, nearbase's untouched
|
|
155
|
+
// `field` subtree, reindex's `base.k === 'var'`), but `mapExprChildren` spreads the field across
|
|
156
|
+
// an arbitrary base substitution, so that is five accidents rather than a check. This ordering
|
|
157
|
+
// is the check. Measured: zero test moves either way.
|
|
158
|
+
const declared = ix.baseElem !== undefined ? T.ptr(ix.baseElem) : undefined;
|
|
159
|
+
return derefStrideOk(exprCType(ix.base, vt.type) ?? declared, ix.width, ix.signed)
|
|
140
160
|
? ix.base
|
|
141
161
|
: {
|
|
142
162
|
k: 'cast',
|
|
@@ -226,11 +246,19 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
|
|
|
226
246
|
// Leading constant subscripts (a multidimensional array global's bare spelling) keep the
|
|
227
247
|
// postfix form whatever `idx` is: `g[0][0]` is the element, `*g[0]` would be its ROW.
|
|
228
248
|
//
|
|
229
|
-
// `lead` implies the base already strides the access width
|
|
230
|
-
// matching element type for the
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
249
|
+
// `lead` implies the base already strides the access width, and its producers reach that by
|
|
250
|
+
// TWO different mechanisms. The two GLOBAL ones register a matching element type for the
|
|
251
|
+
// global (structure/globalaccess.ts `bareArrayLead` and `declaredSubscripts`, both through
|
|
252
|
+
// structure.ts's `noteGlobal`, so the type walk finds it); the MEMBER one
|
|
253
|
+
// (structure.ts `pointeeElement`, `gPtr->grid[0][i]`) has no global to register and states
|
|
254
|
+
// the element type on the node instead (`baseElem`). Nothing else enforced either, and the
|
|
255
|
+
// failure would be quiet-ish: legalization would wrap the base, spelling `((u16 *)g)[0][i]`,
|
|
256
|
+
// which subscripts a `u16` twice. Check it rather than assume.
|
|
257
|
+
//
|
|
258
|
+
// This throw covers rank >= 2 ONLY, because only a rank >= 2 access carries `lead`. A rank-1
|
|
259
|
+
// member (the large majority of the corpus's dims-carrying members) has no guard here: a
|
|
260
|
+
// missing or stale `baseElem` there degrades silently to the cast form the rule replaces,
|
|
261
|
+
// which is a lost spelling rather than a wrong address.
|
|
234
262
|
if (e.lead && e.lead.length > 0) {
|
|
235
263
|
if (base !== e.base) {
|
|
236
264
|
throw new Error(
|
|
@@ -396,8 +424,8 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
|
|
|
396
424
|
// arm first, loudly), so it is a contract on the next one.
|
|
397
425
|
//
|
|
398
426
|
// `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
|
|
399
|
-
// statement, which a `switch` is not, so it already means what L3 means. What a
|
|
400
|
-
//
|
|
427
|
+
// statement, which a `switch` is not, so it already means what L3 means. What a pass that re-spells
|
|
428
|
+
// a loop must preserve is exactly that — the three that can re-spell a loop into one whose
|
|
401
429
|
// `continue` would run a different increment (`recognizeForLoops` in structure.ts,
|
|
402
430
|
// `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
|
|
403
431
|
// before firing, and a fourth must too.
|
|
@@ -539,7 +567,7 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
|
|
|
539
567
|
// differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
|
|
540
568
|
// pointer declarator to the pointee — the INNERMOST one for a multi-level pointer
|
|
541
569
|
// (`volatile u16 ** p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
|
|
542
|
-
// no
|
|
570
|
+
// no variation or recognizer produces one.
|
|
543
571
|
lines.push(` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
|
|
544
572
|
}
|
|
545
573
|
for (const s of fn.body) {
|
package/src/contracts.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
stmtLists,
|
|
17
17
|
walkExprs,
|
|
18
18
|
} from './l3/ast';
|
|
19
|
+
import { mentionedLocals } from './l3/mentions';
|
|
19
20
|
import { declaredTypes, exprCType } from './l3/typing';
|
|
20
21
|
|
|
21
22
|
export class ContractError extends Error {
|
|
@@ -277,10 +278,58 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
/**
|
|
281
|
+
/** After a respell variation: a local a pass DELETED from the declaration list is named nowhere in the tree it
|
|
282
|
+
* produced.
|
|
283
|
+
*
|
|
284
|
+
* THE FAILURE THIS CATCHES is the mirror of `assertLocalsWritten` above, and the three contracts
|
|
285
|
+
* beside it do not see it. A pass that consumes a local — l3/unmerge.ts substituting a merge temp
|
|
286
|
+
* into the arms, l3/coalesce.ts folding two names into one, l3/inlinebase.ts deleting a
|
|
287
|
+
* const-address pointer — drops the name from `sfn.locals` on the strength of an in-pass count
|
|
288
|
+
* that it is no longer mentioned. If that count is ever wrong the result is not a loud variation
|
|
289
|
+
* error: it is a candidate handed to the compiler with an undeclared identifier. Normally that is
|
|
290
|
+
* a dropped candidate, but in the REAL tier the candidate is compiled inside the project's
|
|
291
|
+
* vendored translation unit, where an orphaned name that collides with a context symbol compiles
|
|
292
|
+
* and scores. Measured on the shape that inhabits it — `locals = [p]`, body `p = 0; v16 = 1;
|
|
293
|
+
* *p = v16;` — `assertResolved`, `assertDerefsTyped` and `assertLocalsWritten` all pass: the name
|
|
294
|
+
* is neither `?` nor `undefined`, it is well-typed, and the question the third one asks is the
|
|
295
|
+
* OPPOSITE one (read but never written).
|
|
296
|
+
*
|
|
297
|
+
* A DIFFERENTIAL, and that is what makes it safe to run on every respelled tree. "Every name the tree
|
|
298
|
+
* mentions is declared" is NOT the invariant and would refuse correct output everywhere:
|
|
299
|
+
* structure.ts spells a write to a scalar global as a bare `assign` whose name is declared in the
|
|
300
|
+
* project's headers and nowhere in the tree (`gBlendValue = v;` — 71 such occurrences across 22
|
|
301
|
+
* winning sources, per l3/unmerge.ts's own note), and `SFn.globals` is the symbol-map-shaped
|
|
302
|
+
* subset, not that population. So the check speaks only about names the tree ITSELF declared a
|
|
303
|
+
* moment ago and the pass then removed — a set with no legitimate inhabitant, because a pass that
|
|
304
|
+
* drops a declaration is asserting exactly this.
|
|
305
|
+
*
|
|
306
|
+
* `addr` counts, like everywhere else: `&v` names the object as surely as a read does. */
|
|
307
|
+
export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
|
|
308
|
+
const kept = new Set(after.locals.map((l) => l.name));
|
|
309
|
+
const dropped = new Set(before.locals.map((l) => l.name).filter((n) => !kept.has(n)));
|
|
310
|
+
if (!dropped.size) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// `l3/mentions.ts`'s walk, not a third copy of the node vocabulary — this is the LOUD BACKSTOP
|
|
314
|
+
// for the mistake that predicate guards, so it is the last place that should own its own.
|
|
315
|
+
// Locals only, deliberately: no L3 respell variation drops `SFn.params` (`pruneDeadParams` is L1 block
|
|
316
|
+
// params, ir/simplify.ts), so a params arm here would be a refusal with no inhabitant.
|
|
317
|
+
const found = mentionedLocals(after.body, dropped);
|
|
318
|
+
if (found.size) {
|
|
319
|
+
throw new ContractError(
|
|
320
|
+
`a respell variation deleted the declaration of ${[...found]
|
|
321
|
+
.sort()
|
|
322
|
+
.map((n) => `'${n}'`)
|
|
323
|
+
.join(', ')} in '${after.name}' ` +
|
|
324
|
+
`while the tree still names ${found.size > 1 ? 'them' : 'it'} — an undeclared identifier in the candidate`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
|
|
281
330
|
* that local's assignment has already run.
|
|
282
331
|
*
|
|
283
|
-
* THE failure a placing
|
|
332
|
+
* THE failure a placing variation can ship, and the only one the byte differ rewards: a base local whose
|
|
284
333
|
* assignment does not reach a use is a DIFFERENT VARIABLE — C that compiles, scores, and can win
|
|
285
334
|
* (the shape #106 shipped). `contracts.ts`'s `assertLocalsWritten` does not see it: it accumulates
|
|
286
335
|
* reads and writes as SETS over the whole body, so a local assigned in one arm and read after the
|
|
@@ -288,16 +337,16 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
288
337
|
*
|
|
289
338
|
* Checked on the EMITTED tree rather than argued from the plan, because the plan is what a bug
|
|
290
339
|
* would be in. `rank.ts`'s `respell` catches the throw and drops the candidate, so the wrong
|
|
291
|
-
* answer becomes a reported
|
|
340
|
+
* answer becomes a reported variation error instead of a scored spelling.
|
|
292
341
|
*
|
|
293
342
|
* IT LIVES HERE, beside `assertLocalsWritten`, because it has that check's population and that
|
|
294
|
-
* check's call site:
|
|
343
|
+
* check's call site: respell variations that place a def — l3/sinkinit.ts, l3/basecse.ts's first-use policy,
|
|
295
344
|
* l3/nearbase.ts, l3/reindex.ts, l3/scopebase.ts, l3/argbase.ts — are the population that can
|
|
296
|
-
* produce the failure, so the check belongs on every
|
|
345
|
+
* produce the failure, so the check belongs on every respelled tree rather than on one variation's.
|
|
297
346
|
*
|
|
298
|
-
* Called ABSOLUTELY by the placing
|
|
347
|
+
* Called ABSOLUTELY by the placing variations that put an init inside a nested list, each over its own
|
|
299
348
|
* plan; everywhere else it is reached through `assertPlacementSurvives` below, which is a
|
|
300
|
-
* DIFFERENTIAL — so a placement no
|
|
349
|
+
* DIFFERENTIAL — so a placement no respelled tree ever satisfied is not judged, and a variation that
|
|
301
350
|
* mints nothing is not judged at all.
|
|
302
351
|
*
|
|
303
352
|
* A nested list gets a COPY of the reaching set, so an assignment inside one arm does not count as
|
|
@@ -357,13 +406,13 @@ export function assertHoistsDominate(sfn: SFn, minted: ReadonlySet<string>): voi
|
|
|
357
406
|
}
|
|
358
407
|
|
|
359
408
|
/** The same guarantee across a re-spelling that MOVES statements over a placement another pass
|
|
360
|
-
* already made — `rank.ts`'s
|
|
361
|
-
* onto every
|
|
409
|
+
* already made — `rank.ts`'s stacked variations (`/initfirst`, `/pollguard`, `/pollread`), derived
|
|
410
|
+
* onto every respelled tree after the variation placed its defs, and the variation-on-variation compositions in
|
|
362
411
|
* the same file where a def-moving pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
|
|
363
|
-
* `reindexWalks`) runs on a tree a placing
|
|
412
|
+
* `reindexWalks`) runs on a tree a placing variation built. `pollReads` folds a materialized re-read
|
|
364
413
|
* back into a loop condition, which is exactly such a move.
|
|
365
414
|
*
|
|
366
|
-
* A DIFFERENTIAL, which is what makes it safe on every
|
|
415
|
+
* A DIFFERENTIAL, which is what makes it safe on every variation: the walk judges the reshaped tree
|
|
367
416
|
* only where it already described the unshaped one, so a placement it cannot model (a def inside
|
|
368
417
|
* a loop body read earlier in the same body is assigned on every iteration but the first) is not
|
|
369
418
|
* judged either way. `minted` may name a local `before` does not carry — a mover mints its own —
|
package/src/frontend/ssa.ts
CHANGED
|
@@ -742,7 +742,7 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
|
|
|
742
742
|
* an attr only one of an otherwise-matching pair carries would cost a recovery. */
|
|
743
743
|
const setupArgc = new WeakMap<Op, number>();
|
|
744
744
|
|
|
745
|
-
/** Whether anything in `fn` HAS the narrower reading — the
|
|
745
|
+
/** Whether anything in `fn` HAS the narrower reading — the variation's gate, so the ~99% of functions
|
|
746
746
|
* with no narrowable call cost no re-lift. Read it off the lift itself: a later pipeline stage may
|
|
747
747
|
* replace a `call` op (softdiv rewrites one to a division), and the table is keyed by op. */
|
|
748
748
|
export function hasSetupArgsNarrowing(fn: Fn): boolean {
|
package/src/frontend/thumb.ts
CHANGED
|
@@ -932,7 +932,7 @@ function decode(
|
|
|
932
932
|
const ad = rest.match(/^\.(align|balign|p2align)([wl]?)\b\s*(.*)$/i);
|
|
933
933
|
if (ad) {
|
|
934
934
|
const kind = ad[1].toLowerCase();
|
|
935
|
-
const patternFill = ad[2] !== ''; // the `w`/`l`
|
|
935
|
+
const patternFill = ad[2] !== ''; // the `w`/`l` forms
|
|
936
936
|
const am = ad[3].trim().match(/^(\d{1,3})(?:\s*,\s*([^,]*?))?\s*$/); // N [, fill]; max-skip → no match
|
|
937
937
|
const n = am ? Number(am[1]) : NaN;
|
|
938
938
|
const pow = kind === 'balign' ? Math.log2(n) : n;
|
|
@@ -3416,7 +3416,7 @@ export function lift(
|
|
|
3416
3416
|
// handed to a callee arrives there through a frame this conjunct never looks at, and what bounds
|
|
3417
3417
|
// its extent is the audit's frame-accounting rule rather than anything here.
|
|
3418
3418
|
//
|
|
3419
|
-
// WHY IT IS NOT WIDENED anyway, since a wider frame is the obvious next
|
|
3419
|
+
// WHY IT IS NOT WIDENED anyway, since a wider frame is the obvious next change. Three shapes,
|
|
3420
3420
|
// each compiled with agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
|
|
3421
3421
|
// -fprologue-bugfix`, and only the first is about extent at all:
|
|
3422
3422
|
//
|
package/src/ir/alias.ts
CHANGED
|
@@ -86,6 +86,30 @@ export function globalCellOf(defs: Map<Value, Op>, addr: Value, off: number): Gl
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The NUMERIC address an access denotes, resolved through defs alone — a literal base, or the sum
|
|
91
|
+
* of two literals — plus the access's own `off`. Null when the address
|
|
92
|
+
* does not reduce to a literal: a `gaddr` (ask {@link globalCellOf} instead), a parameter, a
|
|
93
|
+
* materialized base, anything carrying a runtime term.
|
|
94
|
+
*
|
|
95
|
+
* The caller that has one asks a question about the BOARD — "is this cell a hardware register" —
|
|
96
|
+
* so a null here must be read as "unknown", never as "ordinary memory".
|
|
97
|
+
*/
|
|
98
|
+
export function constAddressOf(defs: Map<Value, Op>, addr: Value, off: number): number | null {
|
|
99
|
+
const d = defs.get(addr);
|
|
100
|
+
if (d?.opcode === 'const') {
|
|
101
|
+
return (d.attrs.value as number) + off;
|
|
102
|
+
}
|
|
103
|
+
if (d?.opcode === 'add' && d.operands.length === 2) {
|
|
104
|
+
const a = defs.get(d.operands[0]);
|
|
105
|
+
const b = defs.get(d.operands[1]);
|
|
106
|
+
if (a?.opcode === 'const' && b?.opcode === 'const') {
|
|
107
|
+
return (a.attrs.value as number) + (b.attrs.value as number) + off;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
89
113
|
/**
|
|
90
114
|
* Are these two accesses through ONE base provably different byte cells — same base value, both at
|
|
91
115
|
* a constant offset and width, ranges non-overlapping? The everyday struct interleave
|
package/src/ir/core.ts
CHANGED
|
@@ -117,6 +117,14 @@ export type SlotHomes = Map<Value, Set<number>>;
|
|
|
117
117
|
* param have no common scale. LAST write, not first: a predecessor commonly writes one key several
|
|
118
118
|
* times (1,867 of 5,283 records over three checkouts), and the edge carries what the last left.
|
|
119
119
|
*
|
|
120
|
+
* A SECOND CONSUMER READS THE ABSENCE, not the order. `structure.ts`'s `enclosingCarrierName` takes
|
|
121
|
+
* a missing `lastWrite` entry on a measured block as the licence to give a nested loop's parameter
|
|
122
|
+
* the enclosing header's NAME: the key was not written, so the machine carried the value into the
|
|
123
|
+
* inner loop in the register it had — one variable, not a copy. There a stale or over-eager "not
|
|
124
|
+
* written" is not a mis-sorted copy but a copy the emitted C no longer makes, so the absence
|
|
125
|
+
* semantics below are load-bearing for naming, and a pass that drops a record it should have
|
|
126
|
+
* folded changes spellings, not just orders.
|
|
127
|
+
*
|
|
120
128
|
* Keyed by OBJECTS (the predecessor block, the destination param), never by arg position, so the
|
|
121
129
|
* param splices in `ir/simplify.ts` cannot leave it stale. A pass that moves one block's ops into
|
|
122
130
|
* another owes `foldWriteOrder`.
|
package/src/ir/opcodes.ts
CHANGED
|
@@ -237,11 +237,11 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
|
|
|
237
237
|
/** Ops with an observable side effect: the flag on the signature, derived rather than re-listed.
|
|
238
238
|
* `isDceSafe` asks the same question of the FLAG through `opSig` rather than of this set, so the
|
|
239
239
|
* two cannot disagree. The SET's own consumers are `HOIST_UNSAFE_OPS` below, structure.ts's
|
|
240
|
-
*
|
|
240
|
+
* block-purity tests (may this block be folded into a loop header, is this exit owned),
|
|
241
241
|
* analysis.ts's memory-write barrier, divpow2's bias block (which is DELETED rather than moved),
|
|
242
|
-
* and the idiom layer's de-sequencing guard (pattern/engine.ts).
|
|
243
|
-
*
|
|
244
|
-
*
|
|
242
|
+
* and the idiom layer's de-sequencing guard (pattern/engine.ts). Derived rather than re-listed
|
|
243
|
+
* because three of those consumers each carried a hand-written copy of this membership, which is
|
|
244
|
+
* how the models drifted apart. */
|
|
245
245
|
export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
246
246
|
(Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
|
|
247
247
|
);
|
|
@@ -254,8 +254,9 @@ export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
|
254
254
|
* is raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
|
|
255
255
|
* inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short-circuit
|
|
256
256
|
* re-guards it. Adding the two reads
|
|
257
|
-
* here
|
|
258
|
-
* synthetic:strcmp1), so the argument
|
|
257
|
+
* here cost three byte-matches (kleod:UpdateHUDCounterDisplay, retired 2026-09-13 with its source,
|
|
258
|
+
* plus synthetic:breakloop and synthetic:strcmp1), so the argument was load-bearing rather than
|
|
259
|
+
* merely plausible. Not re-measured since the kleod row's retirement.
|
|
259
260
|
*
|
|
260
261
|
* KNOWN GAP: the trapping divides are absent too, and there the re-guard argument does NOT carry
|
|
261
262
|
* — a hoisted `sdiv` that the structurer NAMES becomes an unconditional statement. Left as it is
|
|
@@ -271,9 +272,20 @@ export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
|
271
272
|
* re-guard at the new point — actually holds at your call site. */
|
|
272
273
|
export const HOIST_UNSAFE_OPS: ReadonlySet<string> = EFFECTFUL_OPS;
|
|
273
274
|
|
|
275
|
+
/** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
|
|
276
|
+
* `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
|
|
277
|
+
* Authored rather than derived — no registry field records the operand ROLE, and the only other
|
|
278
|
+
* memory-touching opcode is `call`, whose variadic operands are arguments and not a base.
|
|
279
|
+
*
|
|
280
|
+
* Two consumers ask two different questions of it and both need the same answer, which is why it
|
|
281
|
+
* is here and not next to either: `structure/analysis.ts` uses it for the address-home variation's slot
|
|
282
|
+
* model, and `raise/const.ts` to recognise a folded literal that IS an address. */
|
|
283
|
+
export const MEM_BASE_OPS: ReadonlySet<string> = new Set(['load', 'store', 'aload', 'astore']);
|
|
284
|
+
|
|
274
285
|
/** Ops whose answer depends on WHERE they run: an effect (its order against other effects is
|
|
275
286
|
* observable) or a memory read (it answers whichever stores ran before it). The question a pass
|
|
276
|
-
* asks before moving a computation to another point on the SAME path.
|
|
287
|
+
* asks before moving a computation to another point on the SAME path. `SPELLED_WHEN_DEAD_OPS`
|
|
288
|
+
* below asks a DIFFERENT question and derives, today, the same set. */
|
|
277
289
|
export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
|
|
278
290
|
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
279
291
|
const sig = OPCODES[k] as OpSig;
|
|
@@ -281,6 +293,30 @@ export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
|
|
|
281
293
|
}),
|
|
282
294
|
);
|
|
283
295
|
|
|
296
|
+
/** Ops the structurer must still SPELL when nothing consumes their result — an effect, or a memory
|
|
297
|
+
* READ. Consumer: structure.ts's `sideEffects` walk.
|
|
298
|
+
*
|
|
299
|
+
* The read half is the entry worth arguing, because `reads` documents the OPPOSITE about C — a
|
|
300
|
+
* load nobody reads is deletable, nothing observes it. That is the C claim. The COMPILER claim
|
|
301
|
+
* points the other way: an optimizing compiler deletes every dead read it is ALLOWED to delete, so
|
|
302
|
+
* one still in the target is evidence the source's access was `volatile`. Membership here only
|
|
303
|
+
* says the structurer may not drop the op silently; whether a `volatile` can actually reach the
|
|
304
|
+
* access is a second, ADDRESS-level question the call site asks separately
|
|
305
|
+
* (structure.ts `volatileQualifiable`), and a read it answers no to is dropped as before.
|
|
306
|
+
*
|
|
307
|
+
* DERIVED FROM THE REGISTRY, not aliased to `ORDER_SENSITIVE_OPS`, even though the two are
|
|
308
|
+
* extensionally identical today and `HOIST_UNSAFE_OPS` above does alias `EFFECTFUL_OPS`. An alias
|
|
309
|
+
* makes two DIFFERENT questions incapable of ever differing, so the day one of them acquires an
|
|
310
|
+
* opcode the other should not have, the edit lands on both silently. The identity is pinned by a
|
|
311
|
+
* test instead (test/pattern.test.ts), where a future divergence surfaces as a decision to make
|
|
312
|
+
* rather than a coupling nobody sees. */
|
|
313
|
+
export const SPELLED_WHEN_DEAD_OPS: ReadonlySet<string> = new Set(
|
|
314
|
+
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
315
|
+
const sig = OPCODES[k] as OpSig;
|
|
316
|
+
return sig.effects || sig.reads;
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
|
|
284
320
|
/** Ops that may not be RE-EVALUATED at another program point — order-sensitive, or trapping. The
|
|
285
321
|
* trap half is what separates this from `ORDER_SENSITIVE_OPS`: it only matters when the new point
|
|
286
322
|
* can be reached on a path the old one was not, so a consumer that merely re-orders on one path
|
package/src/ir/simplify.ts
CHANGED
|
@@ -105,7 +105,7 @@ function trivialPhiValue(incoming: readonly Successor[], i: number, param: Value
|
|
|
105
105
|
export function firstTrivialPhi(fn: Fn): { block: Block; param: Value } | null {
|
|
106
106
|
// ONE pass over the successor edges, indexed by target — `simplifyTrivialPhis` rescans the
|
|
107
107
|
// whole function per block, which is fine for a mutating fixpoint and not for a check on the
|
|
108
|
-
// raising tower's hot path (a candidate fan re-raises the same function once per lift
|
|
108
|
+
// raising tower's hot path (a candidate fan re-raises the same function once per lift).
|
|
109
109
|
const incomingOf = new Map<Block, Successor[]>();
|
|
110
110
|
for (const pb of fn.blocks) {
|
|
111
111
|
for (const op of pb.ops) {
|
package/src/l3/address.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// declares which one it means instead of restating four lines and drifting.
|
|
8
8
|
//
|
|
9
9
|
// • baseConst — a deref BASE, through SCALAR pointer casts only. A cast to a STRUCT pointer is
|
|
10
|
-
// the dot-form's base and is refused, because a
|
|
11
|
-
// stride (`((struct S *)K)[i].f` is not `((u8 *)K)[…]`). This is the reading a
|
|
10
|
+
// the dot-form's base and is refused, because a respell variation that re-spells THROUGH it collapses the
|
|
11
|
+
// stride (`((struct S *)K)[i].f` is not `((u8 *)K)[…]`). This is the reading a respell variation that
|
|
12
12
|
// REWRITES the base needs: l3/nearbase.ts's clusters, l3/volstore.ts's qualifier.
|
|
13
13
|
// • addrConst — the address an expression IS, through ANY pointer cast. Wider, and safe
|
|
14
14
|
// because nothing re-spells through it: l3/volatileptr.ts counts volatility claims with it.
|