parseman 0.58.3 → 0.59.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.
Files changed (61) hide show
  1. package/dist/analysis/diagnostics.js +141 -93
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +143 -94
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/combinators/field-trivia.d.ts.map +1 -1
  7. package/dist/combinators/keywords.d.ts.map +1 -1
  8. package/dist/combinators/literal.d.ts.map +1 -1
  9. package/dist/combinators/node.d.ts.map +1 -1
  10. package/dist/combinators/regex.d.ts.map +1 -1
  11. package/dist/combinators/scanTo.d.ts.map +1 -1
  12. package/dist/combinators/token.d.ts.map +1 -1
  13. package/dist/compiler/linker.d.ts.map +1 -1
  14. package/dist/compiler/token-capability.js.map +1 -1
  15. package/dist/cst/capture-buffer.d.ts.map +1 -1
  16. package/dist/cst/plain.js +69 -0
  17. package/dist/cst/plain.js.map +6 -0
  18. package/dist/index.js +144 -98
  19. package/dist/index.js.map +3 -3
  20. package/dist/language-service/index.js +19 -2
  21. package/dist/language-service/index.js.map +2 -2
  22. package/dist/line-index.d.ts +13 -0
  23. package/dist/line-index.d.ts.map +1 -1
  24. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  25. package/dist/plugin/index.js +146 -100
  26. package/dist/plugin/index.js.map +3 -3
  27. package/dist/recovery/scan.d.ts.map +1 -1
  28. package/dist/run/index.js +4 -1
  29. package/dist/run/index.js.map +2 -2
  30. package/dist/spec/index.js +18 -1
  31. package/dist/spec/index.js.map +2 -2
  32. package/dist/table/assemble.d.ts.map +1 -1
  33. package/dist/table/emit-assembly.d.ts +1 -1
  34. package/dist/table/emit-assembly.d.ts.map +1 -1
  35. package/dist/table/emit.d.ts +6 -0
  36. package/dist/table/emit.d.ts.map +1 -1
  37. package/dist/table/index.js +124 -89
  38. package/dist/table/index.js.map +3 -3
  39. package/dist/table/run-support.d.ts.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/package.json +2 -1
  42. package/src/combinators/dispatch.ts +2 -1
  43. package/src/combinators/field-trivia.ts +6 -5
  44. package/src/combinators/keywords.ts +3 -2
  45. package/src/combinators/literal.ts +5 -4
  46. package/src/combinators/node.ts +3 -1
  47. package/src/combinators/regex.ts +5 -4
  48. package/src/combinators/scanTo.ts +2 -1
  49. package/src/combinators/token.ts +3 -6
  50. package/src/compiler/linker.ts +4 -2
  51. package/src/cst/capture-buffer.ts +13 -5
  52. package/src/cst/plain.d.ts +35 -0
  53. package/src/cst/plain.js +74 -0
  54. package/src/line-index.ts +37 -0
  55. package/src/plugin/entry-supercompiler.ts +3 -2
  56. package/src/recovery/scan.ts +2 -1
  57. package/src/table/assemble.ts +33 -35
  58. package/src/table/emit-assembly.ts +27 -24
  59. package/src/table/emit.ts +9 -1
  60. package/src/table/run-support.ts +4 -7
  61. package/src/version.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"run-support.d.ts","sourceRoot":"","sources":["../../src/table/run-support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG/C,MAAM,MAAM,IAAI,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AACjD,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,2EAA2E;AAC3E,wBAAgB,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAS3E;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAGjF;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAUjF;AAED,yEAAyE;AACzE,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAKvD;AAKD,KAAK,aAAa,GAAG,CACnB,QAAQ,EAAE,SAAS,OAAO,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACrF,WAAW,EAAE,SAAS,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,KAC5E,OAAO,CAAA;AAEZ;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAClG,OAAO,CAST"}
