parseman 0.46.0 → 0.47.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/README.md +8 -3
- package/dist/analysis/choice-cost-render.d.ts +12 -4
- package/dist/analysis/choice-cost-render.d.ts.map +1 -1
- package/dist/analysis/choice-cost.d.ts +2 -2
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/diagnostics.cjs +9929 -7465
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +9929 -7465
- package/dist/analysis/diagnostics.js.map +3 -4
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix.d.ts +3 -3
- package/dist/analysis/fix.d.ts.map +1 -1
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +39 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/analysis/terminal.d.ts +5 -2
- package/dist/analysis/terminal.d.ts.map +1 -1
- package/dist/cli/index.cjs +10865 -8535
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10865 -8535
- package/dist/cli/index.js.map +3 -4
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +40 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +47 -53
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +9224 -7203
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9219 -7198
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +311 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +311 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11146 -9593
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11146 -9593
- package/dist/plugin/index.js.map +3 -4
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2898 -1623
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1623
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11375 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11352 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +446 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +493 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +25 -2
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +322 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +221 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +821 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +2994 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1766 -0
- package/src/table/emit.ts +418 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1672 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +459 -0
- package/src/table/program.ts +855 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/module-hoist.d.ts +0 -106
- package/dist/compiler/module-hoist.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived tokenization, part 2: emit the SCANNER for an alphabet.
|
|
3
|
+
*
|
|
4
|
+
* Shape, and why each piece is what it is (all measured on the css grammar):
|
|
5
|
+
*
|
|
6
|
+
* - Literal and keyword terminals become a DATA TRIE walked by a fixed-size
|
|
7
|
+
* driver. The trie walk is also the macro-time SEARCH for a distinguishing
|
|
8
|
+
* position set: it walks until the node is uniquely accepting, so it does not
|
|
9
|
+
* depend on a fixed discriminator. A fixed one does not work — `(len,c1,c2)`
|
|
10
|
+
* collides on `@counter-style`/`@color-profile` and
|
|
11
|
+
* `@font-feature-values`/`@font-palette-values`, and widening to the last
|
|
12
|
+
* char still leaves 11 buckets for 13 keys.
|
|
13
|
+
* Emitting the trie as CODE (nested switches) was measured at 25,223 B for 13
|
|
14
|
+
* css at-keywords; as DATA it is 2,057 B for the same set and 1.4x the speed
|
|
15
|
+
* of today's slice-plus-char-chain rather than 2.5x. Data also means the
|
|
16
|
+
* driver's size is independent of how many keys the grammar has.
|
|
17
|
+
*
|
|
18
|
+
* - Regex terminals stay sticky regexes, consulted ONLY when the position's
|
|
19
|
+
* first char is in their first set. Ungated, a 42-regex core alphabet scans
|
|
20
|
+
* benchmark.css at 14.5 MB/s; first-char gated, 30.4 MB/s.
|
|
21
|
+
*
|
|
22
|
+
* - The candidate set consulted is LOCAL to the decision point (a packed table
|
|
23
|
+
* of id lists). The id space is global. Global maximal munch over the whole
|
|
24
|
+
* alphabet produces seven tokens for a 123 KB stylesheet.
|
|
25
|
+
*
|
|
26
|
+
* - `tight` is set AT SCAN TIME: 1 when no trivia preceded the token. This is
|
|
27
|
+
* what carries `noTrivia` adjacency (262 sites across the four dialects, 50 in
|
|
28
|
+
* css) rather than the hand-spelled `boundary` classes, which are not even
|
|
29
|
+
* self-consistent today: the 26 css `keywords(boundary:)` sites use THREE
|
|
30
|
+
* different spellings and 16 of them omit `-` and/or `\\`.
|
|
31
|
+
*
|
|
32
|
+
* - Whitespace is a token in `MODE_SELECTOR` (the descendant combinator is
|
|
33
|
+
* significant there) and skipped otherwise. Mode is part of the memo key.
|
|
34
|
+
*/
|
|
35
|
+
import type { Alphabet } from './token-alphabet.ts'
|
|
36
|
+
import { TOK_EOF, TOK_UNKNOWN, TOK_WS } from './token-alphabet.ts'
|
|
37
|
+
import { firstSetFromRegex } from '../regex/first-set.ts'
|
|
38
|
+
import { packInts, sharedHelperDecl } from './token-dispatch.ts'
|
|
39
|
+
import { foldCode, foldExpr } from './token-dispatch.ts'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* First CHARS a regex terminal can start with, or null when it is unbounded.
|
|
43
|
+
* Reuses the analyzer the interpreter's first-set gating already trusts — the
|
|
44
|
+
* gate must never be narrower than the real first set or a valid token is
|
|
45
|
+
* skipped, and `firstSetFromRegex` is the same over-approximation used there.
|
|
46
|
+
*/
|
|
47
|
+
function firstSetOfRegexSource(source: string, flags: string): Set<number> | null {
|
|
48
|
+
let fs
|
|
49
|
+
try { fs = firstSetFromRegex(source).firstSet } catch { return null }
|
|
50
|
+
if (fs.kind === 'any') return null
|
|
51
|
+
const out = new Set<number>()
|
|
52
|
+
if (fs.kind === 'empty') return out
|
|
53
|
+
for (const r of fs.ranges) {
|
|
54
|
+
// An unbounded tail (non-ASCII ranges are wide) collapses to the non-ASCII flag.
|
|
55
|
+
if (r.hi - r.lo > 512) return null
|
|
56
|
+
for (let c = r.lo; c <= r.hi; c++) out.add(c)
|
|
57
|
+
}
|
|
58
|
+
// Under `/i` the analyzer's ranges may name only one case; admit both, since a
|
|
59
|
+
// gate narrower than the real first set would skip a valid token.
|
|
60
|
+
if (flags.includes('i')) {
|
|
61
|
+
for (const c of [...out]) {
|
|
62
|
+
if (c >= 65 && c <= 90) out.add(c + 32)
|
|
63
|
+
else if (c >= 97 && c <= 122) out.add(c - 32)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type TrieNode = { edges: Map<number, number>; accept: number }
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the literal/keyword trie. Case-insensitive terminals fold ASCII LETTERS
|
|
73
|
+
* which is exact for ASCII letters and identity for everything else, so one
|
|
74
|
+
* folded walk serves both. A case-SENSITIVE terminal whose text contains an
|
|
75
|
+
* ASCII letter cannot share the folded walk and is left to the regex path.
|
|
76
|
+
*/
|
|
77
|
+
function buildTrie(alphabet: Alphabet): { nodes: TrieNode[]; covered: Set<number> } {
|
|
78
|
+
const nodes: TrieNode[] = [{ edges: new Map(), accept: 0 }]
|
|
79
|
+
const covered = new Set<number>()
|
|
80
|
+
const hasAsciiLetter = (s: string): boolean => /[a-zA-Z]/.test(s)
|
|
81
|
+
|
|
82
|
+
const insert = (text: string, id: number): boolean => {
|
|
83
|
+
if (text.length === 0) return false
|
|
84
|
+
let n = 0
|
|
85
|
+
for (let i = 0; i < text.length; i++) {
|
|
86
|
+
// LETTERS ONLY. `| 32` also folds non-letter ASCII pairs -- [ / {, ] / },
|
|
87
|
+
// ^ / ~, @ / ` -- so a case-insensitive scanner would collide on those,
|
|
88
|
+
// skip later terminals, and fire the EARLIER terminal's accept id for the
|
|
89
|
+
// later terminal's text. Same defect `e8612eb` fixed in dispatch; the one
|
|
90
|
+
// implementation is imported rather than written a third time.
|
|
91
|
+
const c = foldCode(text.charCodeAt(i))
|
|
92
|
+
let t = nodes[n]!.edges.get(c)
|
|
93
|
+
if (t === undefined) {
|
|
94
|
+
t = nodes.length
|
|
95
|
+
nodes.push({ edges: new Map(), accept: 0 })
|
|
96
|
+
nodes[n]!.edges.set(c, t)
|
|
97
|
+
}
|
|
98
|
+
n = t
|
|
99
|
+
}
|
|
100
|
+
// A collision means two terminals fold to the same text; keep the first.
|
|
101
|
+
if (nodes[n]!.accept !== 0) return false
|
|
102
|
+
nodes[n]!.accept = id
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const t of alphabet.terminals) {
|
|
107
|
+
if (t.kind === 'literal') {
|
|
108
|
+
if (!t.caseInsensitive && hasAsciiLetter(t.value)) continue
|
|
109
|
+
if (insert(t.value, t.id)) covered.add(t.id)
|
|
110
|
+
} else if (t.kind === 'keywords') {
|
|
111
|
+
// One id per SET: every word in the set accepts the same id, which is
|
|
112
|
+
// exactly what the dispatch needs (the set is the terminal).
|
|
113
|
+
let any = false
|
|
114
|
+
for (const w of t.words) {
|
|
115
|
+
if (!t.caseInsensitive && hasAsciiLetter(w)) continue
|
|
116
|
+
if (insert(w, t.id)) any = true
|
|
117
|
+
}
|
|
118
|
+
if (any) covered.add(t.id)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { nodes, covered }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type ScannerEmission = {
|
|
125
|
+
/** Module-scope declarations for the artifact prelude. */
|
|
126
|
+
decls: string[]
|
|
127
|
+
/** Name of the cursor entry point. */
|
|
128
|
+
nextFn: string
|
|
129
|
+
/** Register a candidate set, returning its table index. */
|
|
130
|
+
candidateIndexOf: (ids: readonly number[]) => number
|
|
131
|
+
/** True when the alphabet produced a usable scanner at all. */
|
|
132
|
+
usable: boolean
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Emit the scanner for `alphabet`. `ns` namespaces every hoisted name exactly
|
|
137
|
+
* like `_re`/`_fx`/`_pf` so two fused pieces never collide.
|
|
138
|
+
*/
|
|
139
|
+
export function emitScanner(alphabet: Alphabet, ns: string): ScannerEmission {
|
|
140
|
+
const { nodes, covered } = buildTrie(alphabet)
|
|
141
|
+
|
|
142
|
+
// Regex terminals, with their first-char gate. A terminal whose first set is
|
|
143
|
+
// empty can never start a token; one that is effectively unbounded is only
|
|
144
|
+
// consulted on the default arm.
|
|
145
|
+
const regexes: Array<{ id: number; src: string; flags: string; first: Set<number> | null }> = []
|
|
146
|
+
for (const t of alphabet.terminals) {
|
|
147
|
+
if (t.kind !== 'regex') continue
|
|
148
|
+
regexes.push({ id: t.id, src: t.source, flags: t.flags, first: firstSetOfRegexSource(t.source, t.flags) })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const usable = covered.size > 0 || regexes.length > 0
|
|
152
|
+
if (!usable) {
|
|
153
|
+
return { decls: [], nextFn: '', candidateIndexOf: () => -1, usable: false }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Flatten the trie.
|
|
157
|
+
const START: number[] = [0]
|
|
158
|
+
const CHAR: number[] = []
|
|
159
|
+
const NEXT: number[] = []
|
|
160
|
+
const ACCEPT: number[] = []
|
|
161
|
+
for (const nd of nodes) {
|
|
162
|
+
for (const [c, t] of [...nd.edges.entries()].sort((a, b) => a[0] - b[0])) { CHAR.push(c); NEXT.push(t) }
|
|
163
|
+
START.push(CHAR.length)
|
|
164
|
+
ACCEPT.push(nd.accept)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Candidate-set table, filled as decision points register.
|
|
168
|
+
const candOffsets: number[] = [0]
|
|
169
|
+
const candIds: number[] = []
|
|
170
|
+
const candKeyToIndex = new Map<string, number>()
|
|
171
|
+
const candidateIndexOf = (ids: readonly number[]): number => {
|
|
172
|
+
const sorted = [...new Set(ids)].sort((a, b) => a - b)
|
|
173
|
+
const key = sorted.join(',')
|
|
174
|
+
const hit = candKeyToIndex.get(key)
|
|
175
|
+
if (hit !== undefined) return hit
|
|
176
|
+
const idx = candOffsets.length - 1
|
|
177
|
+
for (const id of sorted) candIds.push(id)
|
|
178
|
+
candOffsets.push(candIds.length)
|
|
179
|
+
candKeyToIndex.set(key, idx)
|
|
180
|
+
return idx
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const p = (n: string): string => `${ns}${n}`
|
|
184
|
+
const decls: string[] = []
|
|
185
|
+
|
|
186
|
+
decls.push(`const ${p('_tkChar')} = ${JSON.stringify(CHAR.map(c => String.fromCharCode(c)).join(''))}`)
|
|
187
|
+
// The DECODER for `packInts`, taken from the one shared helper rather than
|
|
188
|
+
// re-spelled here. `packInts` itself was deduplicated after a second copy shipped
|
|
189
|
+
// without the 12-bit bound check; the matching decoder was left copied, so the
|
|
190
|
+
// two halves of one encoding still lived in two files. They agree today — the
|
|
191
|
+
// point is that widening the encoding (which the `RangeError` tells you to do)
|
|
192
|
+
// must not be able to change one half and miss the other.
|
|
193
|
+
decls.push(sharedHelperDecl('unpack', () => p('_tkUnpack')))
|
|
194
|
+
decls.push(`const ${p('_tkNext_')} = ${p('_tkUnpack')}(${packInts(NEXT)})`)
|
|
195
|
+
decls.push(`const ${p('_tkStart')} = ${p('_tkUnpack')}(${packInts(START)})`)
|
|
196
|
+
decls.push(`const ${p('_tkAccept')} = ${p('_tkUnpack')}(${packInts(ACCEPT)})`)
|
|
197
|
+
|
|
198
|
+
for (const [i, r] of regexes.entries()) {
|
|
199
|
+
const flags = r.flags.includes('y') ? r.flags : `${r.flags.replace(/[g]/g, '')}y`
|
|
200
|
+
decls.push(`const ${p(`_tkRx${i}`)} = /${r.src}/${flags}`)
|
|
201
|
+
}
|
|
202
|
+
decls.push(`const ${p('_tkRx')} = [${regexes.map((_, i) => p(`_tkRx${i}`)).join(', ')}]`)
|
|
203
|
+
decls.push(`const ${p('_tkRxId')} = new Int32Array([${regexes.map(r => r.id).join(', ')}])`)
|
|
204
|
+
|
|
205
|
+
// Per-regex first-char gate as a bitset over 0..127 plus an "also non-ASCII" flag.
|
|
206
|
+
const gateWords: number[] = []
|
|
207
|
+
const gateNonAscii: number[] = []
|
|
208
|
+
for (const r of regexes) {
|
|
209
|
+
const w = new Array<number>(4).fill(0)
|
|
210
|
+
let nonAscii = 0
|
|
211
|
+
if (r.first === null) { w.fill(-1); nonAscii = 1 } else {
|
|
212
|
+
for (const c of r.first) {
|
|
213
|
+
if (c >= 128) { nonAscii = 1; continue }
|
|
214
|
+
w[c >> 5] = (w[c >> 5]! | (1 << (c & 31))) | 0
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
gateWords.push(...w)
|
|
218
|
+
gateNonAscii.push(nonAscii)
|
|
219
|
+
}
|
|
220
|
+
decls.push(`const ${p('_tkGate')} = new Int32Array([${gateWords.join(', ')}])`)
|
|
221
|
+
decls.push(`const ${p('_tkGateHi')} = new Uint8Array([${gateNonAscii.join(', ')}])`)
|
|
222
|
+
|
|
223
|
+
// Candidate table is emitted lazily by the caller after all sites registered;
|
|
224
|
+
// placeholders are patched in `finalizeScanner`.
|
|
225
|
+
decls.push(`__TK_CAND_OFFSETS__`)
|
|
226
|
+
decls.push(`__TK_CAND_IDS__`)
|
|
227
|
+
|
|
228
|
+
const nextFn = p('_tkScan')
|
|
229
|
+
decls.push([
|
|
230
|
+
`let ${p('_tkId')} = ${TOK_EOF}, ${p('_tkStartPos')} = 0, ${p('_tkEnd')} = 0, ${p('_tkTight')} = 0`,
|
|
231
|
+
// The memo is module state, so INPUT IDENTITY is part of the key. Without it a
|
|
232
|
+
// second parse of a different string hits the first string's cached token at the
|
|
233
|
+
// same (pos, mode, set) and returns it — a wrong token from a warm cache, with no
|
|
234
|
+
// error. The reference check is O(1) on the common path: the same parse passes the
|
|
235
|
+
// same string object every call.
|
|
236
|
+
`let ${p('_tkMemoInput')} = null, ${p('_tkMemoPos')} = -1, ${p('_tkMemoMode')} = -1, ${p('_tkMemoSet')} = -1`,
|
|
237
|
+
`function ${nextFn}(input, pos, mode, set) {`,
|
|
238
|
+
` if (input === ${p('_tkMemoInput')} && pos === ${p('_tkMemoPos')} && mode === ${p('_tkMemoMode')} && set === ${p('_tkMemoSet')}) return ${p('_tkId')}`,
|
|
239
|
+
` ${p('_tkMemoInput')} = input; ${p('_tkMemoPos')} = pos; ${p('_tkMemoMode')} = mode; ${p('_tkMemoSet')} = set`,
|
|
240
|
+
` const n = input.length`,
|
|
241
|
+
` let q = pos`,
|
|
242
|
+
// Comments skip in both modes; whitespace only outside selector mode.
|
|
243
|
+
` for (;;) {`,
|
|
244
|
+
` const before = q`,
|
|
245
|
+
` if (mode !== 1) { while (q < n) { const c = input.charCodeAt(q); if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) q++; else break } }`,
|
|
246
|
+
` if (q + 1 < n && input.charCodeAt(q) === 47 && input.charCodeAt(q + 1) === 42) { const e = input.indexOf('*/', q + 2); q = e < 0 ? n : e + 2 }`,
|
|
247
|
+
` if (q === before) break`,
|
|
248
|
+
` }`,
|
|
249
|
+
` ${p('_tkTight')} = q === pos ? 1 : 0`,
|
|
250
|
+
` ${p('_tkStartPos')} = q`,
|
|
251
|
+
` if (mode === 1) {`,
|
|
252
|
+
` let w = q`,
|
|
253
|
+
` while (w < n) { const c = input.charCodeAt(w); if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) w++; else break }`,
|
|
254
|
+
` if (w > q) { ${p('_tkEnd')} = w; ${p('_tkId')} = ${TOK_WS}; return ${TOK_WS} }`,
|
|
255
|
+
` }`,
|
|
256
|
+
` if (q >= n) { ${p('_tkEnd')} = q; ${p('_tkId')} = ${TOK_EOF}; return ${TOK_EOF} }`,
|
|
257
|
+
` const lo = ${p('_tkCandOff')}[set], hi = ${p('_tkCandOff')}[set + 1]`,
|
|
258
|
+
` let bestId = ${TOK_UNKNOWN}, bestEnd = q`,
|
|
259
|
+
// Trie walk: longest accepting prefix wins, which is maximal munch over the
|
|
260
|
+
// literal/keyword part of the candidate set.
|
|
261
|
+
` {`,
|
|
262
|
+
` let node = 0, i = q`,
|
|
263
|
+
` while (i < n) {`,
|
|
264
|
+
// The emitted walker must fold exactly as the trie above did, or a lookup
|
|
265
|
+
// misses the node it built.
|
|
266
|
+
` const c = ${foldExpr(`input.charCodeAt(i)`)}`,
|
|
267
|
+
` let nx = -1`,
|
|
268
|
+
` for (let e = ${p('_tkStart')}[node], z = ${p('_tkStart')}[node + 1]; e < z; e++) if (${p('_tkChar')}.charCodeAt(e) === c) { nx = ${p('_tkNext_')}[e]; break }`,
|
|
269
|
+
` if (nx < 0) break`,
|
|
270
|
+
` node = nx; i++`,
|
|
271
|
+
` const acc = ${p('_tkAccept')}[node]`,
|
|
272
|
+
` if (acc !== 0 && i > bestEnd) { for (let k = lo; k < hi; k++) if (${p('_tkCandIds')}[k] === acc) { bestId = acc; bestEnd = i; break } }`,
|
|
273
|
+
` }`,
|
|
274
|
+
` }`,
|
|
275
|
+
// Regex candidates, first-char gated.
|
|
276
|
+
` {`,
|
|
277
|
+
` const c0 = input.charCodeAt(q)`,
|
|
278
|
+
` const wi = c0 < 128 ? (c0 >> 5) : -1, bit = c0 < 128 ? (1 << (c0 & 31)) : 0`,
|
|
279
|
+
` for (let k = lo; k < hi; k++) {`,
|
|
280
|
+
` const want = ${p('_tkCandIds')}[k]`,
|
|
281
|
+
` for (let r = 0; r < ${p('_tkRxId')}.length; r++) {`,
|
|
282
|
+
` if (${p('_tkRxId')}[r] !== want) continue`,
|
|
283
|
+
` if (wi < 0 ? !${p('_tkGateHi')}[r] : !(${p('_tkGate')}[r * 4 + wi] & bit)) break`,
|
|
284
|
+
` const rx = ${p('_tkRx')}[r]`,
|
|
285
|
+
` rx.lastIndex = q`,
|
|
286
|
+
` const m = rx.exec(input)`,
|
|
287
|
+
` if (m !== null && m[0].length > 0 && q + m[0].length > bestEnd) { bestId = want; bestEnd = q + m[0].length }`,
|
|
288
|
+
` break`,
|
|
289
|
+
` }`,
|
|
290
|
+
` }`,
|
|
291
|
+
` }`,
|
|
292
|
+
` ${p('_tkEnd')} = bestEnd > q ? bestEnd : q`,
|
|
293
|
+
` ${p('_tkId')} = bestId`,
|
|
294
|
+
` return bestId`,
|
|
295
|
+
`}`,
|
|
296
|
+
].join('\n'))
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
decls,
|
|
300
|
+
nextFn,
|
|
301
|
+
candidateIndexOf,
|
|
302
|
+
usable: true,
|
|
303
|
+
// Consumed by `finalizeScanner`.
|
|
304
|
+
...({ _cand: { offsets: candOffsets, ids: candIds, ns } } as object),
|
|
305
|
+
} as ScannerEmission
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Patch the candidate table placeholders once every decision point registered. */
|
|
309
|
+
export function finalizeScanner(emission: ScannerEmission): string[] {
|
|
310
|
+
const cand = (emission as unknown as { _cand?: { offsets: number[]; ids: number[]; ns: string } })._cand
|
|
311
|
+
if (cand === undefined) return emission.decls
|
|
312
|
+
const p = (n: string): string => `${cand.ns}${n}`
|
|
313
|
+
return emission.decls.map(d => {
|
|
314
|
+
if (d === '__TK_CAND_OFFSETS__') return `const ${p('_tkCandOff')} = new Int32Array([${cand.offsets.join(', ')}])`
|
|
315
|
+
if (d === '__TK_CAND_IDS__') return `const ${p('_tkCandIds')} = new Int32Array([${cand.ids.join(', ')}])`
|
|
316
|
+
return d
|
|
317
|
+
})
|
|
318
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Combinator, ParserDef } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dead-value analysis (shared by the interpreter and codegen). A container
|
|
5
|
+
* combinator — `many` / `oneOrMore` / `optional` / `sequence` — builds an
|
|
6
|
+
* aggregate value (array / tuple / nullable). Under a `node()`, the node builds
|
|
7
|
+
* from the *captured children*, never from that aggregate, so the aggregate is
|
|
8
|
+
* built and thrown away. This pass marks those containers `valueUnused`; the
|
|
9
|
+
* interpreter and the emitter then skip building the aggregate entirely (the
|
|
10
|
+
* elements still parse and self-capture, so trees are identical).
|
|
11
|
+
*
|
|
12
|
+
* Walks ONE combinator tree (a single rule body, or a standalone root), stopping
|
|
13
|
+
* at `lazy`/ref boundaries — other rules are analyzed on their own root. Starts
|
|
14
|
+
* `consumed = true` (a rule's own value is assumed read — conservative); `node()`
|
|
15
|
+
* flips it to false for its inner parser; `transform` forces it true (its map fn
|
|
16
|
+
* reads the value). Sharing-safe: any `consumed = true` visit wins and is sticky,
|
|
17
|
+
* so a container reachable from BOTH a consuming and a non-consuming site keeps
|
|
18
|
+
* its aggregate (never wrongly elided).
|
|
19
|
+
*
|
|
20
|
+
* Soundness: a container is marked ONLY when every path to it passes through a
|
|
21
|
+
* `node()` (value-discarding) without an intervening value-reader — i.e. its
|
|
22
|
+
* aggregate provably isn't observed. Idempotent; safe to re-run.
|
|
23
|
+
*/
|
|
24
|
+
export function markUnusedValues(root: Combinator<unknown>): void {
|
|
25
|
+
const seenConsumed = new WeakSet<Combinator<unknown>>()
|
|
26
|
+
const seenUnconsumed = new WeakSet<Combinator<unknown>>()
|
|
27
|
+
|
|
28
|
+
const mark = (def: { valueUnused?: boolean }, consumed: boolean): void => {
|
|
29
|
+
if (consumed) def.valueUnused = false
|
|
30
|
+
else if (def.valueUnused !== false) def.valueUnused = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const visit = (c: Combinator<unknown>, consumed: boolean): void => {
|
|
34
|
+
const seen = consumed ? seenConsumed : seenUnconsumed
|
|
35
|
+
if (seen.has(c)) return
|
|
36
|
+
seen.add(c)
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
38
|
+
const def = c._def as ParserDef & { valueUnused?: boolean }
|
|
39
|
+
|
|
40
|
+
switch (def.tag) {
|
|
41
|
+
case 'many':
|
|
42
|
+
case 'oneOrMore':
|
|
43
|
+
mark(def, consumed)
|
|
44
|
+
visit(def.parser, consumed)
|
|
45
|
+
return
|
|
46
|
+
case 'optional':
|
|
47
|
+
// `optional` returns `inner | null` — no aggregate array/tuple to elide, so
|
|
48
|
+
// it carries no valueUnused flag. Still descend so an inner many/sequence
|
|
49
|
+
// under a node() is analyzed (it benefits even when its optional wrapper
|
|
50
|
+
// doesn't).
|
|
51
|
+
visit(def.parser, consumed)
|
|
52
|
+
return
|
|
53
|
+
case 'sequence':
|
|
54
|
+
mark(def, consumed)
|
|
55
|
+
for (const p of def.parsers) visit(p, consumed)
|
|
56
|
+
return
|
|
57
|
+
case 'choice':
|
|
58
|
+
// value is the winning arm's value — propagate consumed to each arm.
|
|
59
|
+
for (const p of def.parsers) visit(p, consumed)
|
|
60
|
+
return
|
|
61
|
+
case 'dispatch':
|
|
62
|
+
visit(def.selector, consumed)
|
|
63
|
+
for (const entry of def.cases) visit(entry.parser, consumed)
|
|
64
|
+
if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed)
|
|
65
|
+
if (def.otherwise) visit(def.otherwise, consumed)
|
|
66
|
+
return
|
|
67
|
+
case 'node':
|
|
68
|
+
// builds from captured children, NOT the inner parser value.
|
|
69
|
+
visit(def.parser, false)
|
|
70
|
+
return
|
|
71
|
+
case 'transform':
|
|
72
|
+
// the map fn reads the value → always consumed.
|
|
73
|
+
visit(def.parser, true)
|
|
74
|
+
return
|
|
75
|
+
case 'field':
|
|
76
|
+
// the field capture records the parsed value → always consumed.
|
|
77
|
+
visit(def.parser, true)
|
|
78
|
+
return
|
|
79
|
+
case 'sepBy':
|
|
80
|
+
// sepBy is NOT elided (no valueUnused), so it always builds its array and
|
|
81
|
+
// therefore always reads each item's value — regardless of whether sepBy's
|
|
82
|
+
// OWN value is observed. Item parser is unconditionally consumed.
|
|
83
|
+
visit(def.parser, true)
|
|
84
|
+
visit(def.separator, false)
|
|
85
|
+
return
|
|
86
|
+
case 'not':
|
|
87
|
+
case 'peek':
|
|
88
|
+
visit(def.parser, false) // lookahead — value never observed
|
|
89
|
+
return
|
|
90
|
+
case 'grammar':
|
|
91
|
+
visit(def.parser, consumed)
|
|
92
|
+
if (def.triviaParser) visit(def.triviaParser, false)
|
|
93
|
+
return
|
|
94
|
+
case 'recover':
|
|
95
|
+
visit(def.parser, consumed)
|
|
96
|
+
visit(def.sentinel, false)
|
|
97
|
+
return
|
|
98
|
+
case 'token':
|
|
99
|
+
// token() returns the matched source slice, not the wrapped parser value.
|
|
100
|
+
// Its child still parses and captures one outer token leaf; inner aggregate
|
|
101
|
+
// values such as sequence tuples are unobserved and can be elided.
|
|
102
|
+
visit(def.parser, false)
|
|
103
|
+
return
|
|
104
|
+
case 'label':
|
|
105
|
+
case 'trivia':
|
|
106
|
+
case 'attempt':
|
|
107
|
+
case 'withCtx':
|
|
108
|
+
case 'expect':
|
|
109
|
+
visit(def.parser, consumed) // single-child pass-through wrappers
|
|
110
|
+
return
|
|
111
|
+
case 'leaf':
|
|
112
|
+
// The reducer observes the complete inner value even when its own result
|
|
113
|
+
// is immediately captured by a node().
|
|
114
|
+
visit(def.parser, true)
|
|
115
|
+
return
|
|
116
|
+
case 'lazy':
|
|
117
|
+
// An INNER ref boundary — a distinct rule, analyzed on its own root. Don't
|
|
118
|
+
// descend (and never call thunk(): an external ref would throw). A ref at
|
|
119
|
+
// the ROOT of a rule is different — it wraps this rule's own body — and is
|
|
120
|
+
// resolved by resolveRoot() before the walk starts.
|
|
121
|
+
return
|
|
122
|
+
default:
|
|
123
|
+
// literal / regex / keywords / guard / scanTo / unknown — no container child.
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A rule's root is often a `ref()`/`lazy` wrapping the real body — that is how
|
|
129
|
+
// rules() and compose() store forward-referenced and composed rules. Walking
|
|
130
|
+
// that lazy directly hits the boundary case and marks nothing, so resolve a
|
|
131
|
+
// ROOT lazy to its defined body first. Inner lazies (references to OTHER rules)
|
|
132
|
+
// stay boundaries — each rule is analyzed from its own resolved root.
|
|
133
|
+
const resolveRoot = (c: Combinator<unknown>): Combinator<unknown> => {
|
|
134
|
+
const guard = new Set<Combinator<unknown>>()
|
|
135
|
+
let cur = c
|
|
136
|
+
while (!guard.has(cur)) {
|
|
137
|
+
const d = cur._def as ParserDef
|
|
138
|
+
if (d.tag === 'grammar') { guard.add(cur); cur = d.parser; continue }
|
|
139
|
+
if (d.tag !== 'lazy') return cur
|
|
140
|
+
guard.add(cur)
|
|
141
|
+
try { cur = d.thunk() } catch { return c } // undefined ref → leave as-is
|
|
142
|
+
}
|
|
143
|
+
return cur
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
visit(resolveRoot(root), true)
|
|
147
|
+
}
|