grammar-composer 0.6.0 → 0.6.2
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/exports/Exports.d.ts +4 -3
- package/dist/exports/Exports.d.ts.map +1 -1
- package/dist/exports/Exports.js +4 -3
- package/dist/exports/Exports.js.map +1 -1
- package/dist/parser/Builder.d.ts +11 -0
- package/dist/parser/Builder.d.ts.map +1 -0
- package/dist/{parser-generator/Grammar.js → parser/Builder.js} +10 -97
- package/dist/parser/Builder.js.map +1 -0
- package/dist/{parser-generator → parser}/Grammar.d.ts +8 -15
- package/dist/parser/Grammar.d.ts.map +1 -0
- package/dist/parser/Grammar.js +97 -0
- package/dist/parser/Grammar.js.map +1 -0
- package/dist/{parser-generator → parser}/ParseError.d.ts +0 -2
- package/dist/parser/ParseError.d.ts.map +1 -0
- package/dist/{parser-generator → parser}/ParseError.js +35 -8
- package/dist/parser/ParseError.js.map +1 -0
- package/dist/{parser-generator/TopDownParser.d.ts → parser/Parser.d.ts} +4 -3
- package/dist/parser/Parser.d.ts.map +1 -0
- package/dist/{parser-generator/TopDownParser.js → parser/Parser.js} +40 -6
- package/dist/parser/Parser.js.map +1 -0
- package/dist/parser/StaticAnalysis.d.ts.map +1 -0
- package/dist/{parser-generator → parser}/StaticAnalysis.js +16 -5
- package/dist/parser/StaticAnalysis.js.map +1 -0
- package/dist/parser/TerminalToText.d.ts.map +1 -0
- package/dist/{parser-generator → parser}/TerminalToText.js +1 -1
- package/dist/parser/TerminalToText.js.map +1 -0
- package/dist/tests/Test.js +2 -2
- package/dist/tests/Test.js.map +1 -1
- package/dist/utilities/Utilities.d.ts +2 -0
- package/dist/utilities/Utilities.d.ts.map +1 -1
- package/dist/utilities/Utilities.js +15 -1
- package/dist/utilities/Utilities.js.map +1 -1
- package/package.json +6 -4
- package/src/exports/Exports.ts +4 -3
- package/src/{parser-generator/Grammar.ts → parser/Builder.ts} +14 -191
- package/src/parser/Grammar.ts +194 -0
- package/src/{parser-generator → parser}/ParseError.ts +42 -11
- package/src/{parser-generator/TopDownParser.ts → parser/Parser.ts} +59 -10
- package/src/{parser-generator → parser}/StaticAnalysis.ts +17 -7
- package/src/{parser-generator → parser}/TerminalToText.ts +1 -1
- package/src/tests/Test.ts +2 -2
- package/src/utilities/Utilities.ts +21 -1
- package/tsconfig.json +51 -53
- package/dist/parser-generator/Grammar.d.ts.map +0 -1
- package/dist/parser-generator/Grammar.js.map +0 -1
- package/dist/parser-generator/ParseError.d.ts.map +0 -1
- package/dist/parser-generator/ParseError.js.map +0 -1
- package/dist/parser-generator/StaticAnalysis.d.ts.map +0 -1
- package/dist/parser-generator/StaticAnalysis.js.map +0 -1
- package/dist/parser-generator/TerminalToText.d.ts.map +0 -1
- package/dist/parser-generator/TerminalToText.js.map +0 -1
- package/dist/parser-generator/TopDownParser.d.ts.map +0 -1
- package/dist/parser-generator/TopDownParser.js.map +0 -1
- /package/dist/{parser-generator → parser}/StaticAnalysis.d.ts +0 -0
- /package/dist/{parser-generator → parser}/TerminalToText.d.ts +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Pattern, buildRegExp, inputStart, isPatternOptional } from 'regexp-composer'
|
|
2
|
+
import { isArray } from '../utilities/Utilities.js'
|
|
3
|
+
|
|
4
|
+
import { parse } from './Parser.js'
|
|
5
|
+
import { validatePatternCaptureGroups } from './StaticAnalysis.js'
|
|
6
|
+
import { productionToGrammarElement } from './Builder.js'
|
|
7
|
+
|
|
8
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
9
|
+
// Grammar class
|
|
10
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
export class Grammar<T> {
|
|
12
|
+
readonly productions: Record<keyof T, any>
|
|
13
|
+
readonly startProductionName: keyof T
|
|
14
|
+
|
|
15
|
+
readonly maxCacheId: number
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
productions: Record<keyof T, any>,
|
|
19
|
+
startProductionName: keyof T,
|
|
20
|
+
maxCacheId: number) {
|
|
21
|
+
|
|
22
|
+
this.startProductionName = startProductionName
|
|
23
|
+
this.productions = productions
|
|
24
|
+
this.maxCacheId = maxCacheId
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
parse(text: string) {
|
|
28
|
+
return parse(text, this)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get rootElement() {
|
|
32
|
+
return this.productions[this.startProductionName]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
37
|
+
// Exported grammar builder functions
|
|
38
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
39
|
+
export function zeroOrMore(content: Production): Repetition {
|
|
40
|
+
return {
|
|
41
|
+
type: 'Repetition',
|
|
42
|
+
content: productionToGrammarElement(content),
|
|
43
|
+
optional: true,
|
|
44
|
+
cached: false,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function oneOrMore(content: Production): Repetition {
|
|
49
|
+
return {
|
|
50
|
+
type: 'Repetition',
|
|
51
|
+
content: productionToGrammarElement(content),
|
|
52
|
+
optional: false,
|
|
53
|
+
cached: false,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function anyOf(...members: Production[]): Choice {
|
|
58
|
+
if (members.length === 0) {
|
|
59
|
+
throw new Error(`'anyOf' requires at least one member.`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const normalizedMembers = members.map(member => productionToGrammarElement(member))
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
type: 'Choice',
|
|
66
|
+
members: normalizedMembers,
|
|
67
|
+
optional: false,
|
|
68
|
+
exhaustive: false,
|
|
69
|
+
cached: false,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function bestOf(...members: Production[]): Choice {
|
|
74
|
+
if (members.length === 0) {
|
|
75
|
+
throw new Error(`'bestOf' requires at least one member.`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const normalizedMembers = members.map(member => productionToGrammarElement(member))
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
type: 'Choice',
|
|
82
|
+
members: normalizedMembers,
|
|
83
|
+
optional: false,
|
|
84
|
+
exhaustive: true,
|
|
85
|
+
|
|
86
|
+
cached: false,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function possibly<T extends Production>(content: Production): T {
|
|
91
|
+
return { ...productionToGrammarElement(content), optional: true } as T
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function pattern(pattern: Pattern): PatternTerminal {
|
|
95
|
+
if (isArray(pattern)) {
|
|
96
|
+
pattern = [inputStart, ...pattern]
|
|
97
|
+
} else {
|
|
98
|
+
pattern = [inputStart, pattern]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
validatePatternCaptureGroups(pattern)
|
|
102
|
+
|
|
103
|
+
const regExp = buildRegExp(pattern)
|
|
104
|
+
const optional = isPatternOptional(pattern)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
type: 'PatternTerminal',
|
|
108
|
+
pattern,
|
|
109
|
+
regExp,
|
|
110
|
+
optional,
|
|
111
|
+
|
|
112
|
+
cached: false,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function cached<T extends Production>(content: Production): T {
|
|
117
|
+
return { ...productionToGrammarElement(content), cached: true } as T
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function uncached<T extends Production>(content: Production): T {
|
|
121
|
+
return { ...productionToGrammarElement(content), cached: false } as T
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
125
|
+
// Type definitions
|
|
126
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
127
|
+
export type Production = string | GrammarElement | (() => Production) | Production[]
|
|
128
|
+
|
|
129
|
+
export type GrammarElement =
|
|
130
|
+
StringTerminal |
|
|
131
|
+
PatternTerminal |
|
|
132
|
+
Nonterminal |
|
|
133
|
+
Sequence |
|
|
134
|
+
Repetition |
|
|
135
|
+
Choice |
|
|
136
|
+
NonterminalReference
|
|
137
|
+
|
|
138
|
+
interface GrammarElementBase {
|
|
139
|
+
type: string
|
|
140
|
+
optional: boolean
|
|
141
|
+
|
|
142
|
+
cached: boolean
|
|
143
|
+
cacheId?: number
|
|
144
|
+
|
|
145
|
+
// The name of the grammar property this element was assigned to, if any.
|
|
146
|
+
// Used by the parser to refer to the element by name in error messages.
|
|
147
|
+
name?: string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type Terminal = StringTerminal | PatternTerminal
|
|
151
|
+
|
|
152
|
+
export interface StringTerminal extends GrammarElementBase {
|
|
153
|
+
type: 'StringTerminal'
|
|
154
|
+
content: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface PatternTerminal extends GrammarElementBase {
|
|
158
|
+
type: 'PatternTerminal'
|
|
159
|
+
pattern: Pattern
|
|
160
|
+
regExp: RegExp
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface Nonterminal extends GrammarElementBase {
|
|
164
|
+
type: 'Nonterminal'
|
|
165
|
+
name: string
|
|
166
|
+
content: GrammarElement
|
|
167
|
+
unwrapped: boolean
|
|
168
|
+
|
|
169
|
+
// The canonical nonterminal as defined in the grammar. Clones created for
|
|
170
|
+
// optional references and cached references keep a reference to the original,
|
|
171
|
+
// so identity comparisons work across clones.
|
|
172
|
+
grammarNonterminal?: Nonterminal
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface Sequence extends GrammarElementBase {
|
|
176
|
+
type: 'Sequence'
|
|
177
|
+
members: GrammarElement[]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface Repetition extends GrammarElementBase {
|
|
181
|
+
type: 'Repetition'
|
|
182
|
+
content: GrammarElement
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface Choice extends GrammarElementBase {
|
|
186
|
+
type: 'Choice'
|
|
187
|
+
members: GrammarElement[]
|
|
188
|
+
exhaustive: boolean
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface NonterminalReference extends GrammarElementBase {
|
|
192
|
+
type: 'NonterminalReference'
|
|
193
|
+
reference: Function
|
|
194
|
+
}
|
|
@@ -122,21 +122,24 @@ export function analyzeFailedMatches(failedMatches: FailedMatch[]): FailedMatchA
|
|
|
122
122
|
// A candidate is redundant when the same terminal is also expected by a deeper
|
|
123
123
|
// production within the same context, i.e. when another candidate's production
|
|
124
124
|
// appears below this candidate's production in its own stack.
|
|
125
|
+
//
|
|
126
|
+
// Compare within the other candidate's own stack: if there exists another
|
|
127
|
+
// candidate `other` such that `candidate` is an ancestor of `other` in
|
|
128
|
+
// `other`'s stack, keep only `other` (innermost).
|
|
125
129
|
function isRedundantCandidate(candidate: TerminalCandidate, candidates: TerminalCandidate[]): boolean {
|
|
126
|
-
const candidateIndex = stackIndexOf(candidate.productionStack, candidate.production)
|
|
127
|
-
|
|
128
130
|
for (const other of candidates) {
|
|
129
131
|
if (other === candidate) {
|
|
130
132
|
continue
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
const
|
|
135
|
+
const candidateIndexInOther = stackIndexOf(other.productionStack, candidate.production)
|
|
136
|
+
|
|
137
|
+
const otherIndexInOther = stackIndexOf(other.productionStack, other.production)
|
|
134
138
|
|
|
135
|
-
if (
|
|
139
|
+
if (candidateIndexInOther !== -1 && otherIndexInOther !== -1 && otherIndexInOther > candidateIndexInOther) {
|
|
136
140
|
return true
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
|
-
|
|
140
143
|
return false
|
|
141
144
|
}
|
|
142
145
|
|
|
@@ -148,6 +151,26 @@ function stackIndexOf(productionStack: Nonterminal[], production: Nonterminal):
|
|
|
148
151
|
return productionStack.findIndex(nonterminal => (nonterminal.grammarNonterminal ?? nonterminal) === canonical)
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
function areTerminalsEqual(a: any, b: any): boolean {
|
|
155
|
+
if (a === b) {
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (a?.type !== b?.type) {
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (a.type === 'StringTerminal') {
|
|
164
|
+
return a.content === b.content
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (a.type === 'PatternTerminal') {
|
|
168
|
+
return a.regExp.source === b.regExp.source && a.regExp.flags === b.regExp.flags
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
151
174
|
function stringifyFailedTerminal(failedMatch: FailedMatch): string {
|
|
152
175
|
const { terminal, productionStack } = failedMatch
|
|
153
176
|
|
|
@@ -155,10 +178,18 @@ function stringifyFailedTerminal(failedMatch: FailedMatch): string {
|
|
|
155
178
|
return terminal.name
|
|
156
179
|
}
|
|
157
180
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
181
|
+
// Compare terminals by structural identity: PatternTerminals are
|
|
182
|
+
// reconstructed objects, StringTerminals are cloned, so object
|
|
183
|
+
// identity vs innermostProduction.content is not reliable.
|
|
184
|
+
//
|
|
185
|
+
// Instead check if the failed terminal equals the content of any
|
|
186
|
+
// production in the stack by value.
|
|
187
|
+
for (let i = productionStack.length - 1; i >= 0; i--) {
|
|
188
|
+
const production = productionStack[i]
|
|
189
|
+
|
|
190
|
+
if (areTerminalsEqual(production.content, terminal)) {
|
|
191
|
+
return production.name
|
|
192
|
+
}
|
|
162
193
|
}
|
|
163
194
|
|
|
164
195
|
const stringifiedLengthLimit = 100
|
|
@@ -176,7 +207,7 @@ function stringifyFailedTerminal(failedMatch: FailedMatch): string {
|
|
|
176
207
|
// during a single parse: the grammar builder creates spread copies of the canonical
|
|
177
208
|
// nonterminal for optional references and for references wrapped in cached(). Each clone
|
|
178
209
|
// keeps a reference to the original in 'grammarNonterminal'.
|
|
179
|
-
|
|
210
|
+
function findCommonProductionPrefix(productionStacks: Nonterminal[][]): Nonterminal[] {
|
|
180
211
|
if (productionStacks.length === 0) {
|
|
181
212
|
return []
|
|
182
213
|
}
|
|
@@ -251,7 +282,7 @@ function buildExpectedGroupsMessage(expected: readonly ExpectedTerminal[]): stri
|
|
|
251
282
|
.join('\n\n')
|
|
252
283
|
}
|
|
253
284
|
|
|
254
|
-
|
|
285
|
+
function formatErrorContext(input: string, offset: number, maxDisplayedLineLength: number): string {
|
|
255
286
|
const { line, column } = getLineAndColumn(input, offset)
|
|
256
287
|
|
|
257
288
|
let lineStartOffset = offset
|
|
@@ -5,7 +5,7 @@ import { type FailedMatch, ParseError } from './ParseError.js'
|
|
|
5
5
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
// Main parser function
|
|
7
7
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
|
8
|
-
export function parse(inputString: string, grammar: Grammar<any>, options?:
|
|
8
|
+
export function parse(inputString: string, grammar: Grammar<any>, options?: ParserOptions) {
|
|
9
9
|
options = { ...options }
|
|
10
10
|
|
|
11
11
|
const inputLength = inputString.length
|
|
@@ -15,7 +15,7 @@ export function parse(inputString: string, grammar: Grammar<any>, options?: TopD
|
|
|
15
15
|
let bestFailedMatches: FailedMatch[] = []
|
|
16
16
|
let bestFailedMatchesOffset = -1
|
|
17
17
|
|
|
18
|
-
const cacheKeyOffsetMultiplier = grammar.maxCacheId + 1
|
|
18
|
+
const cacheKeyOffsetMultiplier = grammar.maxCacheId > 0 ? grammar.maxCacheId + 1 : 1
|
|
19
19
|
const parseResultsCache = new Map<number, ParseResult | null>()
|
|
20
20
|
|
|
21
21
|
function updateBestFailedMatchesIfNeeded(terminal: Terminal, startOffset: number) {
|
|
@@ -58,6 +58,21 @@ export function parse(inputString: string, grammar: Grammar<any>, options?: TopD
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function tryParseUncached(grammarElement: GrammarElement, startOffset: number): ParseResult | null {
|
|
61
|
+
const result = tryParseElement(grammarElement, startOffset)
|
|
62
|
+
|
|
63
|
+
if (result === null && grammarElement.optional === true) {
|
|
64
|
+
// An optional element that doesn't match is "skipped": it succeeds
|
|
65
|
+
// by consuming no input and producing no nodes. This uniformly
|
|
66
|
+
// implements the `possibly()` "or skip" semantics wherever the
|
|
67
|
+
// element appears (sequence members, repetition content, choice
|
|
68
|
+
// members, or as the content of a production itself).
|
|
69
|
+
return { endOffset: startOffset, nodes: undefined, skipped: true }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function tryParseElement(grammarElement: GrammarElement, startOffset: number): ParseResult | null {
|
|
61
76
|
switch (grammarElement.type) {
|
|
62
77
|
case 'StringTerminal': {
|
|
63
78
|
const target = grammarElement.content
|
|
@@ -102,16 +117,35 @@ export function parse(inputString: string, grammar: Grammar<any>, options?: TopD
|
|
|
102
117
|
const groupsIndices = matchResults.indices
|
|
103
118
|
|
|
104
119
|
if (groupsIndices !== undefined) {
|
|
105
|
-
|
|
120
|
+
const children: ParseTreeNode[] = []
|
|
121
|
+
|
|
122
|
+
// Build a reverse lookup from indices entry -> group name by identity.
|
|
123
|
+
// This is robust even when some groups do not participate (undefined)
|
|
124
|
+
// and avoids relying on Object.keys order aligning with numeric indices.
|
|
125
|
+
let indexToName: (string | undefined)[] | undefined = undefined
|
|
106
126
|
|
|
107
127
|
if (groupsIndices.groups) {
|
|
108
|
-
|
|
109
|
-
}
|
|
128
|
+
indexToName = new Array(groupsIndices.length)
|
|
110
129
|
|
|
111
|
-
|
|
130
|
+
for (const [name, range] of Object.entries(groupsIndices.groups)) {
|
|
131
|
+
if (range === undefined) {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
112
134
|
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
// `range` is the same array instance as groupsIndices[index]
|
|
136
|
+
const index = groupsIndices.indexOf(range)
|
|
137
|
+
|
|
138
|
+
if (index >= 0) {
|
|
139
|
+
indexToName[index] = name
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Mark which indices actually correspond to named groups
|
|
144
|
+
// (validatePatternCaptureGroups guarantees all-or-nothing, but be defensive)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (let i = 1; i < (groupsIndices as unknown as number[][]).length; i++) {
|
|
148
|
+
const groupIndices = (groupsIndices as unknown as (number[] | undefined)[])[i]
|
|
115
149
|
|
|
116
150
|
if (groupIndices === undefined) {
|
|
117
151
|
continue
|
|
@@ -120,8 +154,10 @@ export function parse(inputString: string, grammar: Grammar<any>, options?: TopD
|
|
|
120
154
|
const groupStartOffset = startOffset + groupIndices[0]
|
|
121
155
|
const groupEndOffset = startOffset + groupIndices[1]
|
|
122
156
|
|
|
157
|
+
const name = indexToName?.[i] ?? i.toString()
|
|
158
|
+
|
|
123
159
|
children.push({
|
|
124
|
-
name
|
|
160
|
+
name,
|
|
125
161
|
startOffset: groupStartOffset,
|
|
126
162
|
endOffset: groupEndOffset,
|
|
127
163
|
sourceText: inputString.substring(groupStartOffset, groupEndOffset),
|
|
@@ -151,6 +187,13 @@ export function parse(inputString: string, grammar: Grammar<any>, options?: TopD
|
|
|
151
187
|
return null
|
|
152
188
|
}
|
|
153
189
|
|
|
190
|
+
if (result.skipped === true) {
|
|
191
|
+
// The content was skipped (it was optional and didn't match),
|
|
192
|
+
// so no node should be created for this nonterminal either,
|
|
193
|
+
// and the skip is propagated to the parent.
|
|
194
|
+
return { endOffset: startOffset, nodes: undefined, skipped: true }
|
|
195
|
+
}
|
|
196
|
+
|
|
154
197
|
const grammarElementName = grammarElement.name
|
|
155
198
|
|
|
156
199
|
if (grammarElement.unwrapped) {
|
|
@@ -297,6 +340,12 @@ export function parse(inputString: string, grammar: Grammar<any>, options?: TopD
|
|
|
297
340
|
export interface ParseResult {
|
|
298
341
|
endOffset: number
|
|
299
342
|
nodes: ParseTreeNode[] | undefined
|
|
343
|
+
|
|
344
|
+
// Marks a result produced by an optional element that was "skipped" (it
|
|
345
|
+
// didn't match, so the parse succeeded by consuming no input and producing
|
|
346
|
+
// no nodes). Set internally by the parser; a receiving `Nonterminal` uses
|
|
347
|
+
// it to avoid creating a node for skipped content.
|
|
348
|
+
skipped?: boolean
|
|
300
349
|
}
|
|
301
350
|
|
|
302
351
|
export interface ParseTreeNode {
|
|
@@ -310,5 +359,5 @@ export interface ParseTreeNode {
|
|
|
310
359
|
children?: ParseTreeNode[]
|
|
311
360
|
}
|
|
312
361
|
|
|
313
|
-
export interface
|
|
362
|
+
export interface ParserOptions {
|
|
314
363
|
}
|
|
@@ -131,7 +131,7 @@ export function detectAndAnnotateOptionalNodes(rootNode: GrammarElement) {
|
|
|
131
131
|
// If it stays false, it means that no improvement was made during the iteration,
|
|
132
132
|
// and we should exit the loop.
|
|
133
133
|
let atLastOneDependencyResolvedInAnyNode = false
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
const nodesToDelete: GrammarElement[] = []
|
|
136
136
|
|
|
137
137
|
// Scan the unresolved nodes to locate any new resolved dependencies
|
|
@@ -189,10 +189,12 @@ export function detectAndAnnotateOptionalNodes(rootNode: GrammarElement) {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
// All remaining unresolved nodes
|
|
193
|
-
//
|
|
192
|
+
// All remaining unresolved nodes are part of a strongly-connected cyclic
|
|
193
|
+
// component with no resolvable nullable base case. The least fixed-point
|
|
194
|
+
// for nullability is `false` (not optional): a cycle without an empty
|
|
195
|
+
// alternative cannot derive the empty string.
|
|
194
196
|
for (const node of unresolvedNodes.keys()) {
|
|
195
|
-
resolvedNodes.set(node,
|
|
197
|
+
resolvedNodes.set(node, false)
|
|
196
198
|
unresolvedNodes.delete(node)
|
|
197
199
|
}
|
|
198
200
|
|
|
@@ -210,7 +212,11 @@ export function detectAndErrorOnLeftRecursion(rootNode: GrammarElement) {
|
|
|
210
212
|
if (node.type === 'Nonterminal') {
|
|
211
213
|
throw new Error(`Detected left recursion for nonterminal '${node.name}'.`)
|
|
212
214
|
} else {
|
|
213
|
-
|
|
215
|
+
try {
|
|
216
|
+
throw new Error(`Detected left recursion for node: ${JSON.stringify(node, undefined, 4)}`)
|
|
217
|
+
} catch {
|
|
218
|
+
throw new Error(`Detected left recursion for node type '${(node as any).type}'.`)
|
|
219
|
+
}
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
|
|
@@ -282,7 +288,7 @@ export function validatePatternCaptureGroups(pattern: Pattern): void {
|
|
|
282
288
|
case 'capture': {
|
|
283
289
|
if (node.name !== undefined) {
|
|
284
290
|
if (seenNamedCaptureGroupNames.has(node.name)) {
|
|
285
|
-
throw new Error(`The regular expression pattern contains multiple named capture groups with the same name '${node.name}'. Named capture groups must have unique names.`)
|
|
291
|
+
throw new Error(`The regular expression pattern contains multiple named capture groups with the same name '${(node as any).name}'. Named capture groups must have unique names.`)
|
|
286
292
|
}
|
|
287
293
|
|
|
288
294
|
seenNamedCaptureGroupNames.add(node.name)
|
|
@@ -296,7 +302,11 @@ export function validatePatternCaptureGroups(pattern: Pattern): void {
|
|
|
296
302
|
return
|
|
297
303
|
}
|
|
298
304
|
|
|
299
|
-
// Composite nodes that wrap content without creating a capturing group
|
|
305
|
+
// Composite nodes that wrap content without creating a capturing group.
|
|
306
|
+
// regexp-composer uses a `greedy` boolean to distinguish `zeroOrMore` vs
|
|
307
|
+
// `zeroOrMoreNonGreedy` etc, but both share the same `type` string
|
|
308
|
+
// (`zeroOrMore`, `oneOrMore`, `repeated`). Handle any type that wraps
|
|
309
|
+
// a single `content` field.
|
|
300
310
|
case 'possibly':
|
|
301
311
|
case 'zeroOrMore':
|
|
302
312
|
case 'oneOrMore':
|
package/src/tests/Test.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ParseTreeNode } from '../exports/Exports.js'
|
|
2
|
+
|
|
1
3
|
export function roundToDigits(val: number, digits = 3) {
|
|
2
4
|
const multiplier = 10 ** digits
|
|
3
5
|
|
|
@@ -17,7 +19,7 @@ export function isBigInt(value: unknown): value is bigint {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function isObject(value: unknown): value is object {
|
|
20
|
-
return typeof value === 'object' && !Array.isArray(value)
|
|
22
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function isArray(value: unknown): value is unknown[] {
|
|
@@ -31,3 +33,21 @@ export function isBoolean(value: unknown): value is boolean {
|
|
|
31
33
|
export function isFunction(value: unknown): value is Function {
|
|
32
34
|
return typeof value === 'function'
|
|
33
35
|
}
|
|
36
|
+
|
|
37
|
+
export function stringifyParseTree(parseTree: ParseTreeNode[], depth = 0): string {
|
|
38
|
+
let result = ''
|
|
39
|
+
|
|
40
|
+
const getNewlineWithIndent = (depth: number) => `\n${'\t'.repeat(depth)}`
|
|
41
|
+
|
|
42
|
+
for (const node of parseTree) {
|
|
43
|
+
result += `${getNewlineWithIndent(depth)}${node.name}`
|
|
44
|
+
|
|
45
|
+
if (node.children && node.children.length > 0) {
|
|
46
|
+
result += stringifyParseTree(node.children, depth + 1)
|
|
47
|
+
} else {
|
|
48
|
+
result += `${getNewlineWithIndent(depth + 1)}'${node.sourceText}'`
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result
|
|
53
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,55 +1,53 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"noErrorTruncation": true,
|
|
54
|
-
}
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
// Only compile library sources; tests under /tests are run by vitest and typechecked separately (tsconfig.tests.json)
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
// File Layout
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
// Environment Settings
|
|
9
|
+
// See also https://aka.ms/tsconfig/module
|
|
10
|
+
"module": "nodenext",
|
|
11
|
+
"target": "esnext",
|
|
12
|
+
"types": [
|
|
13
|
+
"node"
|
|
14
|
+
],
|
|
15
|
+
// For nodejs:
|
|
16
|
+
// "lib": ["esnext"],
|
|
17
|
+
// "types": ["node"],
|
|
18
|
+
// and npm install -D @types/node
|
|
19
|
+
// Other Outputs
|
|
20
|
+
"sourceMap": true,
|
|
21
|
+
"declaration": true,
|
|
22
|
+
"declarationMap": true,
|
|
23
|
+
// Stricter Typechecking Options
|
|
24
|
+
// "noUncheckedIndexedAccess": true,
|
|
25
|
+
// "exactOptionalPropertyTypes": true,
|
|
26
|
+
// Style Options
|
|
27
|
+
"noImplicitReturns": true,
|
|
28
|
+
// "noImplicitOverride": true,
|
|
29
|
+
// "noUnusedLocals": true,
|
|
30
|
+
// "noUnusedParameters": true,
|
|
31
|
+
// "noFallthroughCasesInSwitch": true,
|
|
32
|
+
// "noPropertyAccessFromIndexSignature": true,
|
|
33
|
+
// Recommended Options
|
|
34
|
+
"strict": true,
|
|
35
|
+
"jsx": "react-jsx",
|
|
36
|
+
// "verbatimModuleSyntax": true,
|
|
37
|
+
"isolatedModules": true,
|
|
38
|
+
"noUncheckedSideEffectImports": true,
|
|
39
|
+
"moduleDetection": "force",
|
|
40
|
+
"skipLibCheck": true,
|
|
41
|
+
// Extended options not in generated tsconfig.json
|
|
42
|
+
"newLine": "lf",
|
|
43
|
+
"forceConsistentCasingInFileNames": true,
|
|
44
|
+
"alwaysStrict": true,
|
|
45
|
+
"noImplicitThis": true,
|
|
46
|
+
"strictBindCallApply": true,
|
|
47
|
+
"strictFunctionTypes": true,
|
|
48
|
+
"noErrorTruncation": true,
|
|
49
|
+
},
|
|
50
|
+
"include": [
|
|
51
|
+
"src"
|
|
52
|
+
]
|
|
55
53
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Grammar.d.ts","sourceRoot":"","sources":["../../src/parser-generator/Grammar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA8C,MAAM,iBAAiB,CAAA;AASrF,wBAAgB,YAAY,CAAC,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,EAC5D,gBAAgB,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,EACnC,mBAAmB,EAAE,MAAM,CAAC,EAC5B,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgF/C;AAmFD,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU,CAO1D;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU,CAOzD;AAED,wBAAgB,KAAK,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CActD;AAED,wBAAgB,MAAM,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAevD;AAED,wBAAgB,QAAQ,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,CAAC,CAErE;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAoBzD;AAED,wBAAgB,MAAM,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,CAAC,CAEnE;AAED,wBAAgB,QAAQ,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,CAAC,CAErE;AAwED,qBAAa,OAAO,CAAC,CAAC;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAA;IAC1C,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAA;IAErC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAE3B,YAAY,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,UAAU,EAAE,MAAM,EAI9F;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,gDAEjB;IAED,IAAI,WAAW,QAEd;CACD;AAKD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,cAAc,GAAG,CAAC,MAAM,UAAU,CAAC,GAAG,UAAU,EAAE,CAAA;AAEpF,MAAM,MAAM,cAAc,GACzB,cAAc,GACd,eAAe,GACf,WAAW,GACX,QAAQ,GACR,UAAU,GACV,MAAM,GACN,oBAAoB,CAAA;AAErB,UAAU,kBAAkB;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IAEjB,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAIhB,IAAI,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,eAAe,CAAA;AAEvD,MAAM,WAAW,cAAe,SAAQ,kBAAkB;IACzD,IAAI,EAAE,gBAAgB,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IAC1D,IAAI,EAAE,iBAAiB,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,WAAY,SAAQ,kBAAkB;IACtD,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,cAAc,CAAA;IACvB,SAAS,EAAE,OAAO,CAAA;IAKlB,kBAAkB,CAAC,EAAE,WAAW,CAAA;CAChC;AAED,MAAM,WAAW,QAAS,SAAQ,kBAAkB;IACnD,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,EAAE,cAAc,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,UAAW,SAAQ,kBAAkB;IACrD,IAAI,EAAE,YAAY,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;CACvB;AAED,MAAM,WAAW,MAAO,SAAQ,kBAAkB;IACjD,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,UAAU,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC/D,IAAI,EAAE,sBAAsB,CAAA;IAC5B,SAAS,EAAE,QAAQ,CAAA;CACnB;AAKD,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACvC,yBAAyB,CAAC,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAA;CACtD;AAED,MAAM,MAAM,uBAAuB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAA"}
|