parseman 0.61.1 → 0.61.3
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 +26 -17
- package/dist/index.js.map +2 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +10 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +125 -48
- package/dist/plugin/index.js.map +3 -3
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/index.js +26 -17
- package/dist/table/index.js.map +1 -1
- package/dist/table/select-hot.d.ts +20 -1
- 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/evaluator.ts +42 -2
- package/src/plugin/index.ts +67 -26
- package/src/table/compile-rule-map.ts +8 -21
- package/src/table/select-hot.ts +35 -4
- package/src/version.ts +1 -1
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The sites to emit: highest weight per byte first, stopping at the first that
|
|
3
|
+
* does not fit in `budget` bytes. Stopping rather than skipping is what makes a
|
|
4
|
+
* larger budget a superset: a skip lets a budget that just admits a big site lose
|
|
5
|
+
* the small ones a smaller budget took after skipping it. Insertion order is rank.
|
|
6
|
+
*/
|
|
2
7
|
export declare function selectHotSites(code: readonly number[], entries: readonly number[], siteBytes: ReadonlyMap<number, number>, budget: number,
|
|
3
8
|
/** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
|
|
4
9
|
* static estimate when given. */
|
|
5
10
|
profile?: ArrayLike<number>): Set<number>;
|
|
11
|
+
/**
|
|
12
|
+
* The largest prefix of the ranking whose real source fits `ceiling`. `siteBytes`
|
|
13
|
+
* leaves out what sites add around their bodies, so the estimate can overshoot:
|
|
14
|
+
* each overshoot drops the last site and measures again. Walking down the one
|
|
15
|
+
* order is what keeps a selection a prefix of any larger budget's: that budget has
|
|
16
|
+
* every candidate this one has, so it can never settle on fewer. `undefined` when
|
|
17
|
+
* not even the first site fits.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fitHotSites(code: readonly number[], entries: readonly number[], siteBytes: ReadonlyMap<number, number>,
|
|
20
|
+
/** What any selection costs before its first site body. */
|
|
21
|
+
fixed: number, ceiling: number, sourceBytes: (sites: ReadonlySet<number>) => number, profile?: ArrayLike<number>): {
|
|
22
|
+
readonly sites: ReadonlySet<number>;
|
|
23
|
+
readonly bytes: number;
|
|
24
|
+
} | undefined;
|
|
6
25
|
//# sourceMappingURL=select-hot.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-hot.d.ts","sourceRoot":"","sources":["../../src/table/select-hot.ts"],"names":[],"mappings":"AAuBA
|
|
1
|
+
{"version":3,"file":"select-hot.d.ts","sourceRoot":"","sources":["../../src/table/select-hot.ts"],"names":[],"mappings":"AAuBA;;;;;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,CAgBb;AAED;;;;;;;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.3";
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
package/src/plugin/evaluator.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { Combinator } from '../types.ts'
|
|
|
17
17
|
import type { DispatchArm } from '../combinators/dispatch.ts'
|
|
18
18
|
import { ref } from '../combinators/ref.ts'
|
|
19
19
|
import { rules, RULES_RECIPE, type PriorRules, type RulesOptions, type RulesRecipe } from '../combinators/parser.ts'
|
|
20
|
+
import { parseSync } from 'oxc-parser'
|
|
20
21
|
import * as parseman from '../index.ts'
|
|
21
22
|
import { confirmedBuildParamUnused } from '../compiler/build-arity.ts'
|
|
22
23
|
import { directBuilderBindings } from './direct-builder-static.ts'
|
|
@@ -72,7 +73,7 @@ function stripTsFromSource(node: Node, code: string): string {
|
|
|
72
73
|
walk(node)
|
|
73
74
|
const start = (node as { start: number }).start
|
|
74
75
|
const end = (node as { end: number }).end
|
|
75
|
-
if (cuts.length === 0) return code.slice(start, end)
|
|
76
|
+
if (cuts.length === 0) return stripComments(code.slice(start, end))
|
|
76
77
|
cuts.sort((a, b) => a[0] - b[0])
|
|
77
78
|
let out = ''
|
|
78
79
|
let cur = start
|
|
@@ -81,7 +82,46 @@ function stripTsFromSource(node: Node, code: string): string {
|
|
|
81
82
|
out += code.slice(cur, s)
|
|
82
83
|
cur = e
|
|
83
84
|
}
|
|
84
|
-
return out + code.slice(cur, end)
|
|
85
|
+
return stripComments(out + code.slice(cur, end))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The four characters ECMAScript treats as ending a line. */
|
|
89
|
+
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A reducer's source without its comments, and without the whitespace before them.
|
|
93
|
+
* A reducer ships as a string in the carried IR, where a bundler cannot strip it, and
|
|
94
|
+
* a comment must not change what a module ships. A comment still separates tokens and,
|
|
95
|
+
* if it crossed or followed a line end, still ends a line: automatic semicolon
|
|
96
|
+
* insertion and `return` depend on that line end.
|
|
97
|
+
*/
|
|
98
|
+
function stripComments(text: string): string {
|
|
99
|
+
if (!text.includes('/*') && !text.includes('//')) return text
|
|
100
|
+
const wrapped = `(${text})`
|
|
101
|
+
let out = ''
|
|
102
|
+
let cur = 0
|
|
103
|
+
let lineEnd = false
|
|
104
|
+
const comments = parseSync('reducer.ts', wrapped).comments
|
|
105
|
+
comments.forEach((c, i) => {
|
|
106
|
+
const before = text.slice(cur, c.start - 1)
|
|
107
|
+
const kept = before.trimEnd()
|
|
108
|
+
out += kept
|
|
109
|
+
lineEnd ||= LINE_TERMINATOR.test(before.slice(kept.length)) || LINE_TERMINATOR.test(c.value)
|
|
110
|
+
cur = c.end - 1
|
|
111
|
+
const after = /^\s*/.exec(text.slice(cur))![0]
|
|
112
|
+
const endsLine = LINE_TERMINATOR.test(after)
|
|
113
|
+
if (comments[i + 1]?.start === c.end + after.length) {
|
|
114
|
+
lineEnd ||= endsLine
|
|
115
|
+
cur += after.length
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (out !== '' && cur + after.length < text.length && !endsLine) {
|
|
119
|
+
if (lineEnd) out += '\n'
|
|
120
|
+
else if (after === '') out += ' '
|
|
121
|
+
}
|
|
122
|
+
lineEnd = false
|
|
123
|
+
})
|
|
124
|
+
return out + text.slice(cur)
|
|
85
125
|
}
|
|
86
126
|
|
|
87
127
|
/**
|
package/src/plugin/index.ts
CHANGED
|
@@ -170,11 +170,13 @@ export type ParsecraftPluginOptions = {
|
|
|
170
170
|
*/
|
|
171
171
|
assemblyProfile?: AssemblyProfile
|
|
172
172
|
/**
|
|
173
|
-
* Generated assembly bytes each module may spend, as a multiple of its own
|
|
174
|
-
*
|
|
173
|
+
* Generated assembly bytes each module may spend, as a multiple of its own code,
|
|
174
|
+
* comments and whitespace aside. **Default `0.5`.** `0` ships no assembly. The right number is
|
|
175
175
|
* per grammar: a larger one parses faster in steady state and costs import and
|
|
176
176
|
* first-parse time, so measure both, and keep the artifact within 5× its source
|
|
177
|
-
* (docs/design/runtime-and-size-contract.md, rules 4 and 6).
|
|
177
|
+
* (docs/design/runtime-and-size-contract.md, rules 4 and 6). A larger budget never
|
|
178
|
+
* loses a site, with one limit: a module with two AST grammars spends it in order, so
|
|
179
|
+
* a larger budget can buy the first a big site and leave the second fewer.
|
|
178
180
|
*/
|
|
179
181
|
assemblyBudget?: number
|
|
180
182
|
}
|
|
@@ -184,7 +186,9 @@ export type AssemblyProfile = {
|
|
|
184
186
|
/** The rule each corpus file is parsed from, e.g. `'Stylesheet'`. A grammar
|
|
185
187
|
* without this rule keeps the static ranking. */
|
|
186
188
|
readonly entry: string
|
|
187
|
-
/** Paths of representative inputs.
|
|
189
|
+
/** Paths of representative inputs. At least one must have content, and each that
|
|
190
|
+
* does has to parse in full without recovery, or the build fails. A file with only
|
|
191
|
+
* whitespace counts as empty. */
|
|
188
192
|
readonly files: readonly string[]
|
|
189
193
|
}
|
|
190
194
|
|
|
@@ -206,11 +210,26 @@ const PARSEMAN_MODULE = 'parseman'
|
|
|
206
210
|
|
|
207
211
|
/**
|
|
208
212
|
* The default `assemblyBudget`: generated assembly bytes a module may spend, as a
|
|
209
|
-
* multiple of its own
|
|
210
|
-
*
|
|
211
|
-
* measured curve.
|
|
213
|
+
* multiple of its own code (`codeBytes`). Half keeps every jess grammar far under the
|
|
214
|
+
* 5x ceiling (contract rule 4). A grammar that wants a different trade passes its own.
|
|
212
215
|
*/
|
|
213
|
-
const DEFAULT_ASSEMBLY_BUDGET = 0.
|
|
216
|
+
const DEFAULT_ASSEMBLY_BUDGET = 0.5
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A module's code in bytes, without its comments and whitespace. It is what a budget
|
|
220
|
+
* is a share of, so editing a comment or reflowing a line cannot move a site in or
|
|
221
|
+
* out of the assembly, and with it the module's size and parse speed.
|
|
222
|
+
*/
|
|
223
|
+
function codeBytes(code: string, comments: readonly { start: number; end: number }[], end = code.length): number {
|
|
224
|
+
let bytes = 0
|
|
225
|
+
let at = 0
|
|
226
|
+
for (const c of comments) {
|
|
227
|
+
if (c.start >= end) break
|
|
228
|
+
bytes += code.slice(at, c.start).replace(/\s+/g, '').length
|
|
229
|
+
at = c.end
|
|
230
|
+
}
|
|
231
|
+
return bytes + code.slice(at, end).replace(/\s+/g, '').length
|
|
232
|
+
}
|
|
214
233
|
|
|
215
234
|
/**
|
|
216
235
|
* The REASON a lowering refused, appended to the warning that reports it.
|
|
@@ -767,6 +786,8 @@ export type TransformMacroResult = {
|
|
|
767
786
|
map: ReturnType<MagicString['generateMap']>
|
|
768
787
|
/** Diagnostics for macro-referencing shapes that fell back to the interpreter. */
|
|
769
788
|
warnings: string[]
|
|
789
|
+
/** The sites the module's `assemblyBudget` bought, by program offset, in rank order. */
|
|
790
|
+
assemblySites: number[]
|
|
770
791
|
}
|
|
771
792
|
|
|
772
793
|
/**
|
|
@@ -1749,7 +1770,9 @@ function transformMacroImpl(
|
|
|
1749
1770
|
|
|
1750
1771
|
// Stable, reproducible per-artifact namespace: hash of module id + a label
|
|
1751
1772
|
// (binding name / arg position) — never a counter, so rebuilds are byte-stable
|
|
1752
|
-
// and two artifacts never collide when fused.
|
|
1773
|
+
// and two artifacts never collide when fused. A call is labelled by where it sits
|
|
1774
|
+
// in the module's code (`codeBytes`), so a comment above it leaves the name alone.
|
|
1775
|
+
const callLabel = (fn: string, start: number): string => `${fn}${codeBytes(code, result.comments, start)}`
|
|
1753
1776
|
const nsFor = (label: string): string =>
|
|
1754
1777
|
`_${createHash('sha1').update(`${id}#${label}`).digest('hex').slice(0, 8)}_`
|
|
1755
1778
|
|
|
@@ -2253,7 +2276,7 @@ function transformMacroImpl(
|
|
|
2253
2276
|
const carried: CarriedItem[] = [] // re-lowerable; also SERIALIZED onto the value
|
|
2254
2277
|
const importedFactories: string[] = []
|
|
2255
2278
|
for (let i = 0; i < elements.length; i++) {
|
|
2256
|
-
const r = argPieces(elements[i]!,
|
|
2279
|
+
const r = argPieces(elements[i]!, `${callLabel('compose', init.start)}_${i}`, composing)
|
|
2257
2280
|
if (!r) {
|
|
2258
2281
|
// A build-compiled argument cannot fall back: runtime `compose()` links live
|
|
2259
2282
|
// `rules()` grammars only and would throw at import. Fail here instead.
|
|
@@ -2342,14 +2365,15 @@ function transformMacroImpl(
|
|
|
2342
2365
|
* SELECTIVE ASSEMBLY, BUDGETED PER MODULE. A fused grammar's default AST/no-lines
|
|
2343
2366
|
* table carries generated code for its hottest sites only (`select-hot.ts`); the
|
|
2344
2367
|
* closure engine runs the rest. The site bodies of every fused grammar in this
|
|
2345
|
-
* module together may not exceed `assemblyBudget` times the module's own
|
|
2368
|
+
* module together may not exceed `assemblyBudget` times the module's own code (`codeBytes`).
|
|
2346
2369
|
*
|
|
2347
2370
|
* A module that declares a `composeLeaf` has named its terminal parser, so its
|
|
2348
2371
|
* `compose()` results are composable PIECES: another build re-lowers them and
|
|
2349
2372
|
* never parses through their table, so they spend nothing. Without a
|
|
2350
2373
|
* `composeLeaf`, a `compose()` result is the module's parser.
|
|
2351
2374
|
*/
|
|
2352
|
-
let assemblyBudget = Math.floor(code.
|
|
2375
|
+
let assemblyBudget = Math.floor(codeBytes(code, result.comments) * assemblyBudgetShare)
|
|
2376
|
+
const assemblySites: number[] = []
|
|
2353
2377
|
let leafDeclared: boolean | undefined
|
|
2354
2378
|
const declaresLeaf = (): boolean => leafDeclared ??= (body as Statement[]).some(stmt => {
|
|
2355
2379
|
const vd = unwrapVd(stmt)
|
|
@@ -2373,32 +2397,48 @@ function transformMacroImpl(
|
|
|
2373
2397
|
warnings.push(`${id}:${lineOf(pos)} — ${label}: its precompiled assembly cannot emit ${refusal}; the artifact runs on the closure engine`)
|
|
2374
2398
|
}
|
|
2375
2399
|
assemblyBudget -= bytes
|
|
2400
|
+
assemblySites.push(...sites)
|
|
2376
2401
|
return sites
|
|
2377
2402
|
}
|
|
2378
2403
|
/** Measured site calls over the optional corpus, or `undefined` for the static
|
|
2379
|
-
* ranking. A
|
|
2404
|
+
* ranking. A corpus that cannot run fails the build: a static ranking standing in
|
|
2405
|
+
* for the measured one configured is a slower parser that nothing reports. */
|
|
2380
2406
|
const profileOf = (compiled: CompiledRuleMapTable): Int32Array | undefined => {
|
|
2381
2407
|
// Only the default AST/no-lines table carries an assembly, so only it is profiled.
|
|
2382
2408
|
if (assemblyProfile === undefined || compiled.hostMode !== 'ast' || compiled.prog.lines === 1
|
|
2383
2409
|
|| !(assemblyProfile.entry in compiled.prog.rules)) return undefined
|
|
2384
|
-
|
|
2410
|
+
const refuse = (why: string): never => { throw new Error(`${id} — assemblyProfile: ${why}`) }
|
|
2411
|
+
if (corpusTexts(assemblyProfile).every(text => text.trim() === '')) refuse('`files` has no non-empty file')
|
|
2412
|
+
let thrower: unknown
|
|
2385
2413
|
// A builder re-lowered from carried IR is inert at build time; for COUNTING
|
|
2386
2414
|
// only, a plain node stands in for it, so the corpus can run past it.
|
|
2387
2415
|
const fns = compiled.prog.fns.map(fn => {
|
|
2388
2416
|
const type = (fn as { [INERT_BUILDER]?: string } | null)?.[INERT_BUILDER]
|
|
2389
|
-
|
|
2390
|
-
|
|
2417
|
+
if (type !== undefined) return (children: unknown, _fields: unknown, span: unknown) => ({ type, span, children })
|
|
2418
|
+
return typeof fn !== 'function' ? fn : new Proxy(fn, {
|
|
2419
|
+
apply(target, self, args) {
|
|
2420
|
+
try { return Reflect.apply(target, self, args) as unknown } catch (e) { thrower ??= target; throw e }
|
|
2421
|
+
},
|
|
2422
|
+
})
|
|
2391
2423
|
})
|
|
2392
|
-
|
|
2424
|
+
return countSiteVisits(compiled.prog, rules => {
|
|
2393
2425
|
const entry = rules[assemblyProfile.entry]!
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2426
|
+
corpusTexts(assemblyProfile).forEach((text, i) => {
|
|
2427
|
+
const file = assemblyProfile.files[i]!
|
|
2428
|
+
if (text.trim() === '') return
|
|
2429
|
+
let result
|
|
2430
|
+
try { result = run(entry, text) } catch (e) {
|
|
2431
|
+
const source = String(thrower).replace(/\s+/g, ' ').slice(0, 200)
|
|
2432
|
+
throw new Error(`${id} — assemblyProfile: ${thrower === undefined ? 'the parse' : `gate or reducer \`${source}\``} threw on ${file}: ${String(e)}`, { cause: e })
|
|
2433
|
+
}
|
|
2434
|
+
if (!result.ok || result.unconsumedFrom !== null) {
|
|
2435
|
+
refuse(`${file} does not parse from rule "${assemblyProfile.entry}": it stops at offset ${result.ok ? result.unconsumedFrom : result.span.end}`)
|
|
2436
|
+
}
|
|
2437
|
+
if (result.errors.length > 0) {
|
|
2438
|
+
refuse(`${file} parses only by recovering from ${result.errors.length} error(s), first at offset ${result.errors[0]!.span.start}`)
|
|
2439
|
+
}
|
|
2440
|
+
})
|
|
2397
2441
|
}, fns)
|
|
2398
|
-
if (parsed > 0) return counts
|
|
2399
|
-
// Not `warn()`: nothing fell back to the interpreter — the build is only untuned.
|
|
2400
|
-
warnings.push(`${id} — assemblyProfile: no corpus file parsed from rule "${assemblyProfile.entry}"; using the static ranking`)
|
|
2401
|
-
return undefined
|
|
2402
2442
|
}
|
|
2403
2443
|
/** The fused table's replacement, with its selective default assembly if any. */
|
|
2404
2444
|
const assembledReplacement = (compiled: CompiledRuleMapTable, metadata: string, select: ReadonlySet<number>): string =>
|
|
@@ -2428,7 +2468,7 @@ function transformMacroImpl(
|
|
|
2428
2468
|
let localRules: ReadonlyMap<string, Combinator<unknown>> | null = null
|
|
2429
2469
|
const importedFactories: string[] = []
|
|
2430
2470
|
if (isRulesCall(localArg)) {
|
|
2431
|
-
const evaluated = evaluateRulesFactory(localArg,
|
|
2471
|
+
const evaluated = evaluateRulesFactory(localArg, callLabel('composeLeaf', init.start))
|
|
2432
2472
|
localRules = evaluated?.ruleMap ?? null
|
|
2433
2473
|
if (evaluated?.importedFactory) importedFactories.push(evaluated.importedFactory)
|
|
2434
2474
|
} else if (localArg.type === 'Identifier') {
|
|
@@ -2455,7 +2495,7 @@ function transformMacroImpl(
|
|
|
2455
2495
|
const composing = composingTrivia(elements)
|
|
2456
2496
|
const carried: CarriedItem[] = []
|
|
2457
2497
|
for (let i = 0; i < elements.length - 1; i++) {
|
|
2458
|
-
const r = argPieces(elements[i]!,
|
|
2498
|
+
const r = argPieces(elements[i]!, `${callLabel('composeLeaf', init.start)}_${i}`, composing)
|
|
2459
2499
|
if (!r) {
|
|
2460
2500
|
warn(init.start, `composeLeaf(): argument ${i} isn't a build-resolvable recognition grammar`)
|
|
2461
2501
|
return null
|
|
@@ -3277,6 +3317,7 @@ function transformMacroImpl(
|
|
|
3277
3317
|
}
|
|
3278
3318
|
|
|
3279
3319
|
return {
|
|
3320
|
+
assemblySites: applied.length > 0 ? assemblySites : [],
|
|
3280
3321
|
code: ms.toString(),
|
|
3281
3322
|
map: ms.generateMap({ hires: true }),
|
|
3282
3323
|
warnings,
|
|
@@ -5,7 +5,7 @@ import { collectGrammarReflection, GRAMMAR_REFLECTION, type GrammarReflection }
|
|
|
5
5
|
import { encodeTableProgram, type TableSettings } from './encode.ts'
|
|
6
6
|
import { defaultAssemblyCfgs, emitTableExpression } from './emit.ts'
|
|
7
7
|
import { emitAssemblySource, Unemittable } from './emit-assembly.ts'
|
|
8
|
-
import {
|
|
8
|
+
import { fitHotSites } from './select-hot.ts'
|
|
9
9
|
|
|
10
10
|
/** The most the entry supercompiler may grow an assembly (`entry-supercompiler.ts`, its 5% source cap). */
|
|
11
11
|
const SUPERCOMPILE_GROWTH = 1.05
|
|
@@ -372,29 +372,16 @@ export function compileRuleMap(
|
|
|
372
372
|
// The estimate (prelude plus site bodies) leaves out what selected sites add
|
|
373
373
|
// around their bodies — shared helpers and the `_px` wrappers of the closure
|
|
374
374
|
// sites they call — and the supercompiler may grow the source by up to 5%. So
|
|
375
|
-
// the selection is judged by the source it actually emits, scaled for that 5
|
|
376
|
-
// and narrowed until that fits.
|
|
375
|
+
// the selection is judged by the source it actually emits, scaled for that 5%.
|
|
377
376
|
const ceiling = budget / SUPERCOMPILE_GROWTH
|
|
378
377
|
// A budget that cannot cover the prelude buys nothing, by design: no refusal.
|
|
379
378
|
if (ceiling <= fixed) return none
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const sites = selectHotSites(artifact.code, Object.values(artifact.rules), siteBytes, target - fixed, profile)
|
|
387
|
-
if (sites.size === 0) {
|
|
388
|
-
return {
|
|
389
|
-
...none,
|
|
390
|
-
refusal: `a site within its ${Math.floor(ceiling)} B selection ceiling (the prelude alone is ${fixed} B)`,
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const bytes = emitAssemblySource(table, artifact, cfg, roots, true, sites, true).source.length
|
|
394
|
-
if (bytes <= ceiling) return { sites, bytes: Math.ceil(bytes * SUPERCOMPILE_GROWTH) }
|
|
395
|
-
// Scale by the overshoot, and by at least 1%: near the ceiling a site is
|
|
396
|
-
// the unit, and the ratio alone can stall a few bytes over it.
|
|
397
|
-
target *= Math.min(ceiling / bytes, 0.99)
|
|
379
|
+
const fit = fitHotSites(artifact.code, Object.values(artifact.rules), siteBytes, fixed, ceiling,
|
|
380
|
+
sites => emitAssemblySource(table, artifact, cfg, roots, true, sites, true).source.length, profile)
|
|
381
|
+
if (fit !== undefined) return { sites: fit.sites, bytes: Math.ceil(fit.bytes * SUPERCOMPILE_GROWTH) }
|
|
382
|
+
return {
|
|
383
|
+
...none,
|
|
384
|
+
refusal: `a site within its ${Math.floor(ceiling)} B selection ceiling (the prelude alone is ${fixed} B)`,
|
|
398
385
|
}
|
|
399
386
|
}
|
|
400
387
|
|
package/src/table/select-hot.ts
CHANGED
|
@@ -21,7 +21,12 @@ const LOOP = 8
|
|
|
21
21
|
const ROUNDS = 60
|
|
22
22
|
const CAP = 1e12
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The sites to emit: highest weight per byte first, stopping at the first that
|
|
26
|
+
* does not fit in `budget` bytes. Stopping rather than skipping is what makes a
|
|
27
|
+
* larger budget a superset: a skip lets a budget that just admits a big site lose
|
|
28
|
+
* the small ones a smaller budget took after skipping it. Insertion order is rank.
|
|
29
|
+
*/
|
|
25
30
|
export function selectHotSites(
|
|
26
31
|
code: readonly number[],
|
|
27
32
|
entries: readonly number[],
|
|
@@ -41,14 +46,40 @@ export function selectHotSites(
|
|
|
41
46
|
let spent = 0
|
|
42
47
|
for (const ip of ranked) {
|
|
43
48
|
if (w(ip) === 0) break
|
|
44
|
-
|
|
45
|
-
if (spent
|
|
46
|
-
spent += bytes
|
|
49
|
+
spent += siteBytes.get(ip)!
|
|
50
|
+
if (spent > budget) break
|
|
47
51
|
selected.add(ip)
|
|
48
52
|
}
|
|
49
53
|
return selected
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The largest prefix of the ranking whose real source fits `ceiling`. `siteBytes`
|
|
58
|
+
* leaves out what sites add around their bodies, so the estimate can overshoot:
|
|
59
|
+
* each overshoot drops the last site and measures again. Walking down the one
|
|
60
|
+
* order is what keeps a selection a prefix of any larger budget's: that budget has
|
|
61
|
+
* every candidate this one has, so it can never settle on fewer. `undefined` when
|
|
62
|
+
* not even the first site fits.
|
|
63
|
+
*/
|
|
64
|
+
export function fitHotSites(
|
|
65
|
+
code: readonly number[],
|
|
66
|
+
entries: readonly number[],
|
|
67
|
+
siteBytes: ReadonlyMap<number, number>,
|
|
68
|
+
/** What any selection costs before its first site body. */
|
|
69
|
+
fixed: number,
|
|
70
|
+
ceiling: number,
|
|
71
|
+
sourceBytes: (sites: ReadonlySet<number>) => number,
|
|
72
|
+
profile?: ArrayLike<number>,
|
|
73
|
+
): { readonly sites: ReadonlySet<number>; readonly bytes: number } | undefined {
|
|
74
|
+
const ranked = [...selectHotSites(code, entries, siteBytes, ceiling - fixed, profile)]
|
|
75
|
+
for (; ranked.length > 0; ranked.pop()) {
|
|
76
|
+
const sites = new Set(ranked)
|
|
77
|
+
const bytes = sourceBytes(sites)
|
|
78
|
+
if (bytes <= ceiling) return { sites, bytes }
|
|
79
|
+
}
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
52
83
|
function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
|
|
53
84
|
let weight = new Map<number, number>()
|
|
54
85
|
const kids: number[] = []
|
package/src/version.ts
CHANGED