1
+ {"version":3,"file":"run-support.d.ts","sourceRoot":"","sources":["../../src/table/run-support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAI/C,MAAM,MAAM,IAAI,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AACjD,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,2EAA2E;AAC3E,wBAAgB,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAS3E;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAGjF;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAMjF;AAED,yEAAyE;AACzE,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAKvD;AAKD,KAAK,aAAa,GAAG,CACnB,QAAQ,EAAE,SAAS,OAAO,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACrF,WAAW,EAAE,SAAS,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,KAC5E,OAAO,CAAA;AAEZ;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAClG,OAAO,CAST"}
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.58.3";
16
+ export declare const PARSEMAN_VERSION = "0.59.0";
17
17
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parseman",
3
- "version": "0.58.3",
3
+ "version": "0.59.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "engines": {
6
6
  "node": "^20.19.0 || >=22.13.0"
@@ -96,6 +96,7 @@
96
96
  "perf:guard:grammars": "node --import tsx/esm bench/grammar-perf-guard.ts",
97
97
  "perf:xproc": "node --import tsx/esm bench/xproc-ab.ts",
98
98
  "perf:xproc:gate": "node --import tsx/esm bench/xproc-gate.ts",
99
+ "perf:pretenure": "node --import tsx/esm bench/pretenure-check.ts",
99
100
  "prepare": "git config core.hooksPath scripts/git-hooks || true; node scripts/build.mjs",
100
101
  "prepublishOnly": "node scripts/check-changelog.mjs --publish",
101
102
  "check:changelog": "node scripts/check-changelog.mjs",
@@ -1,6 +1,7 @@
1
1
  import type { Combinator, DispatchCase, DispatchMatcherCase, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
2
  import { cstCaptureActive, pushCstLeaf } from '../cst/capture-buffer.ts'
3
3
  import { rollbackTrivia, saveTriviaMark } from './trivia-skip.ts'
4
+ import { PlainLeaf } from '../cst/plain.js'
4
5
 
5
6
  export type DispatchWhen<T> = {
6
7
  readonly kind: 'when'
@@ -240,7 +241,7 @@ export function routed<T = string>(fallback?: Combinator<T>): Combinator<T> {
240
241
  return { ok: false, expected: ['routed()'], span: { start: pos, end: pos } }
241
242
  }
242
243
  if (cstCaptureActive(ctx)) {
243
- pushCstLeaf(ctx, { _tag: 'leaf', value: item.value, span: item.span })
244
+ pushCstLeaf(ctx, new PlainLeaf(item.value, item.span))
244
245
  }
245
246
  return { ok: true, value: item.value as T, span: item.span }
246
247
  },
@@ -1,4 +1,5 @@
1
1
  import type { Combinator } from '../types.ts'
2
+ import { PlainSpan, PlainTriviaField } from '../cst/plain.js'
2
3
 
3
4
  /**
4
5
  * Where the trivia before a `field(…, { leadingTrivia: true })` began.
@@ -29,13 +30,13 @@ export const FIELD_TRAILING_TRIVIA = 2
29
30
  export function triviaFieldCapture(
30
31
  name: string, value: unknown, input: string, start: number, end: number, leadFrom: number, trailEnd: number,
31
32
  ): { name: string; value: unknown; span: { start: number; end: number }; leading: string; trailing: string } {
32
- return {
33
+ return new PlainTriviaField(
33
34
  name,
34
35
  value,
35
- span: { start, end },
36
- leading: leadFrom >= 0 && leadFrom < start ? input.slice(leadFrom, start) : '',
37
- trailing: trailEnd > end ? input.slice(end, trailEnd) : '',
38
- }
36
+ new PlainSpan(start, end),
37
+ leadFrom >= 0 && leadFrom < start ? input.slice(leadFrom, start) : '',
38
+ trailEnd > end ? input.slice(end, trailEnd) : '',
39
+ )
39
40
  }
40
41
 
41
42
  /** Is `p` a `field(…, { leadingTrivia: true })` — the term itself, not wrapped? */
@@ -5,6 +5,7 @@ import { failAt } from './probe.ts'
5
5
  import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
6
6
  import { directTerminalFailureExpected } from './expected.ts'
7
7
  import type { ScalarParser } from './scalar.ts'
8
+ import { PlainLeaf, PlainSpan } from '../cst/plain.js'
8
9
 
9
10
  export type KeywordsOptions = {
10
11
  /** Match case-insensitively. */
@@ -164,8 +165,8 @@ export function keywords(words: readonly string[], opts: KeywordsOptions = {}):
164
165
  const m = re.exec(input)
165
166
  if (m === null) return failAt(ctx, directTerminalFailureExpected(def), pos)
166
167
  const value = m[0]!
167
- const span = { start: pos, end: pos + value.length }
168
- if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span })
168
+ const span = new PlainSpan(pos, pos + value.length)
169
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, new PlainLeaf(value, span))
169
170
  return { ok: true, value, span }
