@asmlift/core 0.4.0 → 0.6.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/ir/opcodes.ts
CHANGED
|
@@ -16,6 +16,13 @@ export interface OpSig {
|
|
|
16
16
|
* an unconditional position. THE one effect vocabulary — DCE (pattern/engine.ts) and the
|
|
17
17
|
* short-circuit hoist guard (raise/shortcircuit.ts) both derive from this flag. */
|
|
18
18
|
effects?: boolean;
|
|
19
|
+
/** reads memory. Deletable when dead — nothing observes a load nobody reads — but NOT movable:
|
|
20
|
+
* a load answers whichever stores ran before it, so crossing one changes the value it yields.
|
|
21
|
+
* The two questions are separate flags because a load answers them differently. */
|
|
22
|
+
reads?: boolean;
|
|
23
|
+
/** may fault on operands the program never actually gave it — the integer divides, on a zero
|
|
24
|
+
* divisor. Deletable and movable, but not safe to run on a path that did not run it. */
|
|
25
|
+
traps?: boolean;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export const OPCODES = {
|
|
@@ -60,16 +67,29 @@ export const OPCODES = {
|
|
|
60
67
|
zext: { operands: 1, results: 1, requiredAttrs: ['width'] },
|
|
61
68
|
sext: { operands: 1, results: 1, requiredAttrs: ['width'] },
|
|
62
69
|
// Division/remainder. `sdiv` is variadic like the shifts: the immediate form (1 operand +
|
|
63
|
-
// `imm` attr) is the strength-reduced constant divisor an idiom folds to (`sdiv X {imm=2}`)
|
|
64
|
-
// the register form
|
|
65
|
-
// ISA with `capabilities.hwDivide`); the structurer branches on count. `udiv`/`smod`/`umod`
|
|
70
|
+
// `imm` attr) is the strength-reduced constant divisor an idiom folds to (`sdiv X {imm=2}`),
|
|
71
|
+
// the register form is 2 operands, and the structurer branches on count. `udiv`/`smod`/`umod`
|
|
66
72
|
// are 2-operand only. `sdiv`/`udiv` = quotient, `smod`/`umod` = remainder; signedness lives in
|
|
67
|
-
// the op
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
// the op, and L3 keeps the pair apart (`/`/`%` against `/u`/`%u`) so the C backend can spell
|
|
74
|
+
// both with C's one token over an operand cast that says which.
|
|
75
|
+
//
|
|
76
|
+
// A register form does NOT mean the machine had a divide instruction — never read the ISA off it.
|
|
77
|
+
// Six recognizers at three layers build these ops, and which fires is a fact about the COMPILER's
|
|
78
|
+
// lowering, not the hardware. Quotient AND remainder: frontend/mips.ts (`mfhi` off a real `div`),
|
|
79
|
+
// raise/softdiv.ts (a `bl __modsi3` on an ISA with NO divide, gated `!hwDivide`), and
|
|
80
|
+
// pattern/engine.ts HWMOD_PATTERNS (`divw`+`mullw`+`subf`, where the hardware divides but has no
|
|
81
|
+
// remainder instruction). Quotient ONLY: raise/magicdiv.ts, raise/divpow2.ts, SDIV_POW2_2.
|
|
82
|
+
// KNOWN GAP, by pass ORDER rather than by decision: the quotient-only three run at stage 2.35,
|
|
83
|
+
// AFTER the stage-2 idiom fold, so no remainder fold can ever see the `sdiv` they build, and a
|
|
84
|
+
// remainder over a CONSTANT divisor stays written out on every target — visible in the artifact
|
|
85
|
+
// as `modc`/`umod10`, which recover `%` on agbcc+ido and the decomposition on kmc+mwcc. All four
|
|
86
|
+
// are byte-exact, so it costs 0 points; closing it re-prices those rows and re-derives both of
|
|
87
|
+
// HWMOD_PATTERNS' gates (a constant multiply carries no operand order to read), so it is its own
|
|
88
|
+
// round.
|
|
89
|
+
sdiv: { operands: 'variadic', results: 1, traps: true },
|
|
90
|
+
udiv: { operands: 2, results: 1, traps: true },
|
|
91
|
+
smod: { operands: 2, results: 1, traps: true },
|
|
92
|
+
umod: { operands: 2, results: 1, traps: true },
|
|
73
93
|
// signed/equality comparisons (result is a boolean-valued u32)
|
|
74
94
|
icmp_slt: { operands: 2, results: 1 },
|
|
75
95
|
icmp_sle: { operands: 2, results: 1 },
|
|
@@ -90,13 +110,13 @@ export const OPCODES = {
|
|
|
90
110
|
logic_and: { operands: 2, results: 1 },
|
|
91
111
|
logic_or: { operands: 2, results: 1 },
|
|
92
112
|
// --- memory ---
|
|
93
|
-
load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'] },
|
|
113
|
+
load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'], reads: true },
|
|
94
114
|
store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
|
|
95
115
|
// Typed element-scaled array access. Unlike load/store's constant `off`, these carry an
|
|
96
116
|
// explicit runtime `index` operand plus the `elemSize` the index scales by, so the base is a
|
|
97
117
|
// genuine `elem *` and no byte-offset arithmetic leaks into the emitted source. Produced by
|
|
98
118
|
// the array-recognition legalization pass (raise/arrays.ts).
|
|
99
|
-
aload: { operands: 2, results: 1, requiredAttrs: ['elemSize', 'signed'] }, // aload base, index
|
|
119
|
+
aload: { operands: 2, results: 1, requiredAttrs: ['elemSize', 'signed'], reads: true }, // aload base, index
|
|
100
120
|
astore: { operands: 3, results: 0, requiredAttrs: ['elemSize'], effects: true }, // astore base, index, value
|
|
101
121
|
// --- call: operands are the argument values (r0..), result is the return value (r0),
|
|
102
122
|
// `target` attr is the callee symbol. Caller-saved clobbering is implicit. ---
|
|
@@ -108,8 +128,54 @@ export const OPCODES = {
|
|
|
108
128
|
// - an indexed or non-zero-offset AGGREGATE access → the address-cast `((T *)&gSym)[i]`;
|
|
109
129
|
// - any other use (e.g. `&gSym` passed to a call) → the `{k:'addr'}` L3 node, printed `&gSym`.
|
|
110
130
|
gaddr: { operands: 0, results: 1, requiredAttrs: ['sym'] },
|
|
131
|
+
// The address of a FRAME-LOCAL object — gaddr's local twin, for the address-taken stack local
|
|
132
|
+
// (`mov rD, sp` feeding a DMA register or a callee). `off` is the byte offset inside the frame's
|
|
133
|
+
// reserved local area; the Thumb frontend's post-lift audit stamps `name`/`width`/`signed` after
|
|
134
|
+
// proving every access agrees, and the structurer declares the local and renders `&name` exactly
|
|
135
|
+
// as it renders a gaddr's `&sym`. Operand-free and pure, so GVN numbers it like gaddr and a dead
|
|
136
|
+
// one is reaped.
|
|
137
|
+
// `width`/`signed` are stamped by the frontend's frame-object AUDIT — requiring them makes
|
|
138
|
+
// "the audit ran" a verifier-checkable fact instead of a convention: a frontend that emits a
|
|
139
|
+
// laddr and skips the audit fails verify loudly instead of rendering `&undefined`.
|
|
140
|
+
laddr: { operands: 0, results: 1, requiredAttrs: ['off', 'width', 'signed'] },
|
|
141
|
+
// An UNDEFINED value: a read of storage that carries no INPUT — nothing was entitled to hand this
|
|
142
|
+
// function a value there, and none of its own stores reached it on this path. Deliberately NOT
|
|
143
|
+
// "storage nobody could have written": a callee-saved register holds the CALLER's value at entry,
|
|
144
|
+
// which is exactly what the prologue pushes it for, and the read is undefined all the same
|
|
145
|
+
// because the ABI gives no caller a way to pass an argument in one. The C declared a local with
|
|
146
|
+
// no initialiser and assigned it only inside some arms of a conditional or `switch` (a `switch`
|
|
147
|
+
// with no `default` being the commonest source), so the read is legal to compile and the compiler
|
|
148
|
+
// emitted the unassigned path faithfully.
|
|
149
|
+
//
|
|
150
|
+
// "No input" is established differently in the two places a local lives, and `frontend/ssa.ts`
|
|
151
|
+
// (LiveInModel) is where each is declared:
|
|
152
|
+
// • a FRAME SLOT is storage whose only writer is this function's own stores — SOLE WRITER, not
|
|
153
|
+
// merely "owns the storage", because a frame the function owns can still be written by
|
|
154
|
+
// someone else once an address into it escapes to a callee, which fills a wider object than
|
|
155
|
+
// any in-function access reveals. Whoever mints one owes the retraction on escape
|
|
156
|
+
// (frontend/thumb.ts, after the frame-object audit).
|
|
157
|
+
// • a REGISTER the ABI does not pass arguments in AND this function's prologue SAVED cannot
|
|
158
|
+
// carry a value a caller handed over, and has no address for anything else to reach it by, so
|
|
159
|
+
// there is nothing to retract. The save is half of the premise, not a corroboration of it:
|
|
160
|
+
// asm that follows no ABI is handed live values in registers it never saved.
|
|
161
|
+
//
|
|
162
|
+
// An opcode rather than a live-in because Braun's construction resolves a def-less read to a
|
|
163
|
+
// live-in, and a live-in of the entry block is a PARAMETER — right for an argument register, a
|
|
164
|
+
// fabricated argument for anything else.
|
|
165
|
+
//
|
|
166
|
+
// Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set — where numbering
|
|
167
|
+
// it would be VACUOUS rather than harmful, since two undefs in one function always carry
|
|
168
|
+
// different keys. "Same key, therefore same value" is empty for a value that has none.
|
|
169
|
+
//
|
|
170
|
+
// `key` names the storage (`sp@0`, `r4`); the structurer reads it to name the local
|
|
171
|
+
// (`uninit_sp0`) and emits NO assignment — that absence is the recovery, and an edge argument
|
|
172
|
+
// that is one emits no copy either (structure.ts undefCarriesNothing).
|
|
173
|
+
undef: { operands: 0, results: 1, requiredAttrs: ['key'] },
|
|
111
174
|
// --- black-box escape hatch (keeps lifting total) ---
|
|
112
|
-
|
|
175
|
+
// `effects: true`: an instruction asmlift could not model may do anything — write memory, trap,
|
|
176
|
+
// touch a system register — and `results[0]` is only the part we can name. So a dead `opaque` is
|
|
177
|
+
// no more reapable than a dead `call`.
|
|
178
|
+
opaque: { operands: 'variadic', results: 1, effects: true },
|
|
113
179
|
// --- terminators ---
|
|
114
180
|
ret: { operands: 'variadic', results: 0, terminator: true, successors: 0 },
|
|
115
181
|
br: { operands: 0, results: 0, terminator: true, successors: 1 },
|
|
@@ -124,6 +190,13 @@ export const OPCODES = {
|
|
|
124
190
|
* does not. */
|
|
125
191
|
export type Opcode = keyof typeof OPCODES;
|
|
126
192
|
|
|
193
|
+
/** The widths a `zext`/`sext` carries — a fact about those two opcodes, so it lives with them
|
|
194
|
+
* rather than re-declared per consumer (raise/narrow.ts pairs a narrowing op with its re-widening,
|
|
195
|
+
* raise/paramwidth.ts declares a parameter at one). Every frontend that produces the pair produces
|
|
196
|
+
* one of these: agbcc's gated shift-pair fold (pattern/engine.ts CAST_PATTERNS) and PPC's
|
|
197
|
+
* `extsb`/`extsh` (frontend/ppc.ts). A third width would be a C type the backend cannot spell. */
|
|
198
|
+
export const CAST_WIDTHS: ReadonlySet<number> = new Set([8, 16]);
|
|
199
|
+
|
|
127
200
|
/** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
|
|
128
201
|
* on it); undefined for an unregistered opcode. */
|
|
129
202
|
export function opSig(opcode: string): OpSig | undefined {
|
|
@@ -138,11 +211,10 @@ export function opSig(opcode: string): OpSig | undefined {
|
|
|
138
211
|
* holds by construction (a hand-written map is one typo away from breaking it, and the symptom is a
|
|
139
212
|
* plainly inverted condition in the emitted C). Completeness against the icmp family is the part
|
|
140
213
|
* construction cannot give, so a test asserts it (test/pattern.test.ts) — an eleventh comparison
|
|
141
|
-
* added to `OPCODES` would otherwise degrade
|
|
214
|
+
* added to `OPCODES` would otherwise degrade its consumers several different ways.
|
|
142
215
|
*
|
|
143
216
|
* It lives here for the reason HOIST_UNSAFE_OPS does: every consumer that has to say "the opposite
|
|
144
|
-
* of this compare" reads THIS one
|
|
145
|
-
* short-circuit recognizer's diamond negation, and the idiom layer's `cmp ^ 1` fold — so they
|
|
217
|
+
* of this compare" reads THIS one, so they
|
|
146
218
|
* cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
|
|
147
219
|
* shortcircuit.ts derives its `BOOL_OPS` from these keys (asserting negatable-icmp == boolean-op,
|
|
148
220
|
* true today), and l3/ast.ts `NEGATE_REL` is the SAME relation over the neutral L3 operator
|
|
@@ -162,25 +234,82 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
|
|
|
162
234
|
]),
|
|
163
235
|
);
|
|
164
236
|
|
|
165
|
-
/** Ops with an observable side effect
|
|
237
|
+
/** Ops with an observable side effect: the flag on the signature, derived rather than re-listed.
|
|
238
|
+
* `isDceSafe` asks the same question of the FLAG through `opSig` rather than of this set, so the
|
|
239
|
+
* two cannot disagree. The SET's own consumers are `HOIST_UNSAFE_OPS` below, structure.ts's
|
|
240
|
+
* `sideEffects` walk (an effectful op whose result nobody reads is still an execution),
|
|
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). Three of them — the
|
|
243
|
+
* `sideEffects` walk, the barrier and the bias block — each carried a hand-written copy of this
|
|
244
|
+
* membership, which is how the models drifted apart before. */
|
|
166
245
|
export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
167
246
|
(Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
|
|
168
247
|
);
|
|
169
248
|
|
|
170
|
-
/** Ops that may not be
|
|
249
|
+
/** Ops that may not be SPECULATED — run on a path that did not run them before. Identical to
|
|
250
|
+
* `EFFECTFUL_OPS`, and kept as its own name because its call sites ask the speculation question
|
|
251
|
+
* rather than the deletion one.
|
|
252
|
+
*
|
|
253
|
+
* A memory read is deliberately absent, and that is the one entry worth arguing: its only consumer
|
|
254
|
+
* is raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
|
|
255
|
+
* inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short-circuit
|
|
256
|
+
* re-guards it. Adding the two reads
|
|
257
|
+
* here costs three byte-matches (kleod:UpdateHUDCounterDisplay, synthetic:breakloop,
|
|
258
|
+
* synthetic:strcmp1), so the argument is load-bearing rather than merely plausible.
|
|
259
|
+
*
|
|
260
|
+
* KNOWN GAP: the trapping divides are absent too, and there the re-guard argument does NOT carry
|
|
261
|
+
* — a hoisted `sdiv` that the structurer NAMES becomes an unconditional statement. Left as it is
|
|
262
|
+
* because closing it is a separate change with its own measurement; `REEVAL_UNSAFE_OPS` does
|
|
263
|
+
* refuse them, so the pre-update sink is not exposed to it.
|
|
171
264
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
|
|
180
|
-
|
|
265
|
+
* AND THE EXEMPTION IS NOT TRANSFERABLE, which is worth saying beside it: a second consumer once
|
|
266
|
+
* read this set to answer "would gcc have SPECULATED this arm above a compare", where nothing
|
|
267
|
+
* re-guards anything, and admitted single-load arms `gcc/jump.c:483`'s `! may_trap_p` refuses.
|
|
268
|
+
* Pointing that consumer back at this set costs `synthetic:mergeldcast:agbcc` its byte-match
|
|
269
|
+
* (MATCH -> diff:6), measured. That consumer is `raise/narrowlocal.ts` and it reads
|
|
270
|
+
* `REEVAL_UNSAFE_OPS` instead. Reach for this set only when the argument above — a C-level
|
|
271
|
+
* re-guard at the new point — actually holds at your call site. */
|
|
272
|
+
export const HOIST_UNSAFE_OPS: ReadonlySet<string> = EFFECTFUL_OPS;
|
|
273
|
+
|
|
274
|
+
/** Ops whose answer depends on WHERE they run: an effect (its order against other effects is
|
|
275
|
+
* 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. */
|
|
277
|
+
export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
|
|
278
|
+
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
279
|
+
const sig = OPCODES[k] as OpSig;
|
|
280
|
+
return sig.effects || sig.reads;
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
/** Ops that may not be RE-EVALUATED at another program point — order-sensitive, or trapping. The
|
|
285
|
+
* trap half is what separates this from `ORDER_SENSITIVE_OPS`: it only matters when the new point
|
|
286
|
+
* can be reached on a path the old one was not, so a consumer that merely re-orders on one path
|
|
287
|
+
* wants the smaller set. Both are needed because the two consumers differ on exactly the divides:
|
|
288
|
+
* a collapsed switch re-renders a test block's ops AT THEIR USES, and every use is dominated by
|
|
289
|
+
* the def, so it evaluates them on a SUBSET of the original paths — a narrowing, never a
|
|
290
|
+
* speculation. */
|
|
291
|
+
export const REEVAL_UNSAFE_OPS: ReadonlySet<string> = new Set(
|
|
292
|
+
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
293
|
+
const sig = OPCODES[k] as OpSig;
|
|
294
|
+
return sig.effects || sig.reads || sig.traps;
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
/** Ops that MATERIALIZE a value out of nothing: no operands, no state read, no effect, no trap, no
|
|
299
|
+
* control flow — so where one sits in a block says nothing about what ran before it. Derived, so a
|
|
300
|
+
* future pure nullary opcode joins without a second edit. Consumed by raise/paramwidth.ts, whose
|
|
301
|
+
* prologue scan steps over them; the effect flags are what keep the EFFECTFUL nullary ops (a
|
|
302
|
+
* zero-argument `call`, an `opaque` with no sources) out, and an extension behind a call is body
|
|
303
|
+
* code rather than a prologue. */
|
|
304
|
+
export const MATERIALIZING_OPS: ReadonlySet<string> = new Set(
|
|
305
|
+
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
306
|
+
const sig = OPCODES[k] as OpSig;
|
|
307
|
+
return sig.operands === 0 && !sig.terminator && !sig.effects && !sig.reads && !sig.traps;
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
181
310
|
|
|
182
311
|
/** May a dead result of this opcode be deleted? Registered, no observable effects, not control
|
|
183
|
-
* flow.
|
|
312
|
+
* flow. `opaque` is excluded via its `effects` flag — see the note on its signature. */
|
|
184
313
|
export function isDceSafe(opcode: string): boolean {
|
|
185
314
|
const sig = opSig(opcode);
|
|
186
315
|
return !!sig && !sig.effects && !sig.terminator;
|
package/src/ir/parse.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
// both the debug dump and the test oracle. The parser builds the graph but enforces NO
|
|
5
5
|
// semantics — that is the verifier's job — so malformed-but-well-formed-syntax IR can be
|
|
6
6
|
// constructed and then rejected by verify().
|
|
7
|
+
// The write-order annotation `print(fn, { writeOrder: true })` adds is DISCARDED, not rejected: the
|
|
8
|
+
// text parses (`stripWriteOrderAnnotation`) and the fn comes back with `writeOrder: undefined`.
|
|
9
|
+
// Both halves are deliberate — only a frontend can make that measurement, and a parsed fn that came
|
|
10
|
+
// back measured would structure differently from every other parsed fn, while every stage dump
|
|
11
|
+
// carries the annotation (pipeline.ts, trace.ts) and a dump nobody can paste back in is not an
|
|
12
|
+
// oracle.
|
|
7
13
|
// ROUND-TRIP DOMAIN: parse(print(fn)) holds for L1/scalar types only — `unkN`/`sN`/`uN` and
|
|
8
14
|
// `*`-pointers to them. STRUCT/ARRAY/VOID types print (typeToString) but do NOT parse back; a
|
|
9
15
|
// post-type-recovery dump is a one-way debugging artifact, not a test oracle.
|
|
@@ -32,7 +38,7 @@ export function parse(text: string): Fn {
|
|
|
32
38
|
if (raw[i].trim() === '') {
|
|
33
39
|
continue;
|
|
34
40
|
}
|
|
35
|
-
body.push(raw[i].trim());
|
|
41
|
+
body.push(stripWriteOrderAnnotation(raw[i].trim()));
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
// PASS A — create all blocks and pre-declare every value by its textual name, so
|
|
@@ -113,7 +119,18 @@ export function parse(text: string): Fn {
|
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
// No write order: the text form is the value graph, and the record is a measurement of the
|
|
123
|
+
// MACHINE. A parsed fn's edges are UNMEASURED, never written-nowhere (ir/core.ts `WriteOrder`).
|
|
124
|
+
return { name, blocks: rawBlocks.map((r) => r.block), writeOrder: undefined, slotHomes: undefined };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Drop the two annotations `print(fn, { writeOrder: true })` appends, and NOTHING else.
|
|
128
|
+
*
|
|
129
|
+
* Anchored on their exact shapes rather than on "text after a `;`", because a `;` is not a comment
|
|
130
|
+
* marker in this format: a string attr prints JSON-quoted (`opaque {text="mrs r0, cpsr; …"}`) and
|
|
131
|
+
* a list attr prints `[1;2;3]`, so a general trailing-comment strip would eat an operand. */
|
|
132
|
+
function stripWriteOrderAnnotation(line: string): string {
|
|
133
|
+
return line.replace(/^(\^\w+\(.*\):)\s+;\s+writes=\d+$/, '$1').replace(/\s+;\s+order(?:\s+\^\w+\([^()]*\))+$/, '');
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
function parseOp(line: string, refValue: (nm: string) => Value, refBlock: (label: string) => Block): Op {
|
package/src/ir/print.ts
CHANGED
|
@@ -7,7 +7,17 @@
|
|
|
7
7
|
import type { AttrVal, Block, Fn, Value } from './core';
|
|
8
8
|
import { typeToString } from './types';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** WRITE-ORDER ANNOTATION (`ir/core.ts` WriteOrder) — OFF by default and deliberately not part of
|
|
11
|
+
* the round-trip artifact. The record is a measurement `parse` cannot reconstruct, and a parsed fn
|
|
12
|
+
* that came back MEASURED would structure differently from every other parsed fn, so the text form
|
|
13
|
+
* never carries it back in. It is printed for the per-stage DUMPS, which are otherwise unable to
|
|
14
|
+
* explain themselves: two functions with identical `stage:lift` output emit different C when their
|
|
15
|
+
* records differ. */
|
|
16
|
+
export interface PrintOptions {
|
|
17
|
+
writeOrder?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function print(fn: Fn, opts: PrintOptions = {}): string {
|
|
11
21
|
const blockLabel = new Map<Block, string>();
|
|
12
22
|
fn.blocks.forEach((b, i) => blockLabel.set(b, `bb${i}`));
|
|
13
23
|
|
|
@@ -31,10 +41,14 @@ export function print(fn: Fn): string {
|
|
|
31
41
|
}
|
|
32
42
|
const ref = (v: Value) => name.get(v) ?? '%<undef>';
|
|
33
43
|
|
|
44
|
+
// Present only when asked for AND measured, so an unmeasured fn prints identically either way —
|
|
45
|
+
// the distinction a reader of the dump most needs to see.
|
|
46
|
+
const order = opts.writeOrder ? fn.writeOrder : undefined;
|
|
34
47
|
const lines: string[] = [`fn ${fn.name} {`];
|
|
35
48
|
for (const b of fn.blocks) {
|
|
36
49
|
const params = b.params.map((p) => `${ref(p)}: ${typeToString(p.type)}`).join(', ');
|
|
37
|
-
|
|
50
|
+
const writes = order?.writes.get(b);
|
|
51
|
+
lines.push(`^${blockLabel.get(b)}(${params}):` + (writes === undefined ? '' : ` ; writes=${writes}`));
|
|
38
52
|
for (const op of b.ops) {
|
|
39
53
|
let s = ' ';
|
|
40
54
|
if (op.results.length) {
|
|
@@ -52,6 +66,17 @@ export function print(fn: Fn): string {
|
|
|
52
66
|
s += ' ' + args.join(', ');
|
|
53
67
|
}
|
|
54
68
|
s += fmtAttrs(op.attrs);
|
|
69
|
+
// Per SUCCESSOR, in successor-arg order: the ordinal of this block's last write to each
|
|
70
|
+
// destination param's key — what `structure.ts`'s edge-copy sort reads — and `-` for a
|
|
71
|
+
// destination this block never wrote.
|
|
72
|
+
if (order?.writes.has(b) && op.successors.length) {
|
|
73
|
+
const rec = order.lastWrite.get(b);
|
|
74
|
+
s +=
|
|
75
|
+
' ; order ' +
|
|
76
|
+
op.successors
|
|
77
|
+
.map((su) => `^${blockLabel.get(su.block)}(${su.block.params.map((p) => rec?.get(p) ?? '-').join(', ')})`)
|
|
78
|
+
.join(' ');
|
|
79
|
+
}
|
|
55
80
|
lines.push(s);
|
|
56
81
|
}
|
|
57
82
|
}
|
package/src/ir/simplify.ts
CHANGED
|
@@ -51,11 +51,11 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
|
|
|
51
51
|
const incoming = edgesTo(b);
|
|
52
52
|
for (let i = b.params.length - 1; i >= 0; i--) {
|
|
53
53
|
const param = b.params[i];
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
54
|
+
const collapsed = trivialPhiValue(incoming, i, param);
|
|
55
|
+
if (collapsed === null) {
|
|
56
56
|
continue; // a genuine join, or unreachable (no in-edges at all)
|
|
57
57
|
}
|
|
58
|
-
replaceAllUsesWith(fn, param,
|
|
58
|
+
replaceAllUsesWith(fn, param, collapsed);
|
|
59
59
|
onRemoved?.(param);
|
|
60
60
|
b.params.splice(i, 1);
|
|
61
61
|
for (const s of incoming) {
|
|
@@ -70,3 +70,190 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/** The ONE value block param `i` collapses to — the value every in-edge passes it, ignoring a
|
|
75
|
+
* back edge's self-reference — or null when it is a genuine join (two distinct values) or the
|
|
76
|
+
* block is unreachable (no in-edges at all). THE trivial-phi predicate, shared by the mutating
|
|
77
|
+
* fixpoint and the non-mutating check below so the two cannot come to disagree about which params
|
|
78
|
+
* are trivial.
|
|
79
|
+
*
|
|
80
|
+
* `=== null` IS THE ONLY SAFE TEST. A genuine collapse value is never null, but it CAN be
|
|
81
|
+
* `undefined` where an edge carries fewer args than the block has params — a malformed graph the
|
|
82
|
+
* verifier catches elsewhere, and one today's callers pass straight through rather than treat as
|
|
83
|
+
* "not trivial". A truthiness test would silently change that.
|
|
84
|
+
*/
|
|
85
|
+
function trivialPhiValue(incoming: readonly Successor[], i: number, param: Value): Value | null {
|
|
86
|
+
const distinct = [...new Set(incoming.map((s) => s.args[i]).filter((v) => v !== param))];
|
|
87
|
+
return distinct.length === 1 ? distinct[0] : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* "Is there a block parameter `simplifyTrivialPhis` would remove?" — the same predicate, asked
|
|
92
|
+
* without mutating. Returns the first such param and its block, or null.
|
|
93
|
+
*
|
|
94
|
+
* A BOUNDARY postcondition rather than an IR invariant, which is why `ir/verify.ts` is the wrong
|
|
95
|
+
* home for it: a trivial phi is well-formed IR, and two places mint one deliberately and clear it
|
|
96
|
+
* within their own scope — Braun's construction in `frontend/ssa.ts`, and the `addrnum`
|
|
97
|
+
* pre-recovery pass, whose numbering half leaves one for its own cleanup half. What no pass may do
|
|
98
|
+
* is leave one STANDING once the CFG stops moving (pipeline.ts `raiseRecovered`), because from
|
|
99
|
+
* there down the structurer reads a block parameter as a JOIN and gives it a local of its own.
|
|
100
|
+
* That is how a CFG-motion pass does its damage three stages away: `raise/retsink.ts` stranded a
|
|
101
|
+
* single-predecessor merge, the structurer spelled its alias as `v0 = 0; return v0;`, and Regime-A
|
|
102
|
+
* switch recovery read the block as a SECOND `default` candidate and declined every fall-through
|
|
103
|
+
* tree over it.
|
|
104
|
+
*/
|
|
105
|
+
export function firstTrivialPhi(fn: Fn): { block: Block; param: Value } | null {
|
|
106
|
+
// ONE pass over the successor edges, indexed by target — `simplifyTrivialPhis` rescans the
|
|
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 variant).
|
|
109
|
+
const incomingOf = new Map<Block, Successor[]>();
|
|
110
|
+
for (const pb of fn.blocks) {
|
|
111
|
+
for (const op of pb.ops) {
|
|
112
|
+
for (const s of op.successors) {
|
|
113
|
+
const prev = incomingOf.get(s.block);
|
|
114
|
+
if (prev) {
|
|
115
|
+
prev.push(s);
|
|
116
|
+
} else {
|
|
117
|
+
incomingOf.set(s.block, [s]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const b of fn.blocks) {
|
|
123
|
+
if (b === fn.blocks[0]) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const incoming = incomingOf.get(b) ?? [];
|
|
127
|
+
for (let i = 0; i < b.params.length; i++) {
|
|
128
|
+
const param = b.params[i];
|
|
129
|
+
if (trivialPhiValue(incoming, i, param) !== null) {
|
|
130
|
+
return { block: b, param };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Remove block params NOTHING READS. Their edge args are dropped with them, so a dead join value
|
|
139
|
+
* never surfaces downstream: left in place, the structurer dutifully materializes copies for it
|
|
140
|
+
* on every in-edge (`a3 = v0` after a loop whose counter nobody consumes), and gates keyed on
|
|
141
|
+
* "does this exit carry anything" see cargo that is not there.
|
|
142
|
+
*
|
|
143
|
+
* A read is an OP OPERAND. An edge arg is not one in itself — it forwards a value into a slot, and
|
|
144
|
+
* that is a read exactly when the slot is live — so liveness is a LEAST FIXPOINT seeded by the op
|
|
145
|
+
* operands and grown backwards along the edges. Asking instead "does any arg mention it" is what a
|
|
146
|
+
* per-round reader scan does, and it keeps a mutually-dead CYCLE alive forever: two params feeding
|
|
147
|
+
* only each other across blocks each count as the other's reader. Such a cycle is what the
|
|
148
|
+
* frontend's on-demand construction mints around a loop for a register left holding a stale value,
|
|
149
|
+
* and it is not inert — `raise/struct-arrays.ts` refuses an element pointer that reaches a block
|
|
150
|
+
* arg, so a dead ring around one costs the struct view of an array it has a single real use of.
|
|
151
|
+
*
|
|
152
|
+
* Still conservative in one direction: a param a real op operand reads survives, however dead that
|
|
153
|
+
* op later proves. Liveness is over the IR as it stands, and op-level DCE belongs to
|
|
154
|
+
* `pattern/engine.ts`.
|
|
155
|
+
*
|
|
156
|
+
* THAT MAKES TWO LIVENESS MODELS OVER ONE GRAPH, and they now DISAGREE about the same edge.
|
|
157
|
+
* `pattern/engine.ts`'s `dce` still counts every successor arg as a use unconditionally — the rule
|
|
158
|
+
* abandoned here — and it runs after every changing pre-recovery pass where this runs once, inside
|
|
159
|
+
* the frontend's `finish()`. The disagreement is one-sided and safe: `dce` is the COARSER of the
|
|
160
|
+
* two, so it only ever keeps an op this would have let go, never the reverse. Reach today is ZERO
|
|
161
|
+
* and that is measured rather than argued — re-running this fixpoint over the 458 klonoa functions
|
|
162
|
+
* that clear the frontend, at three checkpoints (straight after the lift, after the idiom fold and
|
|
163
|
+
* after type recovery), removes 0 further params at every one. Booked here so a future round that
|
|
164
|
+
* finds a nonzero reads it as the known divergence rather than a new discovery.
|
|
165
|
+
*
|
|
166
|
+
* THE ENTRY BLOCK IS NEVER TOUCHED — its params are the function's signature, and an argument the
|
|
167
|
+
* body ignores is still an argument (frontend/ssa.ts `ensureParam` creates exactly those on
|
|
168
|
+
* purpose). They are therefore seeded LIVE rather than merely skipped: an entry block that is also
|
|
169
|
+
* a loop header has in-edges, and a slot that is kept has to keep whatever feeds it defined. A
|
|
170
|
+
* function with no blocks has no entry and no params, and is a no-op rather than a throw — this
|
|
171
|
+
* runs mid-construction, ahead of the verifier that rejects such a graph.
|
|
172
|
+
* Returns how many were removed.
|
|
173
|
+
*/
|
|
174
|
+
export function pruneDeadParams(fn: Fn, onRemoved?: (param: Value) => void): number {
|
|
175
|
+
// Liveness travels BACKWARD — from a live slot to the args feeding it — so it is driven off a
|
|
176
|
+
// WORKLIST over an inverted edge index, not by re-sweeping the graph until a round adds nothing.
|
|
177
|
+
// Round-robin over `fn.blocks` in forward order advances one hop per round along a chain of
|
|
178
|
+
// block params, which is quadratic in the chain length. The worklist is linear in
|
|
179
|
+
// (values + edge args) whatever the block order, and it matters because this is shared L1
|
|
180
|
+
// substrate that runs once per lift on every ISA. Nothing in the corpus reaches the shape today
|
|
181
|
+
// — the largest klonoa function that lifts at all is 107 blocks — but that ceiling is a property
|
|
182
|
+
// of what this frontend currently accepts (274 of the checkout's 732 `.s` decline), not of the
|
|
183
|
+
// game's code, so `test/dead-params.test.ts` budgets both halves against `parse` of the same
|
|
184
|
+
// text.
|
|
185
|
+
//
|
|
186
|
+
// `edgesTo` is the same index the removal pass needs, so it is built ONCE for both: without it
|
|
187
|
+
// each removed param re-walks every op in the function.
|
|
188
|
+
const edgesTo = new Map<Block, Successor[]>();
|
|
189
|
+
for (const b of fn.blocks) {
|
|
190
|
+
for (const op of b.ops) {
|
|
191
|
+
for (const s of op.successors) {
|
|
192
|
+
const list = edgesTo.get(s.block);
|
|
193
|
+
if (list === undefined) {
|
|
194
|
+
edgesTo.set(s.block, [s]);
|
|
195
|
+
} else {
|
|
196
|
+
list.push(s);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Which args feed a given slot. Keyed by the PARAM value (identity is the graph's own), so a
|
|
202
|
+
// slot that becomes live hands back exactly the values that flow into it.
|
|
203
|
+
const feeders = new Map<Value, Value[]>();
|
|
204
|
+
for (const b of fn.blocks) {
|
|
205
|
+
b.params.forEach((slot, i) => {
|
|
206
|
+
const args = (edgesTo.get(b) ?? []).map((s) => s.args[i]).filter((a): a is Value => a !== undefined);
|
|
207
|
+
if (args.length > 0) {
|
|
208
|
+
feeders.set(slot, (feeders.get(slot) ?? []).concat(args));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const live = new Set<Value>();
|
|
214
|
+
const work: Value[] = [];
|
|
215
|
+
const mark = (v: Value): void => {
|
|
216
|
+
if (!live.has(v)) {
|
|
217
|
+
live.add(v);
|
|
218
|
+
work.push(v);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
for (const p of fn.blocks[0]?.params ?? []) {
|
|
222
|
+
mark(p);
|
|
223
|
+
}
|
|
224
|
+
for (const b of fn.blocks) {
|
|
225
|
+
for (const op of b.ops) {
|
|
226
|
+
for (const v of op.operands) {
|
|
227
|
+
mark(v);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (let v = work.pop(); v !== undefined; v = work.pop()) {
|
|
232
|
+
for (const a of feeders.get(v) ?? []) {
|
|
233
|
+
mark(a);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// One removal pass suffices: `live` is the fixpoint over the whole graph, so dropping the slots
|
|
238
|
+
// outside it cannot make a surviving slot dead, and the args it drops were feeding dead slots.
|
|
239
|
+
let removed = 0;
|
|
240
|
+
for (const b of fn.blocks) {
|
|
241
|
+
if (b === fn.blocks[0]) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const incoming = edgesTo.get(b) ?? [];
|
|
245
|
+
for (let i = b.params.length - 1; i >= 0; i--) {
|
|
246
|
+
const param = b.params[i];
|
|
247
|
+
if (live.has(param)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
onRemoved?.(param);
|
|
251
|
+
b.params.splice(i, 1);
|
|
252
|
+
for (const s of incoming) {
|
|
253
|
+
s.args.splice(i, 1);
|
|
254
|
+
}
|
|
255
|
+
removed++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return removed;
|
|
259
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// THE STRUCT-NAME ALLOCATOR, in one place because there are three minters of synthesized struct
|
|
2
|
+
// names (raise/structs.ts `Struct<N>`, raise/struct-arrays.ts `Elem<N>`, l3/offmember.ts `Off<N>`)
|
|
3
|
+
// and each had rolled its own scan, at two different strengths.
|
|
4
|
+
//
|
|
5
|
+
// `l3/hoist.ts`'s header states the rule for LOCALS — "every pass that mints a local takes
|
|
6
|
+
// `nameAllocator` (or `takenNames`, to number its own)". This is that rule for structs.
|
|
7
|
+
//
|
|
8
|
+
// THE CONTRACT IS MONOTONE, never first-free: the next index is past EVERY taken one. Skipping a
|
|
9
|
+
// CONTIGUOUS PREFIX is a weaker guarantee than being free — a set holding `Off0` and `Off2` stops
|
|
10
|
+
// a first-free walk at 1, and the name minted after it is `Off2` again, one layout declared twice
|
|
11
|
+
// under one name, which is invisible in the tree (`structs` is a list, not a map) and surfaces
|
|
12
|
+
// either as a compile error or, through a name-deduping consumer, as one access reading another
|
|
13
|
+
// layout's member. That is the class of silent loss PR #127 named for `localNames`.
|
|
14
|
+
//
|
|
15
|
+
// AND AT THE Elem AND Off CALL SITES THE SEED IS 0. Measured, not assumed: instrumenting both and
|
|
16
|
+
// lifting every corpus function gives 1498 `Elem` seeds and 1417 `Off` seeds map-less, 417 and 398
|
|
17
|
+
// map-ful, all zero. Each of those two prefixes has exactly one minter and each minter runs once
|
|
18
|
+
// per function (raise/pre-recovery.ts is a linear pass list, `/offmember` a single respell), so no
|
|
19
|
+
// tree either one is handed can already carry its own prefix.
|
|
20
|
+
//
|
|
21
|
+
// THE `Struct` PREFIX IS THE COUNTER-EXAMPLE, and it is why the contract is not simply "return 0":
|
|
22
|
+
// TWO passes mint it, raise/memberarrays.ts ahead of raise/structs.ts in the pre-recovery list, so
|
|
23
|
+
// the second is routinely handed a graph already carrying the first's `Struct<N>` types and seeds
|
|
24
|
+
// past them (its own note at the seeding site says so).
|
|
25
|
+
//
|
|
26
|
+
// THAT IS NOT THE SAME AS A GUARD WITH NO INHABITANT, and the difference is why this is kept
|
|
27
|
+
// where such a guard is not. A refusal with no inhabitant still asserts a hazard, and starts
|
|
28
|
+
// deleting candidates the day its predicate widens; an allocator with no taken name is the
|
|
29
|
+
// IDENTITY — it returns the same 0 a hand-rolled counter returns. What it buys is that the 0 is a
|
|
30
|
+
// computed fact rather than a caller invariant nothing checks. The price is one scan of the taken
|
|
31
|
+
// names per call, which at the `Elem` site is a type-graph walk.
|
|
32
|
+
export function nextStructIndex(taken: Iterable<string>, prefix: string): number {
|
|
33
|
+
const re = new RegExp(`^${prefix}(\\d+)$`);
|
|
34
|
+
let next = 0;
|
|
35
|
+
for (const name of taken) {
|
|
36
|
+
const m = re.exec(name);
|
|
37
|
+
if (m) {
|
|
38
|
+
next = Math.max(next, Number(m[1]) + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return next;
|
|
42
|
+
}
|