parseman 0.56.0 → 0.57.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/dist/analysis/diagnostics.js +379 -72
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +379 -72
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/node.d.ts +1 -1
- package/dist/compiler/build-arity.d.ts +1 -1
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/cst/trivia-kinds.d.ts +9 -1
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.js +446 -75
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +1 -0
- package/dist/language-service/index.js.map +1 -1
- package/dist/parse-context.d.ts.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts +1 -1
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +27 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +930 -157
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +2 -0
- package/dist/plugin/ir-eval.d.ts.map +1 -1
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/run/index.js +1 -0
- package/dist/run/index.js.map +1 -1
- package/dist/table/assemble.d.ts +9 -0
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts +4 -6
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +14 -2
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +17 -2
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +5 -1
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec-baseline.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/index.js +442 -70
- package/dist/table/index.js.map +3 -3
- package/dist/table/ops.d.ts +9 -6
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +13 -0
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/select-hot.d.ts +6 -0
- package/dist/table/select-hot.d.ts.map +1 -0
- package/dist/table/trivia-gap.d.ts +31 -0
- package/dist/table/trivia-gap.d.ts.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -1
- package/src/combinators/node.ts +6 -13
- package/src/combinators/trivia-skip.ts +44 -1
- package/src/compiler/build-arity.ts +3 -3
- package/src/cst/trivia-kinds.ts +12 -1
- package/src/parse-context.ts +1 -0
- package/src/plugin/entry-supercompiler.ts +8 -9
- package/src/plugin/index.ts +138 -13
- package/src/plugin/ir-eval.ts +7 -1
- package/src/regex/classes.ts +3 -0
- package/src/table/assemble.ts +122 -24
- package/src/table/child-slots.ts +4 -6
- package/src/table/compile-rule-map.ts +55 -15
- package/src/table/emit-assembly.ts +139 -26
- package/src/table/emit.ts +8 -7
- package/src/table/encode.ts +69 -21
- package/src/table/exec-baseline.ts +1 -3
- package/src/table/exec.ts +7 -6
- package/src/table/ops.ts +9 -6
- package/src/table/program.ts +24 -0
- package/src/table/select-hot.ts +70 -0
- package/src/table/trivia-gap.ts +109 -0
- package/src/types.ts +18 -0
- package/src/version.ts +1 -1
package/src/table/encode.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
|
|
33
33
|
LEX_NODE_TERMINAL,
|
|
34
34
|
} from './lex-program.ts'
|
|
35
|
-
import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
35
|
+
import { covKindCode, dispatchIsExclusive, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
36
36
|
import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
|
|
37
37
|
import { directArrayProjection } from '../compiler/direct-projection.ts'
|
|
38
38
|
import {
|
|
@@ -699,6 +699,38 @@ class Encoder {
|
|
|
699
699
|
}
|
|
700
700
|
}
|
|
701
701
|
|
|
702
|
+
/**
|
|
703
|
+
* THE FIRST-CHARACTER GATE OF AN `optional`, `not` OR `peek` CHILD.
|
|
704
|
+
*
|
|
705
|
+
* A non-nullable child cannot match at a character outside its first set, so
|
|
706
|
+
* the wrapper's outcome there is already decided: `optional` yields `null`,
|
|
707
|
+
* `not` succeeds, `peek` fails with its own set. The interpreter's `optional`
|
|
708
|
+
* makes that test itself (`repeat.ts` `startsFirstSet`); the table entered the
|
|
709
|
+
* child — a rollback mark, the call, the child's own failure — to learn it.
|
|
710
|
+
*
|
|
711
|
+
* The gate is an `OP_GATE` row around the child, so every engine runs it
|
|
712
|
+
* unchanged, a probe or tolerant parse forwards through it as for a node's
|
|
713
|
+
* gate, and the wrapper's row keeps its layout. The engines test the class
|
|
714
|
+
* before taking the wrapper's mark. The first set is resolved through
|
|
715
|
+
* `winners`, the analysis a choice arm's gate uses, so a composed rule's set
|
|
716
|
+
* is the merged one. A scalar `not` child stays bare: `scalarTerminalNotChild`
|
|
717
|
+
* already reduces it to one recognizer call.
|
|
718
|
+
*/
|
|
719
|
+
private lookaheadGate(p: Combinator<unknown>, child: number): number {
|
|
720
|
+
// By opcode FAMILY: a line-tracked variant spells the same terminal
|
|
721
|
+
// `OP_LIT_TRACK`/`OP_RX_TRACK`, and every variant of one grammar must lay
|
|
722
|
+
// down the same words (`foldPrograms`).
|
|
723
|
+
const op = this.code[child]
|
|
724
|
+
if (op === OP_GATE || op === OP_LIT || op === OP_LIT_CI || op === OP_LIT_TRACK || op === OP_LIT_CI_TRACK
|
|
725
|
+
|| op === OP_RX || op === OP_RX_TRACK) return child
|
|
726
|
+
const rr = this.refResolver()
|
|
727
|
+
if (matchesEmpty(p, new Set(), rr)) return child
|
|
728
|
+
const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia))
|
|
729
|
+
if (cls < 0) return child
|
|
730
|
+
const e = deriveExpected(p)
|
|
731
|
+
return this.emit(OP_GATE, cls, child, this.expected(e.length > 0 ? e : [p._tag]))
|
|
732
|
+
}
|
|
733
|
+
|
|
702
734
|
/** A first set becomes a char class string, or −1 for `any`. */
|
|
703
735
|
private charClass(fs: FirstSet): number {
|
|
704
736
|
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
|
|
@@ -715,14 +747,21 @@ class Encoder {
|
|
|
715
747
|
return i
|
|
716
748
|
}
|
|
717
749
|
|
|
718
|
-
/**
|
|
719
|
-
*
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
750
|
+
/**
|
|
751
|
+
* The first set a node's or an attempt's fail-fast gate tests. A bare
|
|
752
|
+
* cross-rule ref resolves through `winners` (a compose OVERRIDE may widen
|
|
753
|
+
* it). Any other body keeps its construction-time `_meta.firstSet`, except
|
|
754
|
+
* where that is `any`: under `compose()` a body that reaches a rule of
|
|
755
|
+
* another piece is built before that rule exists, so its cached set is
|
|
756
|
+
* `any` and the node got NO gate (jess's `CompoundSelector` was entered at
|
|
757
|
+
* `{`, `Call` at `}`). The resolved set is the one a choice arm's gate uses,
|
|
758
|
+
* over the final merged map.
|
|
759
|
+
*/
|
|
760
|
+
private gateFirstSet(p: Combinator<unknown>): FirstSet {
|
|
761
|
+
const cached = p._meta.firstSet
|
|
762
|
+
return p._def.tag === 'lazy' || cached.kind === 'any'
|
|
763
|
+
? firstSetOf(p, new Set(), this.refResolver(), this.activeTrivia)
|
|
764
|
+
: cached
|
|
726
765
|
}
|
|
727
766
|
|
|
728
767
|
/**
|
|
@@ -1202,6 +1241,19 @@ class Encoder {
|
|
|
1202
1241
|
if (transactionalFirst[i] !== undefined) classes[i] = -1
|
|
1203
1242
|
}
|
|
1204
1243
|
}
|
|
1244
|
+
// A STATE-GATED ARM ON THE ORDERED PATH IS ALWAYS ENTERED. The interpreter
|
|
1245
|
+
// skips a gated-off arm and contributes nothing for it (choice.ts
|
|
1246
|
+
// firstMatch), and gates it on only by running it. A char-gated skip would
|
|
1247
|
+
// instead add the arm's static opener set whatever the gate says, so an
|
|
1248
|
+
// ordered site leaves the decision to `OP_ARMGATE`. An exclusive site keeps
|
|
1249
|
+
// its classes: there the dispatched arm's gate failing IS the choice
|
|
1250
|
+
// failing, with that arm's set, as in the interpreter's disjoint path.
|
|
1251
|
+
if (d.gates.some(g => g !== null && g !== undefined) && !dispatchIsExclusive(classes, this.cc)) {
|
|
1252
|
+
for (let k = 0; k < classes.length; k++) {
|
|
1253
|
+
const gate = d.gates[order[k]!]
|
|
1254
|
+
if (gate !== null && gate !== undefined) classes[k] = -1
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1205
1257
|
const dispIdx = this.disp.length
|
|
1206
1258
|
this.disp.push(classes)
|
|
1207
1259
|
// PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
|
|
@@ -1245,7 +1297,7 @@ class Encoder {
|
|
|
1245
1297
|
const rr = this.refResolver()
|
|
1246
1298
|
const itemClass = matchesEmpty(d.parser, new Set(), rr)
|
|
1247
1299
|
? -1
|
|
1248
|
-
: this.
|
|
1300
|
+
: this.charClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
|
|
1249
1301
|
const op = d.valueUnused ? OP_REPV : OP_REP
|
|
1250
1302
|
const min = d.tag === 'many' ? 0 : d.min
|
|
1251
1303
|
// ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
|
|
@@ -1288,7 +1340,7 @@ class Encoder {
|
|
|
1288
1340
|
: this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
|
|
1289
1341
|
}
|
|
1290
1342
|
case 'optional': {
|
|
1291
|
-
const head = this.emit(OP_OPT, this.node(d.parser).ip)
|
|
1343
|
+
const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
|
|
1292
1344
|
if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
|
|
1293
1345
|
return head
|
|
1294
1346
|
}
|
|
@@ -1409,16 +1461,14 @@ class Encoder {
|
|
|
1409
1461
|
// `deriveExpected(body)`, which for a body with nullable leading terms
|
|
1410
1462
|
// names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
|
|
1411
1463
|
// running the body reports `['"x"']` alone.
|
|
1412
|
-
if (matchesEmpty(d.parser)) return body
|
|
1464
|
+
if (matchesEmpty(d.parser, new Set(), this.refResolver())) return body
|
|
1413
1465
|
// A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
|
|
1414
1466
|
// first set from the ref's base binding. Under a compose OVERRIDE the winner
|
|
1415
1467
|
// for `Y` may be WIDER, so the cached set gates out input the merged rule
|
|
1416
1468
|
// accepts (the reducer reroutes via `winners`, but this recognizer gate did
|
|
1417
1469
|
// not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
|
|
1418
1470
|
// resolves the call. Non-ref bodies keep the cached set (byte-identical).
|
|
1419
|
-
const fs = d.parser
|
|
1420
|
-
? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
|
|
1421
|
-
: d.parser._meta.firstSet
|
|
1471
|
+
const fs = this.gateFirstSet(d.parser)
|
|
1422
1472
|
if (fs.kind === 'any') return body
|
|
1423
1473
|
const cls = this.charClass(fs)
|
|
1424
1474
|
if (cls < 0) return body
|
|
@@ -1558,7 +1608,7 @@ class Encoder {
|
|
|
1558
1608
|
// other engine reported `['not(literal)']`. It hid because `expected` is a
|
|
1559
1609
|
// TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
|
|
1560
1610
|
case 'not':
|
|
1561
|
-
return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
|
|
1611
|
+
return this.emit(OP_NOT, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(false, d.parser._tag)))
|
|
1562
1612
|
case 'guard':
|
|
1563
1613
|
// `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
|
|
1564
1614
|
return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
|
|
@@ -1603,7 +1653,7 @@ class Encoder {
|
|
|
1603
1653
|
// lookahead's failure is "the guard did not hold", and naming the body's
|
|
1604
1654
|
// internals offers a token the parse never asked the author for. The
|
|
1605
1655
|
// table propagated the body's set instead.
|
|
1606
|
-
return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
|
|
1656
|
+
return this.emit(OP_PEEK, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(true, d.parser._tag)))
|
|
1607
1657
|
// A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
|
|
1608
1658
|
// lowering was correct only for a choice arm.
|
|
1609
1659
|
case 'attempt': {
|
|
@@ -1619,10 +1669,8 @@ class Encoder {
|
|
|
1619
1669
|
// not a slower equivalent.
|
|
1620
1670
|
// Same override-aware resolution as the node gate: a bare-ref inner resolves
|
|
1621
1671
|
// its gate through `winners` so a widened winner is not gated out.
|
|
1622
|
-
const fs = d.parser
|
|
1623
|
-
|
|
1624
|
-
: d.parser._meta.firstSet
|
|
1625
|
-
if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
|
|
1672
|
+
const fs = this.gateFirstSet(d.parser)
|
|
1673
|
+
if (fs.kind === 'any' || matchesEmpty(d.parser, new Set(), this.refResolver())) return inner
|
|
1626
1674
|
const cls = this.charClass(fs)
|
|
1627
1675
|
if (cls < 0) return inner
|
|
1628
1676
|
const e = deriveExpected(d.parser)
|
|
@@ -487,9 +487,7 @@ function makeDriver(
|
|
|
487
487
|
children: readonly unknown[], fields: undefined, span: { start: number; end: number },
|
|
488
488
|
rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
|
|
489
489
|
) => unknown
|
|
490
|
-
const st = (flags & 8) !== 0
|
|
491
|
-
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
492
|
-
: undefined
|
|
490
|
+
const st = (flags & 8) !== 0 ? ctx.state : undefined
|
|
493
491
|
const nd = build(
|
|
494
492
|
cap.children, undefined,
|
|
495
493
|
code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end },
|
package/src/table/exec.ts
CHANGED
|
@@ -1123,7 +1123,10 @@ function makeDriver(
|
|
|
1123
1123
|
// choice.ts:150 is a `continue`, not a failure — the arm is treated as
|
|
1124
1124
|
// never entered, so a cut it might have raised must not cut the choice.
|
|
1125
1125
|
ctx._fc = false
|
|
1126
|
-
|
|
1126
|
+
// No failure POSITION: an ordered choice ranks arms by `_fe`, and a skipped
|
|
1127
|
+
// arm must not rank. A dispatched choice reads `_fx` alone, so this arm's
|
|
1128
|
+
// set is still the answer when it is the one selected.
|
|
1129
|
+
ctx._fe = -1
|
|
1127
1130
|
ctx._fx = fx[code[ip + 3]!] as string[]
|
|
1128
1131
|
return FAIL
|
|
1129
1132
|
}
|
|
@@ -1479,9 +1482,7 @@ function makeDriver(
|
|
|
1479
1482
|
if (v === FAIL) return FAIL
|
|
1480
1483
|
const end = EC.e
|
|
1481
1484
|
const span = code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end }
|
|
1482
|
-
const st = (flags & 8) !== 0
|
|
1483
|
-
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
1484
|
-
: undefined
|
|
1485
|
+
const st = (flags & 8) !== 0 ? ctx.state : undefined
|
|
1485
1486
|
const tagIdx = code[ip + 6]!
|
|
1486
1487
|
const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
|
|
1487
1488
|
|
|
@@ -1518,13 +1519,13 @@ function makeDriver(
|
|
|
1518
1519
|
nd = kids[0]
|
|
1519
1520
|
} else if (proj >= 0) {
|
|
1520
1521
|
nd = hostCst && host !== undefined
|
|
1521
|
-
? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog,
|
|
1522
|
+
? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
|
|
1522
1523
|
: projectChild(kids, proj, type)
|
|
1523
1524
|
} else if (buildIdx >= 0) {
|
|
1524
1525
|
if (hostCst && host !== undefined) {
|
|
1525
1526
|
// A direct builder is bypassed under a CST host: the host must never
|
|
1526
1527
|
// receive an arbitrary AST object as a child of a CST node.
|
|
1527
|
-
nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog,
|
|
1528
|
+
nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
|
|
1528
1529
|
} else {
|
|
1529
1530
|
const build = fns[buildIdx] as (
|
|
1530
1531
|
children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
|
package/src/table/ops.ts
CHANGED
|
@@ -339,13 +339,16 @@ export const OP_REJECT = 35
|
|
|
339
339
|
* arm INSTEAD of sitting inside it, so the encoder still reads the arm's own
|
|
340
340
|
* first set for `disp` and the site keeps its dispatch slot.
|
|
341
341
|
*
|
|
342
|
-
* A blocked arm is SKIPPED, not failed — `choice.ts
|
|
343
|
-
* therefore clears `_fc` on the gate-false path exactly as `OP_REJECT` does:
|
|
342
|
+
* A blocked arm is SKIPPED, not failed — `choice.ts` firstMatch `continue`s. The
|
|
343
|
+
* row therefore clears `_fc` on the gate-false path exactly as `OP_REJECT` does:
|
|
344
344
|
* "pretend this arm was never entered", so no cut it might have raised survives
|
|
345
|
-
* to cut the choice. It
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
345
|
+
* to cut the choice. It sets `_fe` to −1, so an ORDERED choice, which ranks arms
|
|
346
|
+
* by failure position, takes nothing from it, as the interpreter does. It still
|
|
347
|
+
* leaves `e` in `_fx`, because the other path a blocked arm can be reached by is
|
|
348
|
+
* exclusive first-char dispatch, where skip-and-retry and fail-the-choice are the
|
|
349
|
+
* same thing and the interpreter answers with `deriveExpected(arm)` — this set.
|
|
350
|
+
* On an ordered site the encoder gives a gated arm no char class, so it is always
|
|
351
|
+
* entered and this row, not a char gate, decides what it contributes.
|
|
349
352
|
*
|
|
350
353
|
* The predicate is a live function, like `OP_GUARD`'s, so a grammar using one is
|
|
351
354
|
* runtime-only for `emitTableModule` unless `fnSources` are supplied.
|
package/src/table/program.ts
CHANGED
|
@@ -273,6 +273,19 @@ export function scanRefs(s: ScanSpec): SubtreeRef[] {
|
|
|
273
273
|
return refs
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* The sites an assembly links from OUTSIDE its rule entries — the scan pool's
|
|
278
|
+
* subtrees, then the scan-skip sets — in the one order every assembler names
|
|
279
|
+
* them. The emitted source is keyed by this order, so a second copy of the list
|
|
280
|
+
* that disagrees prints different source for the same program.
|
|
281
|
+
*/
|
|
282
|
+
export function assemblyExtraIps(prog: TableProgram): number[] {
|
|
283
|
+
const ips: number[] = []
|
|
284
|
+
for (const s of prog.scans ?? []) for (const r of scanRefs(s)) ips.push(r[0])
|
|
285
|
+
for (const set of prog.scanSkip ?? []) for (const r of set) ips.push(r[0])
|
|
286
|
+
return ips
|
|
287
|
+
}
|
|
288
|
+
|
|
276
289
|
/** A trivia combinator as data. `arms` empty means a plain `trivia(regex)`. */
|
|
277
290
|
export type TriviaSpec = {
|
|
278
291
|
/** `[label, regexSource, regexFlags]` per classified arm. */
|
|
@@ -644,6 +657,17 @@ export function classHas(cls: ResolvedClass, code: number): boolean {
|
|
|
644
657
|
return false
|
|
645
658
|
}
|
|
646
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Whether a choice with these arm classes (indices into the class-spec pool `cc`)
|
|
662
|
+
* takes the O(1) exclusive dispatch — the same decision `resolveDispatch` makes,
|
|
663
|
+
* for an encoder that has only the specs.
|
|
664
|
+
*/
|
|
665
|
+
export function dispatchIsExclusive(arms: readonly number[], cc: readonly string[]): boolean {
|
|
666
|
+
const resolved: ResolvedClass[] = []
|
|
667
|
+
for (const ci of arms) if (ci >= 0) resolved[ci] = resolveClass(cc[ci]!)
|
|
668
|
+
return resolveDispatch(arms, resolved).exclusive
|
|
669
|
+
}
|
|
670
|
+
|
|
647
671
|
function resolveDispatch(arms: readonly number[], cc: readonly ResolvedClass[]): ResolvedDispatch {
|
|
648
672
|
const ascii = new Uint8Array(128)
|
|
649
673
|
const hi: number[] = []
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH SITES A SELECTIVE BUILD-TIME ASSEMBLY EMITS.
|
|
3
|
+
*
|
|
4
|
+
* The emitted engine beats the closure engine per site, but its bytes are paid by
|
|
5
|
+
* every process that loads the module. Emitting every site made jess's Less AST
|
|
6
|
+
* artifact 20.8x its grammar source. So a build emits only the sites with the
|
|
7
|
+
* highest ESTIMATED hotness per byte, up to a byte budget, and the closure engine
|
|
8
|
+
* runs the rest (`assemble.ts`, the `HYBRID` path).
|
|
9
|
+
*
|
|
10
|
+
* Hotness is estimated statically, since a macro build has no workload to
|
|
11
|
+
* profile: every rule entry starts at 1, a repetition multiplies its body by
|
|
12
|
+
* `LOOP`, and a choice or dispatch splits its weight evenly across its arms. The
|
|
13
|
+
* weights are propagated `ROUNDS` times, so a recursive cycle saturates at `CAP`
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
import { childSlots } from './child-slots.ts'
|
|
18
|
+
import { OP_CHOICE, OP_DISPATCH, OP_REP, OP_REPV } from './ops.ts'
|
|
19
|
+
|
|
20
|
+
const LOOP = 8
|
|
21
|
+
const ROUNDS = 60
|
|
22
|
+
const CAP = 1e12
|
|
23
|
+
|
|
24
|
+
/** The sites to emit: highest weight per byte first, while they fit in `budget` bytes. */
|
|
25
|
+
export function selectHotSites(
|
|
26
|
+
code: readonly number[],
|
|
27
|
+
entries: readonly number[],
|
|
28
|
+
siteBytes: ReadonlyMap<number, number>,
|
|
29
|
+
budget: number,
|
|
30
|
+
/** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
|
|
31
|
+
* static estimate when given. */
|
|
32
|
+
profile?: ArrayLike<number>,
|
|
33
|
+
): Set<number> {
|
|
34
|
+
const selected = new Set<number>()
|
|
35
|
+
if (budget <= 0) return selected
|
|
36
|
+
const weight = profile === undefined ? staticWeights(code, entries) : undefined
|
|
37
|
+
const w = (ip: number): number => (weight !== undefined ? weight.get(ip) : profile![ip]) ?? 0
|
|
38
|
+
// Ties (saturated cycles) break by site offset, so a build is deterministic.
|
|
39
|
+
const ranked = [...siteBytes.keys()].sort((a, b) =>
|
|
40
|
+
w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
|
|
41
|
+
let spent = 0
|
|
42
|
+
for (const ip of ranked) {
|
|
43
|
+
if (w(ip) === 0) break
|
|
44
|
+
const bytes = siteBytes.get(ip)!
|
|
45
|
+
if (spent + bytes > budget) continue
|
|
46
|
+
spent += bytes
|
|
47
|
+
selected.add(ip)
|
|
48
|
+
}
|
|
49
|
+
return selected
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
|
|
53
|
+
let weight = new Map<number, number>()
|
|
54
|
+
const kids: number[] = []
|
|
55
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
56
|
+
const next = new Map<number, number>()
|
|
57
|
+
for (const ip of entries) next.set(ip, 1)
|
|
58
|
+
for (const [ip, w] of weight) {
|
|
59
|
+
kids.length = 0
|
|
60
|
+
childSlots(code, ip, kids)
|
|
61
|
+
const op = code[ip]
|
|
62
|
+
const scale = op === OP_REP || op === OP_REPV ? LOOP
|
|
63
|
+
: op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
|
|
64
|
+
: 1
|
|
65
|
+
for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
|
|
66
|
+
}
|
|
67
|
+
weight = next
|
|
68
|
+
}
|
|
69
|
+
return weight
|
|
70
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CharRange, Combinator, ParseContext } from '../types.ts'
|
|
2
|
+
import type { LabeledTriviaSpec } from '../cst/trivia-kinds.ts'
|
|
3
|
+
import { commitTriviaScan, fastTriviaScanner, scanTriviaCompact } from '../combinators/trivia-skip.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* THE TABLE ENGINES' DEFERRED-COMMIT TRIVIA SKIP: `commitTriviaScan(scanTriviaCompact(…))`,
|
|
7
|
+
* the path every labelled trivia scan takes once a log is live, answered without
|
|
8
|
+
* the scan where the answer is already known. Both engines call it: the closure
|
|
9
|
+
* engine's `skipTrivia` and the emitted assembly's skips.
|
|
10
|
+
*
|
|
11
|
+
* ONLY THE ROOT LOG IS LIVE, so only a root-selected chunk can be recorded,
|
|
12
|
+
* and a run in which no character can start a selected arm holds none: its end
|
|
13
|
+
* is the whole answer, and the unlabelled scanner gives it without the
|
|
14
|
+
* chunk-by-chunk visit. Every chunk starts inside the run, so a run free of
|
|
15
|
+
* selected leaders is free of selected chunks. jess parses with
|
|
16
|
+
* `rootTrivia: { select: [comment labels] }`, which is this case for every run
|
|
17
|
+
* of plain whitespace.
|
|
18
|
+
*
|
|
19
|
+
* A SEQUENCE TERM THAT MATCHES NOTHING hands back the cursor from BEFORE its
|
|
20
|
+
* trivia, so the next term asks for the same gap again: on `benchmark.less`,
|
|
21
|
+
* 21,741 of the closure engine's 53,607 trivia scans per parse started where
|
|
22
|
+
* the previous one did. A scan that recorded nothing records nothing again from
|
|
23
|
+
* the same position, in the same parse, under the same trivia, root selection,
|
|
24
|
+
* root-capture policy, trivia log and state, and with no CST capture, and it
|
|
25
|
+
* ends at the same place, so the last such scan answers the repeat (`ctx._gap`).
|
|
26
|
+
* The state is part of the key because a trivia arm can read it (`gate`), and
|
|
27
|
+
* the log because a `leaf` sets it aside while its child runs. The memo lives
|
|
28
|
+
* on the context so it is released with the parse.
|
|
29
|
+
*
|
|
30
|
+
* Neither applies with `trackLines` (a scan records line ranges) or under CST
|
|
31
|
+
* capture (a scan records into the open buffer).
|
|
32
|
+
*/
|
|
33
|
+
export function skipDeferredTrivia(input: string, cur: number, ctx: ParseContext): number {
|
|
34
|
+
const triviaP = ctx.trivia
|
|
35
|
+
const plain = ctx.trackLines !== true && ctx.captureTrivia !== true
|
|
36
|
+
const gap = ctx._gap
|
|
37
|
+
if (plain && gap !== undefined && cur === gap.at && input === gap.input && triviaP === gap.of
|
|
38
|
+
&& ctx._rootTriviaCapture === gap.root && ctx.state === gap.state
|
|
39
|
+
&& ctx._rootTriviaKindIndex === gap.rootKinds && ctx._triviaLog === gap.log) return gap.end
|
|
40
|
+
const end = clearRunEnd(input, cur, ctx, triviaP, plain)
|
|
41
|
+
if (end >= 0) {
|
|
42
|
+
remember(cur, end, ctx, input, triviaP)
|
|
43
|
+
return end
|
|
44
|
+
}
|
|
45
|
+
const scan = scanTriviaCompact(input, cur, ctx)
|
|
46
|
+
if (typeof scan !== 'number') return commitTriviaScan(scan)
|
|
47
|
+
if (plain) remember(cur, scan, ctx, input, triviaP)
|
|
48
|
+
return scan
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The end of the run at `cur` when nothing root-selected lies in it, else −1. */
|
|
52
|
+
function clearRunEnd(input: string, cur: number, ctx: ParseContext, triviaP: Combinator<unknown> | undefined, plain: boolean): number {
|
|
53
|
+
const rootKinds = ctx._rootTriviaKindIndex
|
|
54
|
+
const spec = triviaP?._meta.labeledTriviaSpec
|
|
55
|
+
if (!plain || triviaP === undefined || spec == null || rootKinds === undefined || ctx._triviaLog !== undefined) return -1
|
|
56
|
+
if (spec.rootKinds !== rootKinds) prime(spec, triviaP, rootKinds)
|
|
57
|
+
const fast = spec.rootScan
|
|
58
|
+
if (fast === null) return -1
|
|
59
|
+
const end = fast(input, cur)
|
|
60
|
+
return ctx._rootTriviaCapture === false || clearOf(spec.rootLeaders, input, cur, end) ? end : -1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function remember(cur: number, end: number, ctx: ParseContext, input: string, triviaP: Combinator<unknown> | undefined): void {
|
|
64
|
+
const gap = ctx._gap
|
|
65
|
+
if (gap === undefined) {
|
|
66
|
+
ctx._gap = { at: cur, end, input, of: triviaP, root: ctx._rootTriviaCapture, rootKinds: ctx._rootTriviaKindIndex, log: ctx._triviaLog, state: ctx.state }
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
gap.at = cur
|
|
70
|
+
gap.end = end
|
|
71
|
+
gap.input = input
|
|
72
|
+
gap.of = triviaP
|
|
73
|
+
gap.root = ctx._rootTriviaCapture
|
|
74
|
+
gap.rootKinds = ctx._rootTriviaKindIndex
|
|
75
|
+
gap.log = ctx._triviaLog
|
|
76
|
+
gap.state = ctx.state
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prime `spec` for the selection `rootKinds`: the first-character ranges of the
|
|
81
|
+
* arms it selects, and the scanner, which is `null` where one of those arms is
|
|
82
|
+
* unbounded or the trivia has no unlabelled scanner. `run()` builds one
|
|
83
|
+
* selection per parse, so this runs once per parse per trivia.
|
|
84
|
+
*/
|
|
85
|
+
function prime(spec: LabeledTriviaSpec, triviaP: Combinator<unknown>, rootKinds: Readonly<Record<string, number>>): void {
|
|
86
|
+
const ranges: CharRange[] = []
|
|
87
|
+
let bounded = true
|
|
88
|
+
for (const arm of spec.arms) {
|
|
89
|
+
if (rootKinds[arm.label] === undefined) continue
|
|
90
|
+
const fs = arm.parser._meta.firstSet
|
|
91
|
+
if (fs.kind === 'any') bounded = false
|
|
92
|
+
else if (fs.kind === 'ranges') ranges.push(...fs.ranges)
|
|
93
|
+
}
|
|
94
|
+
const classified = triviaP._meta.triviaScanner
|
|
95
|
+
spec.rootKinds = rootKinds
|
|
96
|
+
spec.rootLeaders = ranges
|
|
97
|
+
spec.rootScan = !bounded ? null : classified === undefined ? fastTriviaScanner(triviaP) : classified
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** No character in `[from, to)` falls in `ranges`. A surrogate is never clear:
|
|
101
|
+
* the ranges are code points, and a code unit cannot be checked against them. */
|
|
102
|
+
function clearOf(ranges: readonly CharRange[], input: string, from: number, to: number): boolean {
|
|
103
|
+
for (let i = from; i < to; i++) {
|
|
104
|
+
const c = input.charCodeAt(i)
|
|
105
|
+
if (c >= 0xD800 && c <= 0xDFFF) return false
|
|
106
|
+
for (const r of ranges) if (c >= r.lo && c <= r.hi) return false
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -398,6 +398,12 @@ export type ParseContext = {
|
|
|
398
398
|
* selected markers into the document-root capture.
|
|
399
399
|
*/
|
|
400
400
|
_rootTriviaCapture?: boolean | undefined
|
|
401
|
+
/**
|
|
402
|
+
* Framework-internal: the last deferred-commit trivia scan in this parse that
|
|
403
|
+
* recorded nothing (`table/trivia-gap.ts`). Held on the context so it is freed
|
|
404
|
+
* with the parse.
|
|
405
|
+
*/
|
|
406
|
+
_gap?: TriviaGapMemo | undefined
|
|
401
407
|
/**
|
|
402
408
|
* Framework-internal: flat per-node trivia log for CST capture mode.
|
|
403
409
|
* When set alongside _cstRawChildren, each trivia entry is recorded as three
|
|
@@ -606,3 +612,15 @@ export type CompiledParser<T> = {
|
|
|
606
612
|
/** Present only when compiled with `{ coverage: true }`. */
|
|
607
613
|
coverageDefinitions?: readonly import('./compiler/grammar-coverage-ids.ts').GrammarCoverageDefinition[]
|
|
608
614
|
}
|
|
615
|
+
|
|
616
|
+
/** A trivia gap scanned from `at` to `end` that recorded nothing, and the key it holds for. */
|
|
617
|
+
export interface TriviaGapMemo {
|
|
618
|
+
at: number
|
|
619
|
+
end: number
|
|
620
|
+
input: string
|
|
621
|
+
of: Combinator<unknown> | undefined
|
|
622
|
+
root: boolean | undefined
|
|
623
|
+
rootKinds: Readonly<Record<string, number>> | undefined
|
|
624
|
+
log: number[] | undefined
|
|
625
|
+
state: unknown
|
|
626
|
+
}
|
package/src/version.ts
CHANGED