170
171
  },
171
172
  }
@@ -5,6 +5,7 @@ import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
5
5
  import { recordLineRangeFromContext } from '../line-index.ts'
6
6
  import { directTerminalFailureExpected } from './expected.ts'
7
7
  import type { ScalarParser } from './scalar.ts'
8
+ import { PlainLeaf, PlainSpan } from '../cst/plain.js'
8
9
 
9
10
  /**
10
11
  * ASCII case-fold equality — the interpreter twin of the compiler's `foldEq`
@@ -96,9 +97,9 @@ export function literal(value: string, opts: LiteralOptions = {}): Combinator<st
96
97
  const code = value.charCodeAt(0)
97
98
  return function parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
98
99
  if (input.charCodeAt(pos) === code) {
99
- const span = { start: pos, end: pos + 1 }
100
+ const span = new PlainSpan(pos, pos + 1)
100
101
  if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, span.end)
101
- if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span })
102
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, new PlainLeaf(value, span))
102
103
  return { ok: true, value, span }
103
104
  }
104
105
  return failAt(ctx, expected, pos)
@@ -111,9 +112,9 @@ export function literal(value: string, opts: LiteralOptions = {}): Combinator<st
111
112
  }
112
113
  const matchedValue = caseInsensitive ? input.slice(pos, end) : value
113
114
  if (caseInsensitive ? asciiFoldEq(matchedValue, value) : input.startsWith(value, pos)) {
114
- const span = { start: pos, end }
115
+ const span = new PlainSpan(pos, end)
115
116
  if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, end)
116
- if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: matchedValue, span })
117
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, new PlainLeaf(matchedValue, span))
117
118
  return { ok: true, value: matchedValue, span }
118
119
  }
119
120
  return failAt(ctx, expected, pos)
@@ -7,6 +7,8 @@ import { matchesEmpty, startsFirstSet } from './first-set.ts'
7
7
  import { deriveExpected } from './expect.ts'
8
8
  import { annotateSpanFromLineContext } from '../line-index.ts'
9
9
  import { NODE_TAG, NODE_TYPE } from '../cst/reflection-symbols.ts'
10
+ import { PlainNode, PlainSpan } from '../cst/plain.js'
11
+ import { copySpan } from '../line-index.ts'
10
12
 
11
13
  /**
12
14
  * A CST/AST node rule. Runs `combinator` while collecting its terminals into
@@ -331,7 +333,7 @@ export function node<N>(
331
333
  // Structural node: a `ctx.build` host if present, else a default CST.
332
334
  : ctx.build
333
335
  ? ctx.build(nodeType, children, fields, span, rawChildren, triviaLog, st, tags)
334
- : { _tag: 'node', type: nodeType, span: ctx.trackLines ? { ...span } : { start: r.span.start, end: r.span.end }, state: st ?? null, children }
336
+ : new PlainNode(nodeType, ctx.trackLines ? copySpan(span) : new PlainSpan(r.span.start, r.span.end), st ?? null, [...children])
335
337
  const rawEntry = isCstChild(built)
336
338
  ? built
337
339
  // A direct semantic object is opaque to the raw CST, but its source is
@@ -7,6 +7,7 @@ import { shorthandRanges, parseClassRanges } from '../regex/classes.ts'
7
7
  import { firstSetFromRegex } from '../regex/first-set.ts'
8
8
  import { directTerminalFailureExpected } from './expected.ts'
9
9
  import type { ScalarParser } from './scalar.ts'
10
+ import { PlainLeaf, PlainSpan } from '../cst/plain.js'
10
11
 
11
12
  /**
12
13
  * A regex terminal's first-set (for choice-dispatch fast paths) is derived by
@@ -195,9 +196,9 @@ export function regex(pattern: string | RegExp, flags = ''): Combinator<string>
195
196
  if (scanEnd !== undefined) {
196
197
  if (scanEnd === null) return failAt(ctx, directTerminalFailureExpected(def), pos)
197
198
  const value = input.slice(pos, scanEnd)
198
- const span = { start: pos, end: scanEnd }
199
+ const span = new PlainSpan(pos, scanEnd)
199
200
  if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, scanEnd)
200
- if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span })
201
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, new PlainLeaf(value, span))
201
202
  return { ok: true, value, span }
202
203
  }
203
204
  anchored.lastIndex = pos
@@ -205,9 +206,9 @@ export function regex(pattern: string | RegExp, flags = ''): Combinator<string>
205
206
  if (m === null) {
206
207
  return failAt(ctx, directTerminalFailureExpected(def), pos)
207
208
  }
208
- const span = { start: pos, end: pos + m[0]!.length }
209
+ const span = new PlainSpan(pos, pos + m[0]!.length)
209
210
  if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, span.end)
210
- if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: m[0]!, span })
211
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, new PlainLeaf(m[0]!, span))
211
212
  return { ok: true, value: m[0]!, span }
212
213
  },
213
214
  }
@@ -12,6 +12,7 @@ import { ref } from './ref.ts'
12
12
  import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
13
13
  import { token } from './token.ts'
14
14
  import { recordLineRangeFromContext } from '../line-index.ts'
15
+ import { PlainLeaf, PlainSpan } from '../cst/plain.js'
15
16
 
16
17
  export type ScanToOptions = {
17
18
  /**
@@ -154,7 +155,7 @@ export function scanTo(
154
155
  // returned value carries it). Skipped when no collector is active.
155
156
  const emit = (end: number) => {
156
157
  if (end > pos && cstCaptureActive(ctx)) {
157
- const leaf = { _tag: 'leaf', value: input.slice(pos, end), span: { start: pos, end } }
158
+ const leaf = new PlainLeaf(input.slice(pos, end), new PlainSpan(pos, end))
158
159
  pushCstLeaf(ctx, leaf)
159
160
  }
160
161
  }
@@ -1,6 +1,7 @@
1
1
  import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
2
  import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
3
3
  import { cstOutputHost } from '../compiler/build-arity.ts'
4
+ import { PlainLeaf } from '../cst/plain.js'
4
5
 
5
6
  type LeafCstValue<U> = (ctx: ParseContext, input: string, start: number, end: number, value: U) => unknown
6
7
 
@@ -70,7 +71,7 @@ export function token(root: Combinator<unknown>): Combinator<string> {
70
71
 
71
72
  const span = result.span
72
73
  const value = input.slice(pos, span.end)
73
- if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value, span })
74
+ if (wasCapturing) pushCstLeaf(ctx, new PlainLeaf(value, span))
74
75
  return { ok: true, value, span }
75
76
  },
76
77
  }
@@ -133,11 +134,7 @@ function makeLeaf<T, U>(
133
134
  }
134
135
  if (!result.ok) return result
135
136
  const value = fn(result.value, result.span)
136
- if (wasCapturing) pushCstLeaf(ctx, {
137
- _tag: 'leaf',
138
- value: cstValue(ctx, input, pos, result.span.end, value),
139
- span: result.span,
140
- })
137
+ if (wasCapturing) pushCstLeaf(ctx, new PlainLeaf(cstValue(ctx, input, pos, result.span.end, value), result.span))
141
138
  return { ok: true, value, span: result.span }
142
139
  },
143
140
  }
@@ -19,6 +19,8 @@ import { GRAMMAR_REFLECTION } from '../cst/reflection.ts'
19
19
  import { linkRules, RULE_ORDER, RULES_RECIPE, type LinkOptions, type RulesRecipe } from '../combinators/parser.ts'
20
20
  import type { BuildHost, Combinator, CstCollapsePredicate, ParseContext, ParseResult } from '../types.ts'
21
21
  import type { Runnable } from '../functional/run.ts'
22
+ import { PlainNode, PlainTaggedNode } from '../cst/plain.js'
23
+ import { copySpan } from '../line-index.ts'
22
24
 
23
25
  /**
24
26
  * Compile a `rules()` map to a **linkable artifact** — the composable, shippable
@@ -92,8 +94,8 @@ function buildCstNode(
92
94
  // Carry the grammar's `ctx.state` snapshot onto the node (null when unset) — the
93
95
  // CST contract includes `state` and incremental re-parse replays it on edit.
94
96
  return tags !== undefined && tags.length > 0
95
- ? { _tag: 'node', type, tags, span: { ...span }, state: state ?? null, children: [...children] }
96
- : { _tag: 'node', type, span: { ...span }, state: state ?? null, children: [...children] }
97
+ ? new PlainTaggedNode(type, tags, copySpan(span), state ?? null, [...children])
98
+ : new PlainNode(type, copySpan(span), state ?? null, [...children])
97
99
  }
98
100
 
99
101
  /**
@@ -1,5 +1,6 @@
1
1
  import type { ParseContext, Span } from '../types.ts'
2
- import { annotateSpanFromLineContext } from '../line-index.ts'
2
+ import { annotateSpanFromLineContext, lineSpanFromContext } from '../line-index.ts'
3
+ import { PlainBuf, PlainLeaf } from './plain.js'
3
4
 
4
5
  /** Lazy per-node capture state — arrays materialized on first push. */
