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.
@@ -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
- * A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
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 { continuationGate } from './late-frame.ts'
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 ranked) {
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
- kids.length = 0
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 k of kids) {
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
@@ -13,4 +13,4 @@
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 const PARSEMAN_VERSION = '0.61.5'
16
+ export const PARSEMAN_VERSION = '0.61.7'