parseman 0.61.5 → 0.61.6
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/dist/analysis/diagnostics.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/index.js +58 -17
- package/dist/index.js.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts +15 -0
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/index.js +148 -18
- package/dist/plugin/index.js.map +2 -2
- package/dist/table/index.js +58 -17
- package/dist/table/index.js.map +1 -1
- package/dist/table/select-hot.d.ts +9 -0
- package/dist/table/select-hot.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/plugin/entry-supercompiler.ts +110 -1
- package/src/table/select-hot.ts +96 -24
- package/src/version.ts +1 -1
|
@@ -8,6 +8,15 @@ export declare function selectHotSites(code: readonly number[], entries: readonl
|
|
|
8
8
|
/** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
|
|
9
9
|
* static estimate when given. */
|
|
10
10
|
profile?: ArrayLike<number>): Set<number>;
|
|
11
|
+
/**
|
|
12
|
+
* The sites the function for `ip` calls, as `emit-assembly.ts` lowers it. An `OP_CONT`
|
|
13
|
+
* with a body is a late frame, lowered with its wrapper in one function, calling its
|
|
14
|
+
* corner and its tail. With no body, the emitter can't decide there and it's an alias
|
|
15
|
+
* to its wrapper, as `OP_SEED` is to its corner. A gap gate has no body either, so its
|
|
16
|
+
* item counts as the repeat's own child, and adding the row moves no other site's
|
|
17
|
+
* estimate (`gap-gate.ts`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function edgesAsLowered(code: readonly number[], siteBytes: ReadonlyMap<number, number>, ip: number, out: number[]): void;
|
|
11
20
|
/**
|
|
12
21
|
* The largest prefix of the ranking whose real source fits `ceiling`. `siteBytes`
|
|
13
22
|
* leaves out what sites add around their bodies, so the estimate can overshoot:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-hot.d.ts","sourceRoot":"","sources":["../../src/table/select-hot.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"select-hot.d.ts","sourceRoot":"","sources":["../../src/table/select-hot.ts"],"names":[],"mappings":"AAqCA;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,MAAM,EAAE,MAAM;AACd;iCACiC;AACjC,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,GAC1B,GAAG,CAAC,MAAM,CAAC,CAUb;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAe/H;AAqDD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC;AACtC,2DAA2D;AAC3D,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,MAAM,EACnD,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,GAC1B;IAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAQ7E"}
|
package/dist/version.d.ts
CHANGED
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
* Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
|
|
14
14
|
* the build if this constant drifts from the published version.
|
|
15
15
|
*/
|
|
16
|
-
export declare const PARSEMAN_VERSION = "0.61.
|
|
16
|
+
export declare const PARSEMAN_VERSION = "0.61.6";
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -65,6 +65,8 @@ type Call = {
|
|
|
65
65
|
type Edit = { readonly start: number; readonly end: number; readonly text: string }
|
|
66
66
|
|
|
67
67
|
const PIECE = /^_pf(\d+)$/
|
|
68
|
+
const MERGEABLE = /^_(?:pf|sk)\d+$/
|
|
69
|
+
const TEMP = /^_t\d+_/
|
|
68
70
|
const WRAP = 'function __parseman_supercompile__(){\n'
|
|
69
71
|
const SUFFIX = '\n}'
|
|
70
72
|
|
|
@@ -225,6 +227,113 @@ export function supercompileEntryAssembly(
|
|
|
225
227
|
return supercompileEmittedAssembly(prog, cfg, extraIps, emitted)
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
/** A name in the source: a site, or a temporary (`tmp` in `emit-assembly.ts`). */
|
|
231
|
+
type NameUse = { readonly start: number; readonly end: number; readonly name: string }
|
|
232
|
+
|
|
233
|
+
/** The uses of site and temporary names under `root`, and the names bound there. */
|
|
234
|
+
function namesUnder(root: Ast, uses: NameUse[], bound: Set<string>): boolean {
|
|
235
|
+
let plain = true
|
|
236
|
+
const bind = (id: Ast | undefined): void => {
|
|
237
|
+
const name = nameOf(id)
|
|
238
|
+
if (name !== undefined) bound.add(name)
|
|
239
|
+
}
|
|
240
|
+
const walk = (node: Ast, parent: Ast | undefined): void => {
|
|
241
|
+
if (node.type === 'VariableDeclarator') bind(ast(node.id))
|
|
242
|
+
else if (node.type === 'LabeledStatement') bind(ast(node.label))
|
|
243
|
+
else if (isFunction(node)) {
|
|
244
|
+
if (node !== root) bind(ast(node.id))
|
|
245
|
+
nodes(node.params).forEach(bind)
|
|
246
|
+
}
|
|
247
|
+
const name = nameOf(node)
|
|
248
|
+
if (name !== undefined && (MERGEABLE.test(name) || TEMP.test(name))) {
|
|
249
|
+
const isKey = parent?.type === 'Property' && parent.key === node && parent.computed !== true
|
|
250
|
+
const isProperty = parent?.type === 'MemberExpression' && parent.property === node && parent.computed !== true
|
|
251
|
+
if (parent?.type === 'Property' && parent.shorthand === true) plain = false
|
|
252
|
+
if (!isKey && !isProperty) uses.push({ ...range(node), name })
|
|
253
|
+
}
|
|
254
|
+
for (const child of children(node)) walk(child, node)
|
|
255
|
+
}
|
|
256
|
+
walk(root, undefined)
|
|
257
|
+
return plain
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Functions that read the same are one function: `optional(x)` written in 25 places
|
|
262
|
+
* is 25 rows and one body, and a trivia skip can be pooled under two labels that
|
|
263
|
+
* lower alike. This runs after the join, so a site with one caller has
|
|
264
|
+
* already gone into it, the selection is untouched and every call a parse makes is
|
|
265
|
+
* still made. Each pass can make the callers of a merged function read alike, so it
|
|
266
|
+
* repeats until nothing merges.
|
|
267
|
+
*
|
|
268
|
+
* Only identifiers are compared or renamed: a grammar's own text reaches this source
|
|
269
|
+
* as strings and regexes, and they count as written. A temporary is local by the
|
|
270
|
+
* emitter's contract and is numbered per assembly, so one this function binds is
|
|
271
|
+
* compared in order of use. A site name that's also bound somewhere, or abbreviated
|
|
272
|
+
* in `{_pf1}`, can't be renamed by name, so that source stays as it is.
|
|
273
|
+
*/
|
|
274
|
+
export function mergeSameBodies(source: string): string {
|
|
275
|
+
const parsed = parseSync('parseman-merge-same-bodies.js', WRAP + source + SUFFIX)
|
|
276
|
+
if (parsed.errors.length > 0) return source
|
|
277
|
+
const statements = nodes(ast(nodes((parsed.program as unknown as Ast).body)[0]?.body)?.body)
|
|
278
|
+
type Body = NameUse & { readonly uses: readonly NameUse[]; readonly local: ReadonlySet<string> }
|
|
279
|
+
const bodies: Body[] = []
|
|
280
|
+
const others: NameUse[] = []
|
|
281
|
+
const declared = new Set<string>()
|
|
282
|
+
for (const statement of statements) {
|
|
283
|
+
const name = statement.type === 'FunctionDeclaration' ? nameOf(ast(statement.id)) : undefined
|
|
284
|
+
const uses: NameUse[] = []
|
|
285
|
+
const bound = new Set<string>()
|
|
286
|
+
if (!namesUnder(statement, uses, bound)) return source
|
|
287
|
+
for (const local of bound) declared.add(local)
|
|
288
|
+
if (name === undefined || !MERGEABLE.test(name)) others.push(...uses)
|
|
289
|
+
else bodies.push({ ...range(statement), name, uses: uses.sort((a, b) => a.start - b.start), local: bound })
|
|
290
|
+
}
|
|
291
|
+
for (const local of declared) if (MERGEABLE.test(local)) return source
|
|
292
|
+
|
|
293
|
+
const merged = new Map<string, string>()
|
|
294
|
+
const current = (name: string): string => {
|
|
295
|
+
let at = name
|
|
296
|
+
for (let next = merged.get(at); next !== undefined; next = merged.get(at)) at = next
|
|
297
|
+
return at
|
|
298
|
+
}
|
|
299
|
+
let live = bodies
|
|
300
|
+
for (let more = true; more;) {
|
|
301
|
+
more = false
|
|
302
|
+
const first = new Map<string, string>()
|
|
303
|
+
live = live.filter(body => {
|
|
304
|
+
const temps = new Map<string, number>()
|
|
305
|
+
const edits = body.uses.map((use): Edit => {
|
|
306
|
+
if (MERGEABLE.test(use.name)) {
|
|
307
|
+
const site = current(use.name)
|
|
308
|
+
return { ...use, text: site === body.name ? '#self' : site }
|
|
309
|
+
}
|
|
310
|
+
if (!body.local.has(use.name)) return { ...use, text: use.name }
|
|
311
|
+
const seen = temps.get(use.name) ?? temps.size
|
|
312
|
+
temps.set(use.name, seen)
|
|
313
|
+
return { ...use, text: `#t${seen}` }
|
|
314
|
+
})
|
|
315
|
+
const key = render(source, body.start, body.end, edits)
|
|
316
|
+
const kept = first.get(key)
|
|
317
|
+
if (kept === undefined) { first.set(key, body.name); return true }
|
|
318
|
+
merged.set(body.name, kept)
|
|
319
|
+
more = true
|
|
320
|
+
return false
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
if (merged.size === 0) return source
|
|
324
|
+
|
|
325
|
+
const edits: Edit[] = []
|
|
326
|
+
const rename = (use: NameUse): void => {
|
|
327
|
+
if (MERGEABLE.test(use.name) && merged.has(use.name)) edits.push({ ...use, text: current(use.name) })
|
|
328
|
+
}
|
|
329
|
+
others.forEach(rename)
|
|
330
|
+
for (const body of bodies) {
|
|
331
|
+
if (merged.has(body.name)) edits.push({ start: body.start, end: body.end, text: '' })
|
|
332
|
+
else body.uses.forEach(rename)
|
|
333
|
+
}
|
|
334
|
+
return render(source, 0, source.length, edits)
|
|
335
|
+
}
|
|
336
|
+
|
|
228
337
|
/**
|
|
229
338
|
* Plugin-only final splice for an ordinary macro replacement. The table
|
|
230
339
|
* compiler emits its canonical callback-free public artifact first; this pass
|
|
@@ -274,7 +383,7 @@ export function supercompileRuleMapReplacement(
|
|
|
274
383
|
const start = body.start - prefix.length + 1
|
|
275
384
|
const end = body.end - prefix.length - 1
|
|
276
385
|
if (replacement.slice(start, end) !== FACTORY_ARITY_GUARD + canonical.source + '\n') return replacement
|
|
277
|
-
const transformed = replacement.slice(0, start) + FACTORY_ARITY_GUARD + candidate.source + '\n' + replacement.slice(end)
|
|
386
|
+
const transformed = replacement.slice(0, start) + FACTORY_ARITY_GUARD + mergeSameBodies(candidate.source) + '\n' + replacement.slice(end)
|
|
278
387
|
const checked = parseSync('parseman-entry-supercompile-replacement-result.js', prefix + transformed)
|
|
279
388
|
return checked.errors.length === 0 ? transformed : replacement
|
|
280
389
|
}
|
package/src/table/select-hot.ts
CHANGED
|
@@ -12,15 +12,28 @@
|
|
|
12
12
|
* `LOOP`, and a choice or dispatch splits its weight evenly across its arms. The
|
|
13
13
|
* weights are propagated `ROUNDS` times, so a recursive cycle saturates at `CAP`
|
|
14
14
|
* instead of diverging. Sites are then taken greedily by weight per emitted byte.
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
|
+
* A build given a corpus (`assemblyProfile`) ranks by measured calls, and takes
|
|
17
|
+
* sites connected: every call from an emitted site to a closure site, or back,
|
|
18
|
+
* crosses the engine boundary through a wrapper, so a hot site beside the emitted
|
|
19
|
+
* ones is worth more than the same site alone. Ranking by calls per byte alone
|
|
20
|
+
* leaves islands: on Jess's Less grammar at budget 0.5, it made 6.7% to 9.0% more calls
|
|
21
|
+
* per parse than the static ranking.
|
|
16
22
|
*/
|
|
17
23
|
import { childSlots } from './child-slots.ts'
|
|
18
|
-
import {
|
|
19
|
-
import { OP_CHOICE, OP_CONT, OP_DISPATCH, OP_GAPGATE, OP_REP, OP_REPV } from './ops.ts'
|
|
24
|
+
import { OP_CHOICE, OP_CONT, OP_DISPATCH, OP_GAPGATE, OP_REP, OP_REPV, OP_SEED } from './ops.ts'
|
|
20
25
|
|
|
21
26
|
const LOOP = 8
|
|
22
27
|
const ROUNDS = 60
|
|
23
28
|
const CAP = 1e12
|
|
29
|
+
/**
|
|
30
|
+
* What a crossing a site removes counts for, against one call of the site itself.
|
|
31
|
+
* Better than no weight, on every file and budget measured with real builds of
|
|
32
|
+
* Jess's grammars, but not tuned: 1 and 16 were only tried on a prototype, 2 and 8
|
|
33
|
+
* were never tried, and only 4 has been built and measured.
|
|
34
|
+
*/
|
|
35
|
+
const CROSSING_WEIGHT = 4
|
|
36
|
+
const TAKEN = -1
|
|
24
37
|
|
|
25
38
|
/**
|
|
26
39
|
* The sites to emit: highest weight per byte first, stopping at the first that
|
|
@@ -39,14 +52,8 @@ export function selectHotSites(
|
|
|
39
52
|
): Set<number> {
|
|
40
53
|
const selected = new Set<number>()
|
|
41
54
|
if (budget <= 0) return selected
|
|
42
|
-
const weight = profile === undefined ? staticWeights(code, entries) : undefined
|
|
43
|
-
const w = (ip: number): number => (weight !== undefined ? weight.get(ip) : profile![ip]) ?? 0
|
|
44
|
-
// Ties (saturated cycles) break by site offset, so a build is deterministic.
|
|
45
|
-
const ranked = [...siteBytes.keys()].sort((a, b) =>
|
|
46
|
-
w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
|
|
47
55
|
let spent = 0
|
|
48
|
-
for (const ip of
|
|
49
|
-
if (w(ip) === 0) break
|
|
56
|
+
for (const ip of profile === undefined ? staticRanking(code, entries, siteBytes) : connectedRanking(code, siteBytes, profile)) {
|
|
50
57
|
spent += siteBytes.get(ip)!
|
|
51
58
|
if (spent > budget) break
|
|
52
59
|
selected.add(ip)
|
|
@@ -54,6 +61,82 @@ export function selectHotSites(
|
|
|
54
61
|
return selected
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The sites the function for `ip` calls, as `emit-assembly.ts` lowers it. An `OP_CONT`
|
|
66
|
+
* with a body is a late frame, lowered with its wrapper in one function, calling its
|
|
67
|
+
* corner and its tail. With no body, the emitter can't decide there and it's an alias
|
|
68
|
+
* to its wrapper, as `OP_SEED` is to its corner. A gap gate has no body either, so its
|
|
69
|
+
* item counts as the repeat's own child, and adding the row moves no other site's
|
|
70
|
+
* estimate (`gap-gate.ts`).
|
|
71
|
+
*/
|
|
72
|
+
export function edgesAsLowered(code: readonly number[], siteBytes: ReadonlyMap<number, number>, ip: number, out: number[]): void {
|
|
73
|
+
out.length = 0
|
|
74
|
+
if (code[ip] !== OP_CONT) childSlots(code, ip, out)
|
|
75
|
+
else if (siteBytes.has(ip)) out.push(code[ip + 1]!, code[ip + 3]!)
|
|
76
|
+
else out.push(code[ip + 2]!)
|
|
77
|
+
for (let i = 0; i < out.length; i++) {
|
|
78
|
+
let row = out[i]!
|
|
79
|
+
for (;;) {
|
|
80
|
+
const op = code[row]
|
|
81
|
+
if (op === OP_GAPGATE || op === OP_SEED) row = code[row + 1]!
|
|
82
|
+
else if (op === OP_CONT && !siteBytes.has(row)) row = code[row + 2]!
|
|
83
|
+
else break
|
|
84
|
+
}
|
|
85
|
+
out[i] = row
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The sites by estimated weight per byte. Ties (saturated cycles) break by site
|
|
90
|
+
* offset, so a build is deterministic. */
|
|
91
|
+
function staticRanking(code: readonly number[], entries: readonly number[], siteBytes: ReadonlyMap<number, number>): number[] {
|
|
92
|
+
const weight = staticWeights(code, entries, siteBytes)
|
|
93
|
+
const w = (ip: number): number => weight.get(ip) ?? 0
|
|
94
|
+
return [...siteBytes.keys()].filter(ip => w(ip) > 0).sort((a, b) => w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The sites the corpus ran, each next one being the best by calls plus the crossings
|
|
99
|
+
* it removes, per byte. A crossing with a neighbour is estimated by the calls of
|
|
100
|
+
* whichever is the callee, since the profile counts sites, not edges. Nothing bounds
|
|
101
|
+
* the sum: a callee with N emitted callers gains N crossings' worth of its own calls,
|
|
102
|
+
* and `sequence(g.A, g.A)` counts twice. The order doesn't depend on the budget, so a
|
|
103
|
+
* larger budget's selection is still a superset.
|
|
104
|
+
*
|
|
105
|
+
* ponytail: rescans every site per pick, O(n^2) in sites the corpus ran. 3 ms of a
|
|
106
|
+
* 50 s build on Jess's Less grammar. A heap if a grammar's ever takes seconds.
|
|
107
|
+
*/
|
|
108
|
+
function* connectedRanking(code: readonly number[], siteBytes: ReadonlyMap<number, number>, calls: ArrayLike<number>): Generator<number> {
|
|
109
|
+
// Ascending, so the first best a scan meets is the lowest: a tie breaks by site.
|
|
110
|
+
const ran = [...siteBytes.keys()].filter(ip => calls[ip]! > 0).sort((a, b) => a - b)
|
|
111
|
+
const slot = new Map(ran.map((ip, i) => [ip, i]))
|
|
112
|
+
const bytes = ran.map(ip => siteBytes.get(ip)!)
|
|
113
|
+
const worth = ran.map(ip => calls[ip]!)
|
|
114
|
+
const score = worth.map((value, i) => value / bytes[i]!)
|
|
115
|
+
const crossings: [neighbour: number, gain: number][][] = ran.map(() => [])
|
|
116
|
+
const kids: number[] = []
|
|
117
|
+
ran.forEach((ip, from) => {
|
|
118
|
+
edgesAsLowered(code, siteBytes, ip, kids)
|
|
119
|
+
for (const callee of kids) {
|
|
120
|
+
const to = slot.get(callee)
|
|
121
|
+
if (to === undefined) continue
|
|
122
|
+
const gain = CROSSING_WEIGHT * calls[callee]!
|
|
123
|
+
crossings[from]!.push([to, gain])
|
|
124
|
+
crossings[to]!.push([from, gain])
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
for (let left = ran.length; left > 0; left--) {
|
|
128
|
+
let best = 0
|
|
129
|
+
for (let i = 1; i < score.length; i++) if (score[i]! > score[best]!) best = i
|
|
130
|
+
score[best] = TAKEN
|
|
131
|
+
yield ran[best]!
|
|
132
|
+
for (const [neighbour, gain] of crossings[best]!) {
|
|
133
|
+
if (score[neighbour] === TAKEN) continue
|
|
134
|
+
worth[neighbour] = worth[neighbour]! + gain
|
|
135
|
+
score[neighbour] = worth[neighbour]! / bytes[neighbour]!
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
57
140
|
/**
|
|
58
141
|
* The largest prefix of the ranking whose real source fits `ceiling`. `siteBytes`
|
|
59
142
|
* leaves out what sites add around their bodies, so the estimate can overshoot:
|
|
@@ -81,30 +164,19 @@ export function fitHotSites(
|
|
|
81
164
|
return undefined
|
|
82
165
|
}
|
|
83
166
|
|
|
84
|
-
function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
|
|
167
|
+
function staticWeights(code: readonly number[], entries: readonly number[], siteBytes: ReadonlyMap<number, number>): Map<number, number> {
|
|
85
168
|
let weight = new Map<number, number>()
|
|
86
169
|
const kids: number[] = []
|
|
87
170
|
for (let round = 0; round < ROUNDS; round++) {
|
|
88
171
|
const next = new Map<number, number>()
|
|
89
172
|
for (const ip of entries) next.set(ip, 1)
|
|
90
173
|
for (const [ip, w] of weight) {
|
|
91
|
-
|
|
92
|
-
childSlots(code, ip, kids)
|
|
174
|
+
edgesAsLowered(code, siteBytes, ip, kids)
|
|
93
175
|
const op = code[ip]
|
|
94
|
-
// A late frame is lowered with its wrapper in one function, calling these two.
|
|
95
|
-
if (op === OP_CONT && continuationGate(code, code[ip + 3]!) !== undefined) {
|
|
96
|
-
kids.length = 0
|
|
97
|
-
kids.push(code[ip + 1]!, code[ip + 3]!)
|
|
98
|
-
}
|
|
99
176
|
const scale = op === OP_REP || op === OP_REPV ? LOOP
|
|
100
177
|
: op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
|
|
101
178
|
: 1
|
|
102
|
-
for (const
|
|
103
|
-
// A gap gate has no body: weigh its item as the repeat's own child, so
|
|
104
|
-
// adding the row moves no other site's estimate (`gap-gate.ts`).
|
|
105
|
-
const kid = code[k] === OP_GAPGATE ? code[k + 1]! : k
|
|
106
|
-
next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
|
|
107
|
-
}
|
|
179
|
+
for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
|
|
108
180
|
}
|
|
109
181
|
weight = next
|
|
110
182
|
}
|
package/src/version.ts
CHANGED