5
6
  export type CstCaptureBuf = {
@@ -42,7 +43,7 @@ export function beginCstNodeCapture(ctx: ParseContext): CstNodeCaptureSaved {
42
43
  cap: ctx.captureTrivia,
43
44
  buf: ctx._cstBuf,
44
45
  }
45
- ctx._cstBuf = {}
46
+ ctx._cstBuf = new PlainBuf(undefined, undefined, 0)
46
47
  ctx._cstChildren = undefined
47
48
  ctx._cstLeaves = undefined
48
49
  ctx._cstRawChildren = undefined
@@ -79,14 +80,21 @@ export function endCstNodeCapture(
79
80
  return materialized
80
81
  }
81
82
 
83
+ /** Every caller passes a leaf `{ _tag, value, span }` (parseman's own terminals; this is not public API). */
84
+ function isLeaf(v: unknown): v is { _tag: 'leaf'; value: unknown; span: Span } {
85
+ return typeof v === 'object' && v !== null && (v as { _tag?: unknown })._tag === 'leaf'
86
+ }
87
+
82
88
  export function pushCstLeaf(ctx: ParseContext, leaf: unknown): void {
83
89
  if (!ctx.trackLines) {
84
90
  pushCstChild(ctx, leaf, leaf)
85
91
  return
86
92
  }
87
- const annotated = leaf && typeof leaf === 'object' && (leaf as { span?: unknown }).span
88
- ? { ...(leaf as Record<string, unknown>), span: annotateSpanFromLineContext((leaf as { span: Span }).span, ctx) }
89
- : leaf
93
+ const annotated = isLeaf(leaf)
94
+ ? new PlainLeaf(leaf.value, lineSpanFromContext(leaf.span, ctx))
95
+ : leaf && typeof leaf === 'object' && (leaf as { span?: unknown }).span
96
+ ? { ...(leaf as Record<string, unknown>), span: annotateSpanFromLineContext((leaf as { span: Span }).span, ctx) }
97
+ : leaf
90
98
  pushCstChild(ctx, annotated, annotated)
91
99
  }
92
100
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The objects a parse keeps until it ends — spans, leaves, default nodes, field
3
+ * captures, capture buffers — built by `new` calls instead of object literals.
4
+ *
5
+ * WHY. V8 keeps an allocation site per object literal and decides, per site, whether to
6
+ * allocate straight into old space (pretenuring). The decision is taken during warm-up
7
+ * from where scavenges happen to fall, so identical processes split into a fast and a
8
+ * slow mode, and any change in bundle layout re-rolls it: on Node 26 a pretenured
9
+ * process ran 34–61 mark-compacts per run where a fast one ran 1. A `new` call has no
10
+ * allocation site, so these objects are never pretenured. Each constructor's
11
+ * `prototype` is `Object.prototype`, so what it builds is a plain object: same
12
+ * prototype, same `constructor`, same keys in the same order as the literal it replaces.
13
+ *
14
+ * ONE MODULE. Every constructor defines its own hidden class, so two copies of this
15
+ * module build two maps for the same shape, and every site that reads both turns
16
+ * polymorphic. The build therefore emits it once (`dist/cst/plain.js`) and every public
17
+ * entry imports that file (`scripts/build.mjs`). The emitted engine receives the same
18
+ * functions as factory parameters (`EMITTED_PARAMS`).
19
+ */
20
+ import type { Span } from '../types.ts'
21
+ import type { CstCaptureBuf } from './capture-buffer.ts'
22
+
23
+ export type PlainLeafObject = { _tag: 'leaf'; value: unknown; span: Span }
24
+ export type PlainNodeObject = { _tag: 'node'; type: string; span: Span; state: unknown; children: unknown[] }
25
+ export type PlainFieldObject = { name: string; value: unknown; span: Span }
26
+
27
+ export declare const PlainSpan: new (start: number, end: number) => Span
28
+ export declare const PlainLeaf: new (value: unknown, span: Span) => PlainLeafObject
29
+ export type PlainLineSpanObject = { start: number; end: number; startLine: number; startColumn: number; endLine: number; endColumn: number }
30
+ export declare const PlainLineSpan: new (start: number, end: number, startLine: number, startColumn: number, endLine: number, endColumn: number) => PlainLineSpanObject
31
+ export declare const PlainTaggedNode: new (type: string, tags: readonly string[], span: Span, state: unknown, children: unknown[]) => { _tag: 'node'; type: string; tags: readonly string[]; span: Span; state: unknown; children: unknown[] }
32
+ export declare const PlainTriviaField: new (name: string, value: unknown, span: Span, leading: string, trailing: string) => { name: string; value: unknown; span: Span; leading: string; trailing: string }
33
+ export declare const PlainNode: new (type: string, span: Span, state: unknown, children: unknown[]) => PlainNodeObject
34
+ export declare const PlainField: new (name: string, value: unknown, span: Span) => PlainFieldObject
35
+ export declare const PlainBuf: new (rawOnly: true | undefined, noRaw: true | undefined, rawLen: number) => CstCaptureBuf
@@ -0,0 +1,74 @@
1
+ // Constructors for the objects a parse keeps until it ends. Plain JavaScript on
2
+ // purpose: a function constructor whose `prototype` is `Object.prototype` is the only
3
+ // spelling that builds a plain object with a `new` call, and TypeScript cannot declare
4
+ // one in a `.ts` file without a cast. `plain.d.ts` types them. Why constructors at all,
5
+ // and why this is ONE module shared by every bundle: see `plain.d.ts`.
6
+
7
+ export function PlainSpan(start, end) {
8
+ this.start = start
9
+ this.end = end
10
+ }
11
+
12
+ export function PlainLeaf(value, span) {
13
+ this._tag = 'leaf'
14
+ this.value = value
15
+ this.span = span
16
+ }
17
+
18
+ export function PlainLineSpan(start, end, startLine, startColumn, endLine, endColumn) {
19
+ this.start = start
20
+ this.end = end
21
+ this.startLine = startLine
22
+ this.startColumn = startColumn
23
+ this.endLine = endLine
24
+ this.endColumn = endColumn
25
+ }
26
+
27
+ export function PlainNode(type, span, state, children) {
28
+ this._tag = 'node'
29
+ this.type = type
30
+ this.span = span
31
+ this.state = state
32
+ this.children = children
33
+ }
34
+
35
+ export function PlainTaggedNode(type, tags, span, state, children) {
36
+ this._tag = 'node'
37
+ this.type = type
38
+ this.tags = tags
39
+ this.span = span
40
+ this.state = state
41
+ this.children = children
42
+ }
43
+
44
+ export function PlainField(name, value, span) {
45
+ this.name = name
46
+ this.value = value
47
+ this.span = span
48
+ }
49
+
50
+ export function PlainTriviaField(name, value, span, leading, trailing) {
51
+ this.name = name
52
+ this.value = value
53
+ this.span = span
54
+ this.leading = leading
55
+ this.trailing = trailing
56
+ }
57
+
58
+ // Every field assigned, so the three buffer kinds share one shape. `rawLen` is always a
59
+ // count: a field that held `undefined` in most buffers and a number in some would
60
+ // generalize and deoptimize the pushes that read it.
61
+ export function PlainBuf(rawOnly, noRaw, rawLen) {
62
+ this.rawOnly = rawOnly
63
+ this.noRaw = noRaw
64
+ this.rawLen = rawLen
65
+ this.single = undefined
66
+ this.ch = undefined
67
+ this.rawSingle = undefined
68
+ this.raw = undefined
69
+ this.tl = undefined
70
+ }
71
+
72
+ for (const f of [PlainSpan, PlainLineSpan, PlainLeaf, PlainNode, PlainTaggedNode, PlainField, PlainTriviaField, PlainBuf]) {
73
+ f.prototype = Object.prototype
74
+ }
package/src/line-index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Span } from './types.ts'
2
+ import { PlainLineSpan, PlainSpan } from './cst/plain.js'
2
3
 
