@asmlift/core 0.8.0 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +276 -80
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
package/src/l3/address.ts
CHANGED
|
@@ -33,6 +33,11 @@ export const baseConst = (e: Expr): number | null =>
|
|
|
33
33
|
? baseConst(e.e)
|
|
34
34
|
: null;
|
|
35
35
|
|
|
36
|
+
/** Does this base already assert volatility — a `volatile` cast at any depth of the cast chain? A
|
|
37
|
+
* respell variation that replaces such a base drops the qualifier with it, and every access
|
|
38
|
+
* through it becomes a plain one agbcc may delete or hoist. */
|
|
39
|
+
export const qualifiedBase = (e: Expr): boolean => e.k === 'cast' && (e.volatile === true || qualifiedBase(e.e));
|
|
40
|
+
|
|
36
41
|
/** the numeric address an expression IS, through any number of pointer casts */
|
|
37
42
|
export const addrConst = (e: Expr): number | null =>
|
|
38
43
|
e.k === 'const' ? e.value : e.k === 'cast' && e.to.kind === 'ptr' ? addrConst(e.e) : null;
|
package/src/l3/argcopy.ts
CHANGED
|
@@ -101,9 +101,12 @@ export interface ArgCopyRegionCtx {
|
|
|
101
101
|
readonly reads: number;
|
|
102
102
|
/** a loop encloses the region — its own body, or any list nested below one */
|
|
103
103
|
readonly underLoop: boolean;
|
|
104
|
+
/** the region is ONE basic block: no statement opens a nested list, and no expression holds a
|
|
105
|
+
* short-circuit connective */
|
|
106
|
+
readonly straightLine: boolean;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
/** Which regions are worth offering.
|
|
109
|
+
/** Which regions are worth offering. NONE is sound — each refuses a spelling that is correct
|
|
107
110
|
* but models nothing, and what they buy is the candidate count: every one of these is a COMPILE,
|
|
108
111
|
* and this variation multiplies with `/coalesce`. */
|
|
109
112
|
export const ARGCOPY_REGION_GATES: readonly Gate<ArgCopyRegionCtx>[] = [
|
|
@@ -117,13 +120,37 @@ export const ARGCOPY_REGION_GATES: readonly Gate<ArgCopyRegionCtx>[] = [
|
|
|
117
120
|
id: 'loop-region',
|
|
118
121
|
why: 'a copy anywhere inside a loop re-runs every iteration, so it belongs to a region that holds the loop instead — and no region does when the loop is at function top level, where this variation offers nothing at all',
|
|
119
122
|
sound: false,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
guardedBy: [
|
|
124
|
+
'argcopy.test.ts: an arm NESTED inside a loop body is refused too',
|
|
125
|
+
'argcopy.test.ts: a TOP-LEVEL loop is offered NOTHING',
|
|
126
|
+
],
|
|
123
127
|
rejects: (c) => c.underLoop,
|
|
124
128
|
},
|
|
129
|
+
{
|
|
130
|
+
id: 'straight-line',
|
|
131
|
+
why: 'a copy whose every read sits in one basic block is forward-propagated away by every compiler in the matrix, so the copy is erased — a `&&` or `||` in the region is an edge the copy can live across, and such a region is offered',
|
|
132
|
+
sound: false,
|
|
133
|
+
guardedBy: 'argcopy.test.ts: a STRAIGHT-LINE region is refused',
|
|
134
|
+
rejects: (c) => c.straightLine,
|
|
135
|
+
},
|
|
125
136
|
];
|
|
126
137
|
|
|
138
|
+
/** Whether `list` is one basic block. A nested list is a branch or a loop; a short-circuit
|
|
139
|
+
* connective is a branch too — `p[3] = p[0] > v && p[4] < v` is two blocks, and agbcc keeps a
|
|
140
|
+
* copy live across that edge (the synthetic `leafand` row matches only through it). `walkExprs` for the
|
|
141
|
+
* reason `countReads` gives: it is the walk `repoint` rewrites by. */
|
|
142
|
+
function isStraightLine(list: Stmt[]): boolean {
|
|
143
|
+
if (list.some((st) => stmtLists(st).length > 0)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
for (const e of walkExprs(list)) {
|
|
147
|
+
if (e.k === 'bin' && (e.op === '&&' || e.op === '||')) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
127
154
|
/** How many `var n` leaves the region holds, nested statements included — the region rules'
|
|
128
155
|
* yardstick for whether a copy has a range to shorten.
|
|
129
156
|
*
|
|
@@ -176,7 +203,8 @@ function regions(body: Stmt[]): { at: number[]; list: Stmt[]; underLoop: boolean
|
|
|
176
203
|
* for > do-while > if > store). This runs once per region per parameter per candidate, and each
|
|
177
204
|
* candidate is a compile. */
|
|
178
205
|
function repoint(list: Stmt[], n: string, to: string): Stmt[] {
|
|
179
|
-
const inExpr = (e: Expr): Expr =>
|
|
206
|
+
const inExpr = (e: Expr): Expr =>
|
|
207
|
+
(e.k === 'var' || e.k === 'postincr') && e.name === n ? { ...e, name: to } : mapExprChildren(e, inExpr);
|
|
180
208
|
return list.map((s) => mapStmtExprs(s, inExpr));
|
|
181
209
|
}
|
|
182
210
|
|
|
@@ -238,7 +266,11 @@ export function argCopyUnder(
|
|
|
238
266
|
if (reads === 0) {
|
|
239
267
|
continue;
|
|
240
268
|
}
|
|
241
|
-
const regionRefused = firstRejection(regionGates, {
|
|
269
|
+
const regionRefused = firstRejection(regionGates, {
|
|
270
|
+
reads,
|
|
271
|
+
underLoop: r.underLoop,
|
|
272
|
+
straightLine: isStraightLine(r.list),
|
|
273
|
+
});
|
|
242
274
|
if (regionRefused !== null) {
|
|
243
275
|
refusals.set(regionRefused, (refusals.get(regionRefused) ?? 0) + 1);
|
|
244
276
|
continue;
|
package/src/l3/ast.ts
CHANGED
|
@@ -6,8 +6,28 @@ import { type IrType, typeEquals } from '../ir/types';
|
|
|
6
6
|
export type Expr =
|
|
7
7
|
| { k: 'var'; name: string }
|
|
8
8
|
| { k: 'const'; value: number }
|
|
9
|
+
// A `double` literal, carried as its IEEE bit pattern (ir/float-bits.ts) — a kind apart from the
|
|
10
|
+
// integer `const`, so no integer fold, range test or signedness reading ever takes it for one.
|
|
11
|
+
| { k: 'fconst'; bits: string }
|
|
9
12
|
| { k: 'bin'; op: BinOp; l: Expr; r: Expr }
|
|
10
|
-
| { k: 'un'; op: '-' | '~' | '!'; e: Expr }
|
|
13
|
+
| { k: 'un'; op: '-' | '~' | '!' | 'f-'; e: Expr }
|
|
14
|
+
// A post-increment `name++` (`by` 1) or post-decrement `name--` (`by` -1): the value the local
|
|
15
|
+
// held BEFORE the update, with the update as a side effect. The only expression form here that
|
|
16
|
+
// WRITES anything, so `exprHasEffect` answers for it alongside `call` and `marker` — that is what
|
|
17
|
+
// every pass asking "may I move or duplicate this" already consults. It also NAMES a local, so
|
|
18
|
+
// `mentionedName` answers for it beside `var` and `addr`, and the two walks that classify their
|
|
19
|
+
// own leaves — `l3/mentions.ts` and `contracts.ts`'s `assertLocalsWritten` — count it on both
|
|
20
|
+
// sides, because `v++` reads the name and writes it.
|
|
21
|
+
//
|
|
22
|
+
// The target is a NAME rather than an Expr because its one producer has one: structure.ts's
|
|
23
|
+
// `emitDoWhile`, folding a loop update into a bottom test that reads the variable at its
|
|
24
|
+
// pre-update value (`while (v0 != 0 && v1++ <= 9)`). A general lvalue has no inhabitant.
|
|
25
|
+
//
|
|
26
|
+
// WHERE IT MAY STAND IS THE PRODUCER'S OBLIGATION, not a property of the node: C89 leaves the
|
|
27
|
+
// program undefined when the same object is also read elsewhere between two sequence points, and
|
|
28
|
+
// short-circuit operators decide whether the update happens at all. `PREUPDATE_COND_GATES`
|
|
29
|
+
// (structure/hazards.ts) holds both rules for the one producer.
|
|
30
|
+
| { k: 'postincr'; name: string; by: 1 | -1 }
|
|
11
31
|
// A C-style value cast `(T)e`. Tree-level producers: a width-narrowing cast `(u8)e` (the
|
|
12
32
|
// recovered form of a byte/half extend idiom — zext/sext IR ops), the STRUCT-pointer cast
|
|
13
33
|
// (structure.ts memAccess/arrayAccess struct paths — see the note on `field` below), and the
|
|
@@ -22,12 +42,42 @@ export type Expr =
|
|
|
22
42
|
// cv-qualifier, deliberately: volatility is a SPELLING, carried at the declaration or the
|
|
23
43
|
// cast, the same split SFn.locals makes for `volatile`/`pointeeVolatile`.
|
|
24
44
|
| { k: 'cast'; to: IrType; e: Expr; volatile?: true }
|
|
25
|
-
|
|
45
|
+
// A call. `wide64` is the INTEGER RANK the callee's return renders with, and it is carried on the
|
|
46
|
+
// node because nothing downstream can derive it: a callee's return type comes from a prototype
|
|
47
|
+
// outside the emitted function, so `exprCType` answers `undefined` for a call and every reader of
|
|
48
|
+
// the rendered rank would otherwise read C's implicit `int`.
|
|
49
|
+
//
|
|
50
|
+
// IT IS TRUE OF THE C THAT IS COMPILED BECAUSE THE SOURCES AGREE, and that agreement is a
|
|
51
|
+
// property of one lookup rather than of two modules happening to match. The width is set where
|
|
52
|
+
// the frontend was TOLD it — a compiler's own runtime-helper signature, whose callee the
|
|
53
|
+
// candidate never declares because the helper table IS the declaration, or a project's
|
|
54
|
+
// `FnProto.returns`, which `proto.ts` `spellableProto` admits only when the same entry can be
|
|
55
|
+
// PRINTED as the callee's own prototype. `prototypesFromSymbols` resolves that table once, so
|
|
56
|
+
// the frontend that stamps this and the collector that prints the declaration read one answer.
|
|
57
|
+
// The filters in `rank-declare.ts` can still withhold a printed declaration (a name the emitted
|
|
58
|
+
// C uses for its own storage, a non-identifier, a reserved name) — every one of them kills the
|
|
59
|
+
// SPELLING or the whole row rather than leaving this stamp standing over an implicitly-`int`
|
|
60
|
+
// callee.
|
|
61
|
+
//
|
|
62
|
+
// `sret` is the type of a struct the callee returns BY VALUE through memory. `args[0]` is then the
|
|
63
|
+
// hidden pointer the frontend read from the first argument register — the address of the struct
|
|
64
|
+
// 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.
|
|
66
|
+
//
|
|
67
|
+
// `fn` is the callee's NAME, or for an indirect call the EXPRESSION whose value is the address
|
|
68
|
+
// called (ir/core.ts `calleeValue`) — a child of the node like its arguments, listed first in
|
|
69
|
+
// `exprChildren`. That is no evaluation order: C leaves it unspecified, and agbcc loads the callee
|
|
70
|
+
// after the arguments.
|
|
71
|
+
| { k: 'call'; fn: string | Expr; args: Expr[]; wide64?: true; sret?: IrType }
|
|
26
72
|
// The ADDRESS of a named global, `&gSym` (agbcc pool `.word gSym`, frontend `gaddr` op). A
|
|
27
73
|
// DEREF of it collapses to the bare global: memAccess/arrayAccess spell `*(&gSym)` as `gSym`
|
|
28
74
|
// and `(&gSym)[i]` as `gSym[i]` (a global name decays to a pointer). Only a genuinely
|
|
29
75
|
// address-TAKEN global (passed by address, `&gSym` as a call arg) prints the `&` form. The
|
|
30
76
|
// global's type comes from the project headers, so it is never declared as a local.
|
|
77
|
+
//
|
|
78
|
+
// An ARRAY's address is spelled by the bare name, and that is a fact of C's declarator grammar
|
|
79
|
+
// rather than of the address — so the C printer decides it from the name's declared type, and
|
|
80
|
+
// this node carries nothing about it (backend/cfamily.ts, `case 'addr'`).
|
|
31
81
|
| { k: 'addr'; name: string }
|
|
32
82
|
// A memory access `base[idx]` (printed `*base` when idx is the constant 0), CARRYING the
|
|
33
83
|
// access's element width (bytes) and signedness. `idx` counts elements of `width` bytes.
|
|
@@ -184,7 +234,19 @@ export type Expr =
|
|
|
184
234
|
// which side a compare was spelled from genuinely underdetermines — a signed spelling that
|
|
185
235
|
// byte-matched was proved non-negative by the compiler — so it is refereed as a variation, while a
|
|
186
236
|
// division helper is a pure function of the expression's C type with no such proof available.
|
|
237
|
+
//
|
|
238
|
+
// THE FLOAT OPERATORS (`f+` `f-` `f*` `f/`) are the third split, and they are earned by what an
|
|
239
|
+
// integer operator MEANS to the passes that read one rather than by a spelling: C writes both with
|
|
240
|
+
// the same token, but `/` here is the SIGNED divide the C-family backend pins with `(s32)` casts,
|
|
241
|
+
// and `+`/`-` are the pointer walks, reassociations and constant folds of a dozen L3 passes. A float
|
|
242
|
+
// under any of those is a wrong program, so it gets operators none of them match — the same reason
|
|
243
|
+
// the IR has `fadd` rather than a float-typed `add`. The unary minus splits the same way (`f-` on
|
|
244
|
+
// `un`).
|
|
187
245
|
export type BinOp =
|
|
246
|
+
| 'f+'
|
|
247
|
+
| 'f-'
|
|
248
|
+
| 'f*'
|
|
249
|
+
| 'f/'
|
|
188
250
|
| '+'
|
|
189
251
|
| '-'
|
|
190
252
|
| '*'
|
|
@@ -342,6 +404,10 @@ export interface SFn {
|
|
|
342
404
|
* legalization env (exprCType) but NEVER declared by a backend: the project's own headers
|
|
343
405
|
* declare them, exactly like every other global name asmlift emits. */
|
|
344
406
|
globals?: { name: string; type: IrType }[];
|
|
407
|
+
/** The function-scope statics this function DEFINES (structure/local-statics.ts), declared at
|
|
408
|
+
* the top of its body ahead of the locals. Unlike `globals`, the backend declares these: no
|
|
409
|
+
* header does, because the source defined them here. */
|
|
410
|
+
statics?: SStatic[];
|
|
345
411
|
retType: IrType;
|
|
346
412
|
body: Stmt[];
|
|
347
413
|
/** Struct types this function's fields reference, declared above it by the backend. Empty
|
|
@@ -359,6 +425,30 @@ export interface SFn {
|
|
|
359
425
|
* `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
|
|
360
426
|
* and `l3/slotorder.ts` is the identity — never "ascending by default". */
|
|
361
427
|
slotOrder?: 'ascending' | 'descending';
|
|
428
|
+
/** 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++, which
|
|
430
|
+
* refuses three conversions C makes implicitly (backend/cfamily.ts `argConversion`), and by the
|
|
431
|
+
* pointer-cell conversions in every dialect (l3/ptrcell.ts). */
|
|
432
|
+
declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
|
|
433
|
+
/** Each declared callee's return type as the project spells it, never printed: whether a call a
|
|
434
|
+
* pointer cell is compared with returns a pointer (l3/ptrcell.ts). */
|
|
435
|
+
declaredReturns?: Readonly<Record<string, string>>;
|
|
436
|
+
/** The emitted C is compiled AS C++. An emission policy like `slotOrder`. */
|
|
437
|
+
dialect?: 'c++';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** 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
|
+
* `align` is an alignment in bytes wider than the type's own, which the definition must state. */
|
|
443
|
+
export interface SStatic {
|
|
444
|
+
name: string;
|
|
445
|
+
type: IrType;
|
|
446
|
+
const?: true;
|
|
447
|
+
align?: number;
|
|
448
|
+
init?: number[];
|
|
449
|
+
/** the initializer is spelled as a string literal — a byte array the compiler aligns as a
|
|
450
|
+
* string (structure/local-statics.ts) */
|
|
451
|
+
string?: true;
|
|
362
452
|
}
|
|
363
453
|
|
|
364
454
|
/** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
|
|
@@ -427,6 +517,8 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
427
517
|
return a.name === (b as typeof a).name;
|
|
428
518
|
case 'const':
|
|
429
519
|
return a.value === (b as typeof a).value;
|
|
520
|
+
case 'fconst':
|
|
521
|
+
return a.bits === (b as typeof a).bits;
|
|
430
522
|
case 'bin': {
|
|
431
523
|
const bb = b as typeof a;
|
|
432
524
|
return a.op === bb.op && exprEquals(a.l, bb.l) && exprEquals(a.r, bb.r);
|
|
@@ -435,6 +527,10 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
435
527
|
const bb = b as typeof a;
|
|
436
528
|
return a.op === bb.op && exprEquals(a.e, bb.e);
|
|
437
529
|
}
|
|
530
|
+
case 'postincr': {
|
|
531
|
+
const bb = b as typeof a;
|
|
532
|
+
return a.name === bb.name && a.by === bb.by;
|
|
533
|
+
}
|
|
438
534
|
case 'cast': {
|
|
439
535
|
const bb = b as typeof a;
|
|
440
536
|
// `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
|
|
@@ -448,7 +544,25 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
448
544
|
}
|
|
449
545
|
case 'call': {
|
|
450
546
|
const bb = b as typeof a;
|
|
451
|
-
|
|
547
|
+
// `wide64` is part of the node's IDENTITY, compared for the same reason `volatile` is one
|
|
548
|
+
// cast up: it decides the rank every operand pin is taken at, so two calls that differ in it
|
|
549
|
+
// are two different expressions and a dedup that kept one would respell the other's
|
|
550
|
+
// arithmetic.
|
|
551
|
+
//
|
|
552
|
+
// NO PIPELINE INPUT REACHES THIS TERM TODAY, and saying so is cheaper than a witness that
|
|
553
|
+
// cannot exist: the stamp is a pure function of the callee name (`structure.ts` reads the
|
|
554
|
+
// call result's IR width, which the frontend set from the callee's declared return), so two
|
|
555
|
+
// `call` nodes sharing `fn` and `args` always share `wide64` and deleting the term leaves
|
|
556
|
+
// the whole core suite green. What pins it is a direct test of THIS function's contract in
|
|
557
|
+
// test/int64-repr.test.ts, over two nodes built by hand — which is the honest bar, because
|
|
558
|
+
// the claim being made is about `exprEquals` and not about a path through the pipeline.
|
|
559
|
+
return (
|
|
560
|
+
(typeof a.fn === 'string' || typeof bb.fn === 'string' ? a.fn === bb.fn : exprEquals(a.fn, bb.fn)) &&
|
|
561
|
+
(a.wide64 ?? false) === (bb.wide64 ?? false) &&
|
|
562
|
+
(a.sret === undefined ? bb.sret === undefined : bb.sret !== undefined && typeEquals(a.sret, bb.sret)) &&
|
|
563
|
+
a.args.length === bb.args.length &&
|
|
564
|
+
a.args.every((x, i) => exprEquals(x, bb.args[i]))
|
|
565
|
+
);
|
|
452
566
|
}
|
|
453
567
|
case 'index': {
|
|
454
568
|
const bb = b as typeof a;
|
|
@@ -493,12 +607,49 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
493
607
|
}
|
|
494
608
|
}
|
|
495
609
|
|
|
496
|
-
/**
|
|
497
|
-
*
|
|
610
|
+
/** Do two statements print the same C? The leaf statements compare through {@link exprEquals},
|
|
611
|
+
* and an `if` arm by arm; every other kind answers no, which can only keep apart two statements a
|
|
612
|
+
* caller might have merged. */
|
|
613
|
+
export function stmtEquals(a: Stmt, b: Stmt): boolean {
|
|
614
|
+
switch (a.k) {
|
|
615
|
+
case 'assign':
|
|
616
|
+
return b.k === 'assign' && a.name === b.name && exprEquals(a.value, b.value);
|
|
617
|
+
case 'store':
|
|
618
|
+
return b.k === 'store' && exprEquals(a.lval, b.lval) && exprEquals(a.value, b.value);
|
|
619
|
+
case 'exprstmt':
|
|
620
|
+
return b.k === 'exprstmt' && exprEquals(a.value, b.value);
|
|
621
|
+
case 'return':
|
|
622
|
+
return (
|
|
623
|
+
b.k === 'return' &&
|
|
624
|
+
(a.unspelled ?? false) === (b.unspelled ?? false) &&
|
|
625
|
+
(a.value === undefined ? b.value === undefined : b.value !== undefined && exprEquals(a.value, b.value))
|
|
626
|
+
);
|
|
627
|
+
case 'if':
|
|
628
|
+
return b.k === 'if' && exprEquals(a.cond, b.cond) && stmtsEqual(a.then, b.then) && stmtsEqual(a.else, b.else);
|
|
629
|
+
default:
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** {@link stmtEquals} over two lists, in order. */
|
|
635
|
+
export function stmtsEqual(a: readonly Stmt[], b: readonly Stmt[]): boolean {
|
|
636
|
+
return a.length === b.length && a.every((s, i) => stmtEquals(s, b[i]));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** THE spelling of an `opaque`'s gap reason, in one place: `structure.ts` writes it into the
|
|
640
|
+
* marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
|
|
498
641
|
* classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
|
|
499
|
-
* and never firing
|
|
500
|
-
|
|
501
|
-
|
|
642
|
+
* and never firing, since it is an EQUALITY between what the IR carries and what the output shows.
|
|
643
|
+
*
|
|
644
|
+
* TWO PRODUCERS, TWO SENTENCES, and they name different capabilities: an INSTRUCTION no frontend
|
|
645
|
+
* decodes (`adc`), and a call into the compiler's own RUNTIME that no recogniser folded
|
|
646
|
+
* (`__div2i`). The attrs decide which, so a producer chooses by what it stamps rather than by
|
|
647
|
+
* spelling a string. `?` when a frontend stamps neither. */
|
|
648
|
+
export function gapReasonFor(attrs: { mnemonic?: unknown; helper?: unknown }): string {
|
|
649
|
+
if (typeof attrs.helper === 'string') {
|
|
650
|
+
return `no model for the runtime helper '${attrs.helper}'`;
|
|
651
|
+
}
|
|
652
|
+
return `unmodelled instruction '${typeof attrs.mnemonic === 'string' ? attrs.mnemonic : '?'}'`;
|
|
502
653
|
}
|
|
503
654
|
|
|
504
655
|
// ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
|
|
@@ -513,7 +664,9 @@ export function exprChildren(e: Expr): Expr[] {
|
|
|
513
664
|
switch (e.k) {
|
|
514
665
|
case 'var':
|
|
515
666
|
case 'const':
|
|
667
|
+
case 'fconst':
|
|
516
668
|
case 'addr':
|
|
669
|
+
case 'postincr':
|
|
517
670
|
return [];
|
|
518
671
|
case 'bin':
|
|
519
672
|
return [e.l, e.r];
|
|
@@ -521,7 +674,7 @@ export function exprChildren(e: Expr): Expr[] {
|
|
|
521
674
|
case 'cast':
|
|
522
675
|
return [e.e];
|
|
523
676
|
case 'call':
|
|
524
|
-
return e.args;
|
|
677
|
+
return typeof e.fn === 'string' ? e.args : [e.fn, ...e.args];
|
|
525
678
|
case 'index':
|
|
526
679
|
return [e.base, ...(e.lead ?? []), e.idx];
|
|
527
680
|
case 'field':
|
|
@@ -536,7 +689,9 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
|
|
|
536
689
|
switch (e.k) {
|
|
537
690
|
case 'var':
|
|
538
691
|
case 'const':
|
|
692
|
+
case 'fconst':
|
|
539
693
|
case 'addr':
|
|
694
|
+
case 'postincr':
|
|
540
695
|
return e;
|
|
541
696
|
case 'bin':
|
|
542
697
|
return { ...e, l: f(e.l), r: f(e.r) };
|
|
@@ -544,7 +699,7 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
|
|
|
544
699
|
case 'cast':
|
|
545
700
|
return { ...e, e: f(e.e) };
|
|
546
701
|
case 'call':
|
|
547
|
-
return { ...e, args: e.args.map(f) };
|
|
702
|
+
return { ...e, ...(typeof e.fn === 'string' ? {} : { fn: f(e.fn) }), args: e.args.map(f) };
|
|
548
703
|
case 'index':
|
|
549
704
|
return { ...e, base: f(e.base), ...(e.lead ? { lead: e.lead.map(f) } : {}), idx: f(e.idx) };
|
|
550
705
|
case 'field':
|
|
@@ -654,7 +809,7 @@ export function rematerializableAddress(e: Expr): boolean {
|
|
|
654
809
|
case 'un':
|
|
655
810
|
break;
|
|
656
811
|
default:
|
|
657
|
-
ok = false; // var, addr, index, field, call, marker
|
|
812
|
+
ok = false; // var, addr, postincr, index, field, call, marker
|
|
658
813
|
return;
|
|
659
814
|
}
|
|
660
815
|
for (const c of exprChildren(x)) {
|
|
@@ -665,10 +820,27 @@ export function rematerializableAddress(e: Expr): boolean {
|
|
|
665
820
|
return ok && nonZero;
|
|
666
821
|
}
|
|
667
822
|
|
|
668
|
-
/**
|
|
669
|
-
*
|
|
823
|
+
/** The local or global a node NAMES, or undefined for one that names none — the whole Expr
|
|
824
|
+
* vocabulary a "which names does this tree mention" walk has to know about, in one place, because
|
|
825
|
+
* a collector that misses a kind reports a SHORT live range and a short range is a clobber where a
|
|
826
|
+
* long one is only a missed merge.
|
|
827
|
+
*
|
|
828
|
+
* `call`'s `fn` is not one of them: it names a function, and the walks that care about that ask
|
|
829
|
+
* for it beside this (l3/hoist.ts). A walk that must tell the kinds APART — `&v` is a write where
|
|
830
|
+
* a bare `v` is a read — keeps its own arms; this answers the name, not what is done to it. */
|
|
831
|
+
export function mentionedName(e: Expr): string | undefined {
|
|
832
|
+
return e.k === 'var' || e.k === 'addr' || e.k === 'postincr' ? e.name : undefined;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** The callee a call names, or undefined for an indirect one. */
|
|
836
|
+
export function calleeOf(e: Extract<Expr, { k: 'call' }>): string | undefined {
|
|
837
|
+
return typeof e.fn === 'string' ? e.fn : undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, a marker
|
|
841
|
+
* standing in for an unmodelled instruction (annotate mode), or a post-increment's write. */
|
|
670
842
|
export function exprHasEffect(e: Expr): boolean {
|
|
671
|
-
return e.k === 'call' || e.k === 'marker' || exprChildren(e).some(exprHasEffect);
|
|
843
|
+
return e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || exprChildren(e).some(exprHasEffect);
|
|
672
844
|
}
|
|
673
845
|
|
|
674
846
|
/** Whether the tree performs an access to a `volatile` object — the OTHER thing no re-ordering may
|
package/src/l3/coalesce.ts
CHANGED
|
@@ -12,23 +12,24 @@
|
|
|
12
12
|
// and whether fall-through joins the two arms onto one path. `coalesceCandidates` offers both.
|
|
13
13
|
import { typeToString } from '../ir/types';
|
|
14
14
|
import type { Expr, SFn, Stmt } from './ast';
|
|
15
|
-
import { exprChildren, isLoop, mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
15
|
+
import { exprChildren, isLoop, mapExprChildren, mentionedName, stmtChildren, stmtExprs } from './ast';
|
|
16
16
|
import { type Gate, firstRejection } from './gates';
|
|
17
17
|
|
|
18
18
|
function namesIn(e: Expr, out: Set<string>): void {
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// ever built for it.
|
|
19
|
+
// EVERY mention, through `mentionedName`'s vocabulary rather than this file's own: a span that
|
|
20
|
+
// misses one is SHORT, and a short span is a clobber where a long one is only a missed merge.
|
|
21
|
+
// `&sp0` is a LOCAL's mention — the structurer renders an `laddr` frame object as an addr node
|
|
22
|
+
// over a name that IS in `sfn.locals` — and `n++` in a bottom test is a counter's only mention
|
|
23
|
+
// inside its own loop. A global name comes along for the ride and costs nothing — it is not in
|
|
24
|
+
// `sfn.locals`, so no pair is ever built for it.
|
|
25
25
|
//
|
|
26
|
-
// `rename` DISAGREES WITH THIS, and knowingly: it rewrites
|
|
27
|
-
// absorbed while mentioned through `&` leaves that mention standing against a declaration
|
|
28
|
-
// merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
|
|
26
|
+
// `rename` DISAGREES WITH THIS, and knowingly: it rewrites `var` and `postincr` leaves, so a
|
|
27
|
+
// local absorbed while mentioned through `&` leaves that mention standing against a declaration
|
|
28
|
+
// the merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
|
|
29
29
|
// cleanup — so today's behaviour is pinned exactly in coalesce.test.ts ('rename') rather than
|
|
30
30
|
// repaired here.
|
|
31
|
-
|
|
31
|
+
const n = mentionedName(e);
|
|
32
|
+
if (n !== undefined) out.add(n);
|
|
32
33
|
for (const c of exprChildren(e)) namesIn(c, out);
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -62,7 +63,14 @@ export interface Span {
|
|
|
62
63
|
* arithmetic over the variable is its own history. The read must be a `var`: `mentions` counts an
|
|
63
64
|
* `addr` too, and `&a` is not a read of `a`. */
|
|
64
65
|
const readsVarArithmetically = (e: Expr, n: string): boolean => {
|
|
65
|
-
if (
|
|
66
|
+
if (
|
|
67
|
+
e.k === 'index' ||
|
|
68
|
+
e.k === 'field' ||
|
|
69
|
+
e.k === 'call' ||
|
|
70
|
+
e.k === 'marker' ||
|
|
71
|
+
e.k === 'postincr' ||
|
|
72
|
+
e.k === 'addr'
|
|
73
|
+
) {
|
|
66
74
|
return false;
|
|
67
75
|
}
|
|
68
76
|
return (e.k === 'var' && e.name === n) || exprChildren(e).some((c) => readsVarArithmetically(c, n));
|
|
@@ -155,7 +163,7 @@ function spans(body: Stmt[]): Map<string, Span> {
|
|
|
155
163
|
}
|
|
156
164
|
function rename(body: Stmt[], from: string, to: string): Stmt[] {
|
|
157
165
|
const inExpr = (e: Expr): Expr =>
|
|
158
|
-
e.k === 'var' && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
|
|
166
|
+
(e.k === 'var' || e.k === 'postincr') && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
|
|
159
167
|
const inStmt = (s: Stmt): Stmt => {
|
|
160
168
|
const r = { ...s } as Record<string, unknown>;
|
|
161
169
|
if (s.k === 'assign' && s.name === from) r.name = to;
|
package/src/l3/dce.ts
CHANGED
|
@@ -29,9 +29,11 @@ import { exprChildren, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
|
29
29
|
|
|
30
30
|
/** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
|
|
31
31
|
* `addr` name counts too: taking a local's address makes every store to it observable through the
|
|
32
|
-
* escaped pointer, so an address-taken local is never dead here.
|
|
32
|
+
* escaped pointer, so an address-taken local is never dead here. A `postincr` reads its name
|
|
33
|
+
* before it writes it, so it pins the store that reaches it — `n++` in a loop test is the only
|
|
34
|
+
* reader of the `n = 0` above the loop. */
|
|
33
35
|
function readsInto(e: Expr, out: Set<string>): void {
|
|
34
|
-
if (e.k === 'var' || e.k === 'addr') {
|
|
36
|
+
if (e.k === 'var' || e.k === 'addr' || e.k === 'postincr') {
|
|
35
37
|
out.add(e.name);
|
|
36
38
|
}
|
|
37
39
|
for (const c of exprChildren(e)) {
|
|
@@ -48,6 +50,7 @@ function reads(e: Expr): Set<string> {
|
|
|
48
50
|
/** True if `e` carries any reason NOT to speculatively delete its assignment/condition:
|
|
49
51
|
* - `call` — may write memory/globals (a side effect);
|
|
50
52
|
* - `marker` — the annotate-mode ASMLIFT_ERROR gap signal, which must survive so the gap stays loud;
|
|
53
|
+
* - `postincr` — its write is the loop update; deleting the assignment would delete the update;
|
|
51
54
|
* - the strict-mode `?` unresolved sentinel (`{k:'var', name:'?'}`) — dropping it would let a
|
|
52
55
|
* value asmlift could NOT lift slip past `assertResolved`, silently downgrading a loud gap;
|
|
53
56
|
* - a memory load (`index`/`field`) — a deref's volatility is unknowable here, so a possibly-effectful read
|
|
@@ -56,7 +59,7 @@ function reads(e: Expr): Set<string> {
|
|
|
56
59
|
* observable access the machine performed; deleting the dead assignment would delete the read.
|
|
57
60
|
* A dead assignment whose value contains any of these is kept. */
|
|
58
61
|
function mustKeep(e: Expr, volatiles: ReadonlySet<string>): boolean {
|
|
59
|
-
if (e.k === 'call' || e.k === 'marker' || e.k === 'index' || e.k === 'field') {
|
|
62
|
+
if (e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || e.k === 'index' || e.k === 'field') {
|
|
60
63
|
return true;
|
|
61
64
|
}
|
|
62
65
|
if (e.k === 'var' && (e.name === '?' || volatiles.has(e.name))) {
|
|
@@ -95,6 +98,16 @@ function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
|
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
/** Where a `break` and a `continue` at this depth land, as the set live there. A jump does not fall
|
|
102
|
+
* through to the statement after it: the live set at a jump is its TARGET's, not what the rest of
|
|
103
|
+
* the enclosing block reads. `break` targets the innermost loop or switch, `continue` the innermost
|
|
104
|
+
* loop; each is that statement's conservative in-scope set (everything read anywhere in it plus
|
|
105
|
+
* what is live after it), which also covers the loop's own next test and update. */
|
|
106
|
+
interface JumpLive {
|
|
107
|
+
brk: ReadonlySet<string>;
|
|
108
|
+
cont?: ReadonlySet<string>; // absent in a switch no loop encloses
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
/** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
|
|
99
112
|
* returns the rewritten block and the set live on entry. */
|
|
100
113
|
function dceBlock(
|
|
@@ -102,6 +115,7 @@ function dceBlock(
|
|
|
102
115
|
liveOut: ReadonlySet<string>,
|
|
103
116
|
locals: ReadonlySet<string>,
|
|
104
117
|
volatiles: ReadonlySet<string>,
|
|
118
|
+
jumps: JumpLive | null,
|
|
105
119
|
): { out: Stmt[]; liveIn: Set<string> } {
|
|
106
120
|
const live = new Set(liveOut);
|
|
107
121
|
const rev: Stmt[] = [];
|
|
@@ -153,12 +167,19 @@ function dceBlock(
|
|
|
153
167
|
}
|
|
154
168
|
case 'break':
|
|
155
169
|
case 'continue': {
|
|
170
|
+
// A mid-body `if (c) { v = x; break; }` carries the loop's exit value in `v`, while the
|
|
171
|
+
// statements after the `if` overwrite `v` for the next iteration. Leaving `live` as the
|
|
172
|
+
// fall-through set would judge the carried copy dead and drop it.
|
|
173
|
+
const target = s.k === 'break' ? jumps?.brk : jumps?.cont;
|
|
174
|
+
if (target) {
|
|
175
|
+
setLive(new Set(target));
|
|
176
|
+
}
|
|
156
177
|
rev.push(s);
|
|
157
178
|
break;
|
|
158
179
|
}
|
|
159
180
|
case 'if': {
|
|
160
|
-
const t = dceBlock(s.then, live, locals, volatiles);
|
|
161
|
-
const e = dceBlock(s.else, live, locals, volatiles);
|
|
181
|
+
const t = dceBlock(s.then, live, locals, volatiles, jumps);
|
|
182
|
+
const e = dceBlock(s.else, live, locals, volatiles, jumps);
|
|
162
183
|
const nlive = new Set<string>();
|
|
163
184
|
for (const r of reads(s.cond)) {
|
|
164
185
|
nlive.add(r);
|
|
@@ -188,7 +209,7 @@ function dceBlock(
|
|
|
188
209
|
// loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
|
|
189
210
|
const loopLive = new Set(live);
|
|
190
211
|
allReadsInto([s], loopLive);
|
|
191
|
-
const b = dceBlock(s.body, loopLive, locals, volatiles);
|
|
212
|
+
const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
|
|
192
213
|
const nlive = new Set(loopLive);
|
|
193
214
|
for (const r of b.liveIn) {
|
|
194
215
|
nlive.add(r);
|
|
@@ -200,7 +221,7 @@ function dceBlock(
|
|
|
200
221
|
case 'for': {
|
|
201
222
|
const loopLive = new Set(live);
|
|
202
223
|
allReadsInto([s], loopLive);
|
|
203
|
-
const b = dceBlock(s.body, loopLive, locals, volatiles);
|
|
224
|
+
const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
|
|
204
225
|
const nlive = new Set(loopLive);
|
|
205
226
|
for (const r of b.liveIn) {
|
|
206
227
|
nlive.add(r);
|
|
@@ -214,8 +235,9 @@ function dceBlock(
|
|
|
214
235
|
// read anywhere in the switch as live throughout — no case-body store is ever cut.
|
|
215
236
|
const swLive = new Set(live);
|
|
216
237
|
allReadsInto([s], swLive);
|
|
217
|
-
const
|
|
218
|
-
const
|
|
238
|
+
const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont };
|
|
239
|
+
const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles, inSwitch).out }));
|
|
240
|
+
const def = s.default ? dceBlock(s.default, swLive, locals, volatiles, inSwitch).out : s.default;
|
|
219
241
|
const nlive = new Set(swLive);
|
|
220
242
|
for (const r of reads(s.scrutinee)) {
|
|
221
243
|
nlive.add(r);
|
|
@@ -263,7 +285,7 @@ export function eliminateDeadStores(sfn: SFn): SFn {
|
|
|
263
285
|
allAddrNamesInto(sfn.body, addressTaken);
|
|
264
286
|
const volatiles = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
|
|
265
287
|
const locals = new Set(sfn.locals.filter((l) => !l.volatile && !addressTaken.has(l.name)).map((l) => l.name));
|
|
266
|
-
const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles).out;
|
|
288
|
+
const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles, null).out;
|
|
267
289
|
const used = new Set<string>();
|
|
268
290
|
referencedNames(body, used);
|
|
269
291
|
return { ...sfn, body, locals: sfn.locals.filter((l) => used.has(l.name) || l.volatile) };
|
package/src/l3/gates.ts
CHANGED
|
@@ -19,8 +19,9 @@ export interface Gate<Ctx> {
|
|
|
19
19
|
/** Remove it and some candidate is WRONG, not merely worse. Everything else is a codegen
|
|
20
20
|
* heuristic the differ still referees. This flag is what makes `guardedBy` mandatory. */
|
|
21
21
|
readonly sound: boolean;
|
|
22
|
-
/** the test that fails when this gate is removed — required for a sound gate
|
|
23
|
-
|
|
22
|
+
/** the test that fails when this gate is removed — required for a sound gate; a rule with two
|
|
23
|
+
* halves names one test per half */
|
|
24
|
+
readonly guardedBy?: string | readonly string[];
|
|
24
25
|
/** true ⇒ REJECT this candidate */
|
|
25
26
|
readonly rejects: (c: Ctx) => boolean;
|
|
26
27
|
}
|
|
@@ -67,6 +68,11 @@ export function ablateHeuristic<Ctx>(gates: readonly Gate<Ctx>[], id: string): r
|
|
|
67
68
|
return without(gates, id);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Every test a gate names as its guard, one per half of the rule. */
|
|
72
|
+
export function guardsOf<Ctx>(g: Gate<Ctx>): readonly string[] {
|
|
73
|
+
return typeof g.guardedBy === 'string' ? [g.guardedBy] : (g.guardedBy ?? []);
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
/** Structural defects in a gate table — the part checkable without running the pass. Returns
|
|
71
77
|
* findings rather than throwing, so core stays free of a test-framework import. */
|
|
72
78
|
export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
|
|
@@ -84,7 +90,7 @@ export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
|
|
|
84
90
|
out.push(`gate '${g.id}' has no usable \`why\``);
|
|
85
91
|
}
|
|
86
92
|
// the one rule that costs something to declare
|
|
87
|
-
if (g.sound && !g.
|
|
93
|
+
if (g.sound && !guardsOf(g).some((t) => t.trim())) {
|
|
88
94
|
out.push(`gate '${g.id}' is marked sound but names no guard`);
|
|
89
95
|
}
|
|
90
96
|
}
|