parseman 0.58.2 → 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.
- package/dist/analysis/diagnostics.js +344 -130
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +346 -131
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/field-trivia.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/keywords.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +28 -4
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/compiler/token-scanner.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/plain.js +69 -0
- package/dist/cst/plain.js.map +6 -0
- package/dist/index.js +350 -138
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +29 -5
- package/dist/language-service/index.js.map +3 -3
- package/dist/lead-ranges.d.ts +26 -0
- package/dist/lead-ranges.d.ts.map +1 -0
- package/dist/line-index.d.ts +13 -0
- package/dist/line-index.d.ts.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/index.js +352 -140
- package/dist/plugin/index.js.map +3 -3
- package/dist/recovery/scan.d.ts.map +1 -1
- package/dist/regex/classes.d.ts +5 -3
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +7 -1
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.js +14 -4
- package/dist/run/index.js.map +3 -3
- package/dist/spec/index.js +18 -1
- package/dist/spec/index.js.map +2 -2
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +9 -2
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +6 -0
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode-baseline.d.ts.map +1 -1
- package/dist/table/index.js +330 -129
- package/dist/table/index.js.map +3 -3
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/run-support.d.ts.map +1 -1
- package/dist/table/scan-shapes.d.ts +1 -1
- package/dist/table/scan-shapes.d.ts.map +1 -1
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/table/trivia-gap.d.ts +11 -1
- package/dist/table/trivia-gap.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -1
- package/src/combinators/choice.ts +4 -0
- package/src/combinators/dispatch.ts +2 -1
- package/src/combinators/field-trivia.ts +6 -5
- package/src/combinators/first-set.ts +13 -4
- package/src/combinators/keywords.ts +3 -2
- package/src/combinators/literal.ts +5 -4
- package/src/combinators/node.ts +3 -1
- package/src/combinators/regex.ts +33 -13
- package/src/combinators/scanTo.ts +2 -1
- package/src/combinators/token.ts +3 -6
- package/src/compiler/linker.ts +4 -2
- package/src/compiler/token-scanner.ts +4 -1
- package/src/cst/capture-buffer.ts +13 -5
- package/src/cst/plain.d.ts +35 -0
- package/src/cst/plain.js +74 -0
- package/src/lead-ranges.ts +53 -0
- package/src/line-index.ts +37 -0
- package/src/plugin/entry-supercompiler.ts +3 -2
- package/src/recovery/scan.ts +4 -4
- package/src/regex/classes.ts +22 -7
- package/src/regex/first-set.ts +67 -12
- package/src/table/assemble.ts +75 -38
- package/src/table/compile-rule-map.ts +5 -3
- package/src/table/emit-assembly.ts +69 -32
- package/src/table/emit.ts +9 -1
- package/src/table/encode-baseline.ts +2 -1
- package/src/table/program.ts +2 -1
- package/src/table/run-support.ts +4 -7
- package/src/table/scan-shapes.ts +5 -2
- package/src/table/site-labels.ts +2 -1
- package/src/table/trivia-gap.ts +19 -0
- package/src/version.ts +1 -1
package/src/cst/plain.js
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a first set's ranges answer for an input's leading code point (`lead()`).
|
|
3
|
+
* A leaf module: the minimal run entry reaches it through recovery sentinels.
|
|
4
|
+
*/
|
|
5
|
+
import type { CharRange } from './types.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether `ranges` admit an input led by the code point `code`: in a range, or,
|
|
9
|
+
* for an astral lead, through its high surrogate. Outside Unicode mode a regex
|
|
10
|
+
* reads code units, so a set holding a high surrogate starts every astral code
|
|
11
|
+
* point that surrogate begins. Exact for such a set; for a `u` set holding a
|
|
12
|
+
* lone high surrogate it only over-approximates.
|
|
13
|
+
*
|
|
14
|
+
* The interpreter's two hot lead tests (`startsFirstSet`, choice `inFirstSet`)
|
|
15
|
+
* repeat this body inline: called, it costs them about 6% of the instructions per
|
|
16
|
+
* parse, measured on the CSS example.
|
|
17
|
+
*/
|
|
18
|
+
export function rangesHaveLead(ranges: readonly CharRange[], code: number): boolean {
|
|
19
|
+
for (const r of ranges) if (code >= r.lo && code <= r.hi) return true
|
|
20
|
+
if (code <= 0xffff) return false
|
|
21
|
+
const high = 0xd800 + ((code - 0x10000) >> 10)
|
|
22
|
+
for (const r of ranges) if (high >= r.lo && high <= r.hi) return true
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `ranges` with the astral code points `rangesHaveLead` admits through their
|
|
27
|
+
* high surrogates, for a test that reads a lead by range alone. */
|
|
28
|
+
export function leadRanges(ranges: readonly CharRange[]): readonly CharRange[] {
|
|
29
|
+
let out: CharRange[] | undefined
|
|
30
|
+
for (const r of ranges) {
|
|
31
|
+
const lo = Math.max(r.lo, 0xd800), hi = Math.min(r.hi, 0xdbff)
|
|
32
|
+
if (lo <= hi) (out ??= [...ranges]).push({ lo: 0x10000 + ((lo - 0xd800) << 10), hi: 0x10000 + ((hi - 0xd800) << 10) + 0x3ff })
|
|
33
|
+
}
|
|
34
|
+
return out ?? ranges
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The inverse of `leadRanges`: drop the astral code points that `ranges`' own high
|
|
38
|
+
* surrogates already admit, so a set read through `leadRanges` (an intersection)
|
|
39
|
+
* keeps the code-unit form a table encodes narrowly. `leadRanges` of the result
|
|
40
|
+
* equals `leadRanges` of the input. */
|
|
41
|
+
export function implicitLeadsDropped(ranges: readonly CharRange[]): CharRange[] {
|
|
42
|
+
const implied = leadRanges(ranges.filter(r => r.lo <= 0xffff)).filter(r => r.lo > 0xffff)
|
|
43
|
+
const out: CharRange[] = []
|
|
44
|
+
for (const r of ranges) {
|
|
45
|
+
let pieces: CharRange[] = [{ lo: r.lo, hi: r.hi }]
|
|
46
|
+
if (r.hi > 0xffff) for (const c of implied) {
|
|
47
|
+
pieces = pieces.flatMap(q => q.hi < c.lo || q.lo > c.hi ? [q]
|
|
48
|
+
: [...(q.lo < c.lo ? [{ lo: q.lo, hi: c.lo - 1 }] : []), ...(q.hi > c.hi ? [{ lo: c.hi + 1, hi: q.hi }] : [])])
|
|
49
|
+
}
|
|
50
|
+
out.push(...pieces)
|
|
51
|
+
}
|
|
52
|
+
return out
|
|
53
|
+
}
|
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
|
}
|
package/src/recovery/scan.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Combinator, FirstSet, ParseContext, ParseError, ParseResult, RecoveryHelpers } from '../types.ts'
|
|
2
2
|
import { cstCaptureActive, pushCstChild } from '../cst/capture-buffer.ts'
|
|
3
|
+
import { rangesHaveLead } from '../lead-ranges.ts'
|
|
3
4
|
import { annotateSpanFromLineContext, recordLineRangeFromContext } from '../line-index.ts'
|
|
5
|
+
import { PlainSpan } from '../cst/plain.js'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Recovery MECHANISM — sync-source-agnostic. These primitives implement "scan
|
|
@@ -83,9 +85,7 @@ export function firstSetSentinel(fs: FirstSet): Combinator<null> | null {
|
|
|
83
85
|
parse(input: string, pos: number): ParseResult<null> {
|
|
84
86
|
const code = pos < input.length ? input.codePointAt(pos) : undefined
|
|
85
87
|
if (code !== undefined) {
|
|
86
|
-
|
|
87
|
-
if (code >= r.lo && code <= r.hi) return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
88
|
-
}
|
|
88
|
+
if (rangesHaveLead(ranges, code)) return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
89
89
|
}
|
|
90
90
|
return { ok: false, expected: [], span: { start: pos, end: pos } }
|
|
91
91
|
},
|
|
@@ -113,7 +113,7 @@ export function recoverScan(
|
|
|
113
113
|
): { error: ParseError; end: number } {
|
|
114
114
|
let scanPos = from
|
|
115
115
|
while (scanPos < input.length && !probeAt(sync, input, scanPos, ctx)) scanPos++
|
|
116
|
-
const span =
|
|
116
|
+
const span = new PlainSpan(from, scanPos)
|
|
117
117
|
if (ctx.trackLines) recordLineRangeFromContext(ctx, input, from, scanPos)
|
|
118
118
|
const error: ParseError = { _tag: 'parseError', span: ctx.trackLines ? annotateSpanFromLineContext(span, ctx) : span, expected }
|
|
119
119
|
ctx._errors?.push(error)
|
package/src/regex/classes.ts
CHANGED
|
@@ -38,12 +38,20 @@ export function shorthandRanges(ch: 'd' | 'w' | 's'): Array<[number, number]> {
|
|
|
38
38
|
return [[48, 57], [65, 90], [97, 122], [95, 95]]
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** `\uXXXX` at `body[i]` → its code point and the index past it, or null.
|
|
42
|
-
|
|
41
|
+
/** `\uXXXX` at `body[i]` → its code point and the index past it, or null. Under
|
|
42
|
+
* `unicode`, an escaped high surrogate followed by an escaped low one is one
|
|
43
|
+
* code point (`\uD83D\uDE00` is U+1F600). */
|
|
44
|
+
export function readUnicodeEscape(body: string, i: number, unicode = false): { cp: number; next: number } | null {
|
|
43
45
|
if (body[i] !== '\\' || body[i + 1] !== 'u') return null
|
|
44
46
|
const hex = body.slice(i + 2, i + 6)
|
|
45
47
|
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null
|
|
46
|
-
|
|
48
|
+
const cp = Number.parseInt(hex, 16)
|
|
49
|
+
if (unicode && cp >= 0xd800 && cp <= 0xdbff && body[i + 6] === '\\' && body[i + 7] === 'u') {
|
|
50
|
+
const lowHex = body.slice(i + 8, i + 12)
|
|
51
|
+
const low = /^[0-9a-fA-F]{4}$/.test(lowHex) ? Number.parseInt(lowHex, 16) : -1
|
|
52
|
+
if (low >= 0xdc00 && low <= 0xdfff) return { cp: 0x10000 + ((cp - 0xd800) << 10) + (low - 0xdc00), next: i + 12 }
|
|
53
|
+
}
|
|
54
|
+
return { cp, next: i + 6 }
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/** Regex syntax characters that cannot appear in a LITERAL fragment unescaped. */
|
|
@@ -66,6 +74,8 @@ export function literalCodePoints(frag: string): number[] | null {
|
|
|
66
74
|
// Any other word-character escape is a class (`\d`), an assertion (`\b`), a
|
|
67
75
|
// backreference or a hex, unicode or control escape, never the character itself.
|
|
68
76
|
if (!(e in CLASS_ESCAPES) && /\w/.test(e)) return null
|
|
77
|
+
// `\0` then a digit is a legacy octal escape, not NUL followed by a digit.
|
|
78
|
+
if (e === '0' && frag[i + 2] !== undefined && frag[i + 2]! >= '0' && frag[i + 2]! <= '9') return null
|
|
69
79
|
out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e]! : e.codePointAt(0)!)
|
|
70
80
|
i += 2
|
|
71
81
|
continue
|
|
@@ -142,14 +152,14 @@ type ClassAtom = { cp: number } | { set: Array<[number, number]> }
|
|
|
142
152
|
* than being mis-read as a literal letter, so callers fall back to a safe
|
|
143
153
|
* over-approximation instead of a wrong set.
|
|
144
154
|
*/
|
|
145
|
-
export function parseClassRanges(body: string): Array<[number, number]> | null {
|
|
155
|
+
export function parseClassRanges(body: string, unicode = false): Array<[number, number]> | null {
|
|
146
156
|
const ranges: Array<[number, number]> = []
|
|
147
157
|
let i = 0
|
|
148
158
|
const readAtom = (): ClassAtom | null => {
|
|
149
159
|
const ch = body[i]
|
|
150
160
|
if (ch === undefined) return null
|
|
151
161
|
if (ch === '\\') {
|
|
152
|
-
const uni = readUnicodeEscape(body, i)
|
|
162
|
+
const uni = readUnicodeEscape(body, i, unicode)
|
|
153
163
|
if (uni) {
|
|
154
164
|
i = uni.next
|
|
155
165
|
return { cp: uni.cp }
|
|
@@ -157,14 +167,19 @@ export function parseClassRanges(body: string): Array<[number, number]> | null {
|
|
|
157
167
|
const e = body[i + 1]
|
|
158
168
|
if (e === undefined) return null
|
|
159
169
|
i += 2
|
|
170
|
+
// A digit after `\0`, or any other digit escape, is a legacy octal escape
|
|
171
|
+
// (`[\012]` holds a newline), which this does not decode.
|
|
172
|
+
if ((e === '0' && body[i] !== undefined && body[i]! >= '0' && body[i]! <= '9') || (e >= '1' && e <= '9')) return null
|
|
160
173
|
if (e in CLASS_ESCAPES) return { cp: CLASS_ESCAPES[e]! }
|
|
161
174
|
if (e === 'd' || e === 'w' || e === 's') return { set: shorthandRanges(e) }
|
|
162
175
|
// Any other letter escape is a class we can't safely lower (\D, \W, \S, …).
|
|
163
176
|
if ((e >= 'a' && e <= 'z') || (e >= 'A' && e <= 'Z')) return null
|
|
164
177
|
return { cp: e.codePointAt(0)! }
|
|
165
178
|
}
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
// Unicode mode reads a member as one code point; otherwise as one code unit.
|
|
180
|
+
const cp = unicode ? body.codePointAt(i)! : body.charCodeAt(i)
|
|
181
|
+
i += cp > 0xffff ? 2 : 1
|
|
182
|
+
return { cp }
|
|
168
183
|
}
|
|
169
184
|
while (i < body.length) {
|
|
170
185
|
const lo = readAtom()
|
package/src/regex/first-set.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { FirstSet } from '../types.ts'
|
|
25
25
|
import { any, empty, fromRange, union } from '../combinators/first-set.ts'
|
|
26
|
-
import { shorthandRanges, parseClassRanges } from './classes.ts'
|
|
26
|
+
import { shorthandRanges, parseClassRanges, readUnicodeEscape } from './classes.ts'
|
|
27
27
|
|
|
28
28
|
// A minimal regex AST. Each node answers two questions: what first chars can a
|
|
29
29
|
// non-empty match begin with (`firstSet`), and can the node match zero chars
|
|
@@ -72,7 +72,7 @@ const BAIL = Symbol('regex-bail')
|
|
|
72
72
|
* Closure over a single cursor `i` — recursive-descent: alt → seq → quantified
|
|
73
73
|
* → atom.
|
|
74
74
|
*/
|
|
75
|
-
function parseRegex(src: string): Node {
|
|
75
|
+
function parseRegex(src: string, unicode = false, sets = false): Node {
|
|
76
76
|
let i = 0
|
|
77
77
|
|
|
78
78
|
const parseAlt = (): Node => {
|
|
@@ -128,9 +128,13 @@ function parseRegex(src: string): Node {
|
|
|
128
128
|
case '^':
|
|
129
129
|
case '$': i++; return EMPTY // anchors are zero-width
|
|
130
130
|
case '{': i++; return charNode(fromRange(0x7b, 0x7b), false) // a `{` with nothing to quantify is a literal brace
|
|
131
|
-
default:
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
default: {
|
|
132
|
+
// Unicode mode reads a literal as one code point (an astral one is two
|
|
133
|
+
// code units); otherwise each code unit is its own atom.
|
|
134
|
+
const cp = unicode ? src.codePointAt(i)! : src.charCodeAt(i)
|
|
135
|
+
i += cp > 0xffff ? 2 : 1
|
|
136
|
+
return charNode(fromRange(cp, cp), ch === '\n')
|
|
137
|
+
}
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -163,6 +167,24 @@ function parseRegex(src: string): Node {
|
|
|
163
167
|
let k = i + 1
|
|
164
168
|
const negated = src[k] === '^'
|
|
165
169
|
if (negated) k++
|
|
170
|
+
if (sets) {
|
|
171
|
+
// `v` mode nests classes and adds set operations (`[[^a]--b]`, `[a&&b]`) and
|
|
172
|
+
// string members (`\q{…}`). Only a flat class is enumerated here; the rest
|
|
173
|
+
// match one character (or string) of a set this does not compute.
|
|
174
|
+
let depth = 1
|
|
175
|
+
let nested = false
|
|
176
|
+
while (k < src.length && depth > 0) {
|
|
177
|
+
const c = src[k]!
|
|
178
|
+
if (c === '\\') { if (src[k + 1] === 'q') nested = true; k += 2; continue }
|
|
179
|
+
if (c === '[') { depth++; nested = true }
|
|
180
|
+
else if (c === ']') { depth--; if (depth === 0) break }
|
|
181
|
+
else if ((c === '-' || c === '&') && src[k + 1] === c) nested = true
|
|
182
|
+
k++
|
|
183
|
+
}
|
|
184
|
+
if (src[k] !== ']') throw BAIL
|
|
185
|
+
if (nested) { i = k + 1; return charNode(any(), true) }
|
|
186
|
+
k = i + 1 + (negated ? 1 : 0)
|
|
187
|
+
}
|
|
166
188
|
let body = ''
|
|
167
189
|
while (k < src.length && src[k] !== ']') {
|
|
168
190
|
if (src[k] === '\\') { body += src[k]! + (src[k + 1] ?? ''); k += 2 }
|
|
@@ -170,7 +192,7 @@ function parseRegex(src: string): Node {
|
|
|
170
192
|
}
|
|
171
193
|
if (src[k] !== ']') throw BAIL
|
|
172
194
|
i = k + 1
|
|
173
|
-
const ranges = parseClassRanges(body)
|
|
195
|
+
const ranges = parseClassRanges(body, unicode)
|
|
174
196
|
// A negated class can start with (almost) any char, so the first-set widens to
|
|
175
197
|
// `any`, but its newline capability is still cheap and useful to know:
|
|
176
198
|
// `[^,\r\n]*` is a workhorse "field until line end" regex and must not force
|
|
@@ -184,9 +206,19 @@ function parseRegex(src: string): Node {
|
|
|
184
206
|
const e = src[i + 1]
|
|
185
207
|
if (e === undefined) throw BAIL
|
|
186
208
|
// \uXXXX
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
i
|
|
209
|
+
const uni = readUnicodeEscape(src, i, unicode)
|
|
210
|
+
if (uni !== null) {
|
|
211
|
+
i = uni.next
|
|
212
|
+
return charNode(fromRange(uni.cp, uni.cp), uni.cp === 0x0a)
|
|
213
|
+
}
|
|
214
|
+
// \u{X…}: a code point in Unicode mode. Without `u` it is a literal `u` under
|
|
215
|
+
// a `{…}` quantifier, which may repeat it zero times, so it widens.
|
|
216
|
+
if (e === 'u' && src[i + 2] === '{') {
|
|
217
|
+
const close = src.indexOf('}', i + 3)
|
|
218
|
+
const hex = close < 0 ? '' : src.slice(i + 3, close)
|
|
219
|
+
if (!unicode || !/^[0-9a-fA-F]{1,6}$/.test(hex)) { i += 2; return ANY }
|
|
220
|
+
const cp = Number.parseInt(hex, 16)
|
|
221
|
+
i = close + 1
|
|
190
222
|
return charNode(fromRange(cp, cp), cp === 0x0a)
|
|
191
223
|
}
|
|
192
224
|
// \xHH
|
|
@@ -195,6 +227,17 @@ function parseRegex(src: string): Node {
|
|
|
195
227
|
i += 4
|
|
196
228
|
return charNode(fromRange(cp, cp), cp === 0x0a)
|
|
197
229
|
}
|
|
230
|
+
// \cX: the control character X names. Anything else after `\c` is not one.
|
|
231
|
+
if (e === 'c') {
|
|
232
|
+
const x = src[i + 2]
|
|
233
|
+
if (x !== undefined && /^[A-Za-z]$/.test(x)) {
|
|
234
|
+
const cp = x.charCodeAt(0) % 32
|
|
235
|
+
i += 3
|
|
236
|
+
return charNode(fromRange(cp, cp), cp === 0x0a)
|
|
237
|
+
}
|
|
238
|
+
i += 2
|
|
239
|
+
return ANY
|
|
240
|
+
}
|
|
198
241
|
i += 2
|
|
199
242
|
switch (e) {
|
|
200
243
|
case 'd': return charNode(rangesToSet(shorthandRanges('d')), false)
|
|
@@ -207,7 +250,12 @@ function parseRegex(src: string): Node {
|
|
|
207
250
|
case 't': return charNode(fromRange(0x09, 0x09), false)
|
|
208
251
|
case 'f': return charNode(fromRange(0x0c, 0x0c), false)
|
|
209
252
|
case 'v': return charNode(fromRange(0x0b, 0x0b), false)
|
|
210
|
-
|
|
253
|
+
// `\0` then a digit is a legacy octal escape (`\012` is a newline), not NUL.
|
|
254
|
+
case '0': return src[i] !== undefined && src[i]! >= '0' && src[i]! <= '9' ? ANY : charNode(fromRange(0, 0), false)
|
|
255
|
+
// A property escape (`\p{…}`/`\P{…}`) matches one character of a class this
|
|
256
|
+
// analyzer does not enumerate; `\k<name>` is a backreference, which may match empty.
|
|
257
|
+
case 'p': case 'P': return charNode(any(), true)
|
|
258
|
+
case 'k': return ANY
|
|
211
259
|
default:
|
|
212
260
|
// A digit escape is a backreference we don't resolve → widen.
|
|
213
261
|
if (e >= '1' && e <= '9') return ANY
|
|
@@ -300,10 +348,16 @@ export function regexCanMatchEmpty(source: string): boolean {
|
|
|
300
348
|
try { return nullable(parseRegex(source)) } catch { return true }
|
|
301
349
|
}
|
|
302
350
|
|
|
303
|
-
|
|
351
|
+
/**
|
|
352
|
+
* `flags` decides how atoms read: under `u`/`v` each is one code point, else one
|
|
353
|
+
* code unit, so a non-`u` set holds a high surrogate where an astral character
|
|
354
|
+
* can start a match (`rangesHaveLead` reads an astral lead through it). Case
|
|
355
|
+
* folding under `i` is the caller's, since only it knows what it gates.
|
|
356
|
+
*/
|
|
357
|
+
export function firstSetFromRegex(source: string, flags = ''): { firstSet: FirstSet; canMatchNewline: boolean } {
|
|
304
358
|
let ast: Node
|
|
305
359
|
try {
|
|
306
|
-
ast = parseRegex(source)
|
|
360
|
+
ast = parseRegex(source, flags.includes('u') || flags.includes('v'), flags.includes('v'))
|
|
307
361
|
} catch {
|
|
308
362
|
return { firstSet: any(), canMatchNewline: true }
|
|
309
363
|
}
|
|
@@ -314,3 +368,4 @@ export function firstSetFromRegex(source: string): { firstSet: FirstSet; canMatc
|
|
|
314
368
|
if (nullable(ast)) return { firstSet: any(), canMatchNewline: canConsumeNewline(ast) }
|
|
315
369
|
return { firstSet: firstSet(ast), canMatchNewline: canConsumeNewline(ast) }
|
|
316
370
|
}
|
|
371
|
+
|