3
4
  /**
4
5
  * Precomputed index of newline positions for O(log n) offset->line/col lookup.
@@ -98,6 +99,42 @@ export function annotateSpan(span: Span, index: LineIndex): Span {
98
99
  }
99
100
  }
100
101
 
102
+ const SPAN_KEYS = ['start', 'end', 'startLine', 'startColumn', 'endLine', 'endColumn'] as const
103
+
104
+ /**
105
+ * A copy of a span, exactly as given: the same keys in the same order. The two shapes
106
+ * parseman builds itself, `{ start, end }` and the six-key line span, are rebuilt by their
107
+ * constructors (`src/cst/plain.d.ts`); any other shape a caller passes (an extra key, a
108
+ * different order, a partial line span) is spread, as before.
109
+ */
110
+ export function copySpan(span: Span): Span {
111
+ // `for…in` walks the keys in order without allocating a key list.
112
+ let n = 0
113
+ for (const key in span) {
114
+ if (key !== SPAN_KEYS[n]) return { ...span }
115
+ n++
116
+ }
117
+ if (n === 2) return new PlainSpan(span.start, span.end)
118
+ if (n === 6 && span.startLine !== undefined && span.startColumn !== undefined && span.endLine !== undefined && span.endColumn !== undefined) {
119
+ return new PlainLineSpan(span.start, span.end, span.startLine, span.startColumn, span.endLine, span.endColumn)
120
+ }
121
+ return { ...span }
122
+ }
123
+
124
+ /**
125
+ * `annotateSpanFromLineContext` for a span parseman built itself (`{ start, end }` only),
126
+ * returning a constructor-built line span (`src/cst/plain.d.ts`). The public form keeps
127
+ * any extra keys a caller's span carries, so it spreads.
128
+ */
129
+ export function lineSpanFromContext(span: Span, ctx: LineTrackingContext): Span {
130
+ if (!ctx.trackLines) return span
131
+ const index = ctx._lineIndex ?? (ctx._lineStarts ? { lineStarts: ctx._lineStarts } : undefined)
132
+ if (!index) return span
133
+ const s = offsetToLineCol(index, span.start)
134
+ const e = offsetToLineCol(index, span.end)
135
+ return new PlainLineSpan(span.start, span.end, s.line, s.col, e.line, e.col)
136
+ }
137
+
101
138
  export function annotateSpanFromLineContext(
102
139
  span: Span,
103
140
  ctx: LineTrackingContext,
@@ -12,6 +12,7 @@ import { OP_ATTEMPT, OP_NODE, OP_NODE_TRACK, OP_REPV, OP_SCAN } from '../table/o
12
12
  import { emitAssemblySource, type EmitResult } from '../table/emit-assembly.ts'
13
13
  import { defaultAssemblyCfgs } from '../table/emit.ts'
14
14
  import { assemblyExtraIps, resolveTable, type TableProgram } from '../table/program.ts'
15
+ import { FACTORY_ARITY_GUARD } from '../table/emit.ts'
15
16
 
16
17
  type Ast = { type: string; start?: number; end?: number; [key: string]: unknown }
17
18
 
@@ -271,8 +272,8 @@ export function supercompileRuleMapReplacement(
271
272
  if (typeof body.start !== 'number' || typeof body.end !== 'number') return replacement
272
273
  const start = body.start - prefix.length + 1
273
274
  const end = body.end - prefix.length - 1
274
- if (replacement.slice(start, end) !== canonical.source + '\n') return replacement
275
- const transformed = replacement.slice(0, start) + candidate.source + '\n' + replacement.slice(end)
275
+ if (replacement.slice(start, end) !== FACTORY_ARITY_GUARD + canonical.source + '\n') return replacement
276
+ const transformed = replacement.slice(0, start) + FACTORY_ARITY_GUARD + candidate.source + '\n' + replacement.slice(end)
276
277
  const checked = parseSync('parseman-entry-supercompile-replacement-result.js', prefix + transformed)
277
278
  return checked.errors.length === 0 ? transformed : replacement
278
279
  }
@@ -2,6 +2,7 @@ import type { Combinator, FirstSet, ParseContext, ParseError, ParseResult, Recov
2
2
  import { cstCaptureActive, pushCstChild } from '../cst/capture-buffer.ts'
3
3
  import { rangesHaveLead } from '../lead-ranges.ts'
4
4
  import { annotateSpanFromLineContext, recordLineRangeFromContext } from '../line-index.ts'
5
+ import { PlainSpan } from '../cst/plain.js'
5
6
 
6
7
  /**
7
8
  * Recovery MECHANISM — sync-source-agnostic. These primitives implement "scan
@@ -112,7 +113,7 @@ export function recoverScan(
112
113
  ): { error: ParseError; end: number } {
113
114
  let scanPos = from
114
115
  while (scanPos < input.length && !probeAt(sync, input, scanPos, ctx)) scanPos++
115
- const span = { start: from, end: scanPos }
116
+ const span = new PlainSpan(from, scanPos)
116
117
  if (ctx.trackLines) recordLineRangeFromContext(ctx, input, from, scanPos)
117
118
  const error: ParseError = { _tag: 'parseError', span: ctx.trackLines ? annotateSpanFromLineContext(span, ctx) : span, expected }
118
119
  ctx._errors?.push(error)