parseman 0.61.5 → 0.61.7
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 +109 -29
- package/dist/analysis/diagnostics.js.map +2 -2
- package/dist/cli/index.js +109 -29
- package/dist/cli/index.js.map +2 -2
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts +3 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +14 -10
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/index.js +166 -45
- package/dist/index.js.map +2 -2
- package/dist/plugin/entry-supercompiler.d.ts +15 -0
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/index.js +256 -46
- package/dist/plugin/index.js.map +2 -2
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/hold-gap.d.ts +3 -1
- package/dist/table/hold-gap.d.ts.map +1 -1
- package/dist/table/index.js +166 -45
- package/dist/table/index.js.map +2 -2
- 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/combinators/expect.ts +3 -2
- package/src/combinators/first-set.ts +14 -7
- package/src/combinators/sequence.ts +1 -1
- package/src/plugin/entry-supercompiler.ts +110 -1
- package/src/table/assemble.ts +38 -0
- package/src/table/emit-assembly.ts +42 -5
- package/src/table/encode.ts +1 -1
- package/src/table/hold-gap.ts +22 -13
- package/src/table/select-hot.ts +96 -24
- package/src/version.ts +1 -1
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