grammar-composer 0.4.0 → 0.5.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 (55) hide show
  1. package/README.md +144 -89
  2. package/dist/exports/Exports.d.ts +2 -0
  3. package/dist/exports/Exports.d.ts.map +1 -1
  4. package/dist/exports/Exports.js +2 -0
  5. package/dist/exports/Exports.js.map +1 -1
  6. package/dist/parser-generator/Grammar.d.ts +8 -1
  7. package/dist/parser-generator/Grammar.d.ts.map +1 -1
  8. package/dist/parser-generator/Grammar.js +30 -225
  9. package/dist/parser-generator/Grammar.js.map +1 -1
  10. package/dist/parser-generator/ParseError.d.ts +37 -0
  11. package/dist/parser-generator/ParseError.d.ts.map +1 -0
  12. package/dist/parser-generator/ParseError.js +226 -0
  13. package/dist/parser-generator/ParseError.js.map +1 -0
  14. package/dist/parser-generator/StaticAnalysis.d.ts +6 -0
  15. package/dist/parser-generator/StaticAnalysis.d.ts.map +1 -0
  16. package/dist/parser-generator/StaticAnalysis.js +283 -0
  17. package/dist/parser-generator/StaticAnalysis.js.map +1 -0
  18. package/dist/parser-generator/TerminalToText.d.ts +15 -0
  19. package/dist/parser-generator/TerminalToText.d.ts.map +1 -0
  20. package/dist/parser-generator/TerminalToText.js +158 -0
  21. package/dist/parser-generator/TerminalToText.js.map +1 -0
  22. package/dist/parser-generator/TopDownParser.d.ts +4 -2
  23. package/dist/parser-generator/TopDownParser.d.ts.map +1 -1
  24. package/dist/parser-generator/TopDownParser.js +44 -42
  25. package/dist/parser-generator/TopDownParser.js.map +1 -1
  26. package/dist/tests/Test.js +23 -23
  27. package/dist/tests/Test.js.map +1 -1
  28. package/dist/tests/test-grammars/JsonGrammar.d.ts +12 -11
  29. package/dist/tests/test-grammars/JsonGrammar.d.ts.map +1 -1
  30. package/dist/tests/test-grammars/JsonGrammar.js +25 -10
  31. package/dist/tests/test-grammars/JsonGrammar.js.map +1 -1
  32. package/dist/tests/test-grammars/RegExpGrammar.d.ts +25 -25
  33. package/dist/tests/test-grammars/RegExpGrammar.d.ts.map +1 -1
  34. package/dist/tests/test-grammars/RegExpGrammar.js +48 -31
  35. package/dist/tests/test-grammars/RegExpGrammar.js.map +1 -1
  36. package/dist/tests/test-grammars/XmlGrammar.d.ts +11 -10
  37. package/dist/tests/test-grammars/XmlGrammar.d.ts.map +1 -1
  38. package/dist/tests/test-grammars/XmlGrammar.js +19 -8
  39. package/dist/tests/test-grammars/XmlGrammar.js.map +1 -1
  40. package/dist/utilities/LineAndColumn.d.ts +7 -0
  41. package/dist/utilities/LineAndColumn.d.ts.map +1 -0
  42. package/dist/utilities/LineAndColumn.js +52 -0
  43. package/dist/utilities/LineAndColumn.js.map +1 -0
  44. package/package.json +1 -1
  45. package/src/exports/Exports.ts +2 -0
  46. package/src/parser-generator/Grammar.ts +59 -265
  47. package/src/parser-generator/ParseError.ts +337 -0
  48. package/src/parser-generator/StaticAnalysis.ts +340 -0
  49. package/src/parser-generator/TerminalToText.ts +196 -0
  50. package/src/parser-generator/TopDownParser.ts +67 -49
  51. package/src/tests/Test.ts +26 -24
  52. package/src/tests/test-grammars/JsonGrammar.ts +28 -10
  53. package/src/tests/test-grammars/RegExpGrammar.ts +65 -46
  54. package/src/tests/test-grammars/XmlGrammar.ts +20 -8
  55. package/src/utilities/LineAndColumn.ts +61 -0
@@ -2,33 +2,48 @@ import { Pattern, buildRegExp, inputStart, isPatternOptional } from 'regexp-comp
2
2
  import { isArray, isBoolean, isFunction, isString } from '../utilities/Utilities.js'
3
3
 
4
4
  import { parse } from './TopDownParser.js'
5
+ import { detectAndAnnotateOptionalNodes, detectAndErrorOnLeftRecursion, validatePatternCaptureGroups } from './StaticAnalysis.js'
5
6
 
6
7
  /////////////////////////////////////////////////////////////////////////////////////////////////
7
8
  // Grammar builder method
8
9
  /////////////////////////////////////////////////////////////////////////////////////////////////
9
- export function buildGrammar<T extends { [key: string]: any }>(obj: T | (new () => T), startProductionName: keyof T): Grammar<T> {
10
- if (isFunction(obj)) {
11
- obj = new obj()
10
+ export function buildGrammar<T extends { [key: string]: any }>(
11
+ definitionObject: T | (new () => T),
12
+ startProductionName: keyof T,
13
+ options?: GrammarBuilderOptions<T>): Grammar<T> {
14
+
15
+ options = {
16
+ unwrappedNonterminalNames: [],
17
+ ...options
18
+ }
19
+
20
+ if (isFunction(definitionObject)) {
21
+ definitionObject = new definitionObject()
12
22
  }
13
23
 
14
24
  const nameLookup = new Map<any, keyof T>()
15
25
  const nonterminalLookup = new Map<Function, Nonterminal>()
16
26
  const optionalNonterminalLookup = new Map<Function, Nonterminal>()
17
27
 
18
- for (const key in obj) {
19
- const objectProperty = obj[key]
28
+ for (const key in definitionObject) {
29
+ const objectProperty = definitionObject[key]
20
30
 
21
31
  nameLookup.set(objectProperty, key)
22
32
 
23
33
  if (!isFunction(objectProperty)) {
24
- continue
34
+ throw new Error(`Grammar definition contains a property '${key}', which is not a function. All object properties must be productions, declared as functions.`)
25
35
  }
26
36
 
27
- const productionContent = objectProperty.call(obj)
37
+ const productionContent = objectProperty.call(definitionObject)
28
38
  const normalizedProduction = productionToGrammarElement(productionContent)
39
+ const isUnwrappedNonterminal = options.unwrappedNonterminalNames?.includes(key) === true
29
40
 
30
- const newNonterminal = nonterminal(key, normalizedProduction)
31
- const newOptionalNonterminal = { ...newNonterminal, optional: true }
41
+ const newNonterminal = nonterminal(key, normalizedProduction, isUnwrappedNonterminal)
42
+ const newOptionalNonterminal = {
43
+ ...newNonterminal,
44
+ optional: true,
45
+ grammarNonterminal: newNonterminal,
46
+ }
32
47
 
33
48
  nonterminalLookup.set(objectProperty, newNonterminal)
34
49
  optionalNonterminalLookup.set(objectProperty, newOptionalNonterminal)
@@ -55,7 +70,7 @@ export function buildGrammar<T extends { [key: string]: any }>(obj: T | (new ()
55
70
  optionalNonterminal.content = preparedContent
56
71
  }
57
72
 
58
- const startNonterminal = nonterminalLookup.get(obj[startProductionName] as Function)
73
+ const startNonterminal = nonterminalLookup.get(definitionObject[startProductionName] as Function)
59
74
 
60
75
  if (!startNonterminal) {
61
76
  throw new Error(`Couldn't find a start production named '${startProductionName as string}'.`)
@@ -142,6 +157,8 @@ function prepareGrammarElement(
142
157
 
143
158
  cached: true,
144
159
  cacheId: getNewCacheId(),
160
+
161
+ grammarNonterminal: nonterminal.grammarNonterminal ?? nonterminal
145
162
  }
146
163
  } else {
147
164
  return nonterminal
@@ -154,255 +171,7 @@ function prepareGrammarElement(
154
171
  }
155
172
 
156
173
  /////////////////////////////////////////////////////////////////////////////////////////////////
157
- // Internal static analysis methods
158
- /////////////////////////////////////////////////////////////////////////////////////////////////
159
- function detectAndAnnotateOptionalNodes(rootNode: GrammarElement) {
160
- const visitedNodes = new Set<GrammarElement>()
161
-
162
- const resolvedNodes = new Map<GrammarElement, boolean>()
163
- const unresolvedNodes = new Map<GrammarElement, { dependencies: Set<GrammarElement>, isChoice: boolean }>()
164
-
165
- function processDepthFirst(node: GrammarElement): boolean | undefined {
166
- if (visitedNodes.has(node)) {
167
- return resolvedNodes.get(node)
168
- }
169
-
170
- visitedNodes.add(node)
171
-
172
- switch (node.type) {
173
- case 'StringTerminal':
174
- case 'PatternTerminal': {
175
- resolvedNodes.set(node, node.optional)
176
-
177
- return node.optional
178
- }
179
-
180
- case 'Nonterminal':
181
- case 'Repetition': {
182
- const result = processDepthFirst(node.content)
183
-
184
- if (node.optional) {
185
- resolvedNodes.set(node, true)
186
-
187
- return true
188
- } else if (isBoolean(result)) {
189
- resolvedNodes.set(node, result)
190
-
191
- return result
192
- } else {
193
- unresolvedNodes.set(node, { dependencies: new Set([node.content]), isChoice: false })
194
-
195
- return undefined
196
- }
197
- }
198
-
199
- case 'Sequence': {
200
- const dependencies = new Set<GrammarElement>()
201
-
202
- let hasNonOptionalResolvedMember = false
203
-
204
- for (const element of node.members) {
205
- const result = processDepthFirst(element)
206
-
207
- if (isBoolean(result)) {
208
- if (result === false) {
209
- hasNonOptionalResolvedMember = true
210
- }
211
- } else {
212
- dependencies.add(element)
213
- }
214
- }
215
-
216
- if (node.optional == true) {
217
- resolvedNodes.set(node, true)
218
-
219
- return true
220
- } else if (hasNonOptionalResolvedMember) {
221
- resolvedNodes.set(node, false)
222
-
223
- return false
224
- } else if (dependencies.size == 0) {
225
- resolvedNodes.set(node, true)
226
-
227
- return true
228
- } else {
229
- unresolvedNodes.set(node, { dependencies, isChoice: false })
230
-
231
- return undefined
232
- }
233
- }
234
-
235
- case 'Choice': {
236
- const dependencies = new Set<GrammarElement>()
237
- let hasOptionalResolvedMember = false
238
-
239
- for (const element of node.members) {
240
- const result = processDepthFirst(element)
241
-
242
- if (isBoolean(result)) {
243
- if (result === true) {
244
- hasOptionalResolvedMember = true
245
- }
246
- } else {
247
- dependencies.add(element)
248
- }
249
- }
250
-
251
- if (node.optional == true) {
252
- resolvedNodes.set(node, true)
253
-
254
- return true
255
- } else if (hasOptionalResolvedMember) {
256
- resolvedNodes.set(node, true)
257
-
258
- return true
259
- } else if (dependencies.size == 0) {
260
- resolvedNodes.set(node, false)
261
-
262
- return false
263
- } else {
264
- unresolvedNodes.set(node, { dependencies, isChoice: true })
265
-
266
- return undefined
267
- }
268
- }
269
- }
270
-
271
- return undefined
272
- }
273
-
274
- // Process depth first to resolve the easy cases, for productions that contain
275
- // no cyclic references:
276
- processDepthFirst(rootNode)
277
-
278
- // Now the remainder consists of nodes containing cyclic references that have not yet been resolved.
279
- // Use a form of iterative elimination and substitution to resolve them:
280
- while (unresolvedNodes.size > 0) {
281
- // This variable tracks whether at least one dependency was resolved, in any node.
282
- // If it stays false, it means that no improvement was made during the iteration,
283
- // and we should exit the loop.
284
- let atLastOneDependencyResolvedInAnyNode = false
285
- const nodesToDelete: GrammarElement[] = []
286
-
287
- // Scan the unresolved nodes to locate any new resolved dependencies
288
- for (const [node, { dependencies, isChoice }] of unresolvedNodes) {
289
- let nodeResolved = false
290
-
291
- // Iterate over all unresolved dependencies for the node
292
- for (const dependency of Array.from(dependencies)) {
293
- // Check if the dependency has been resolved
294
- const value = resolvedNodes.get(dependency)
295
-
296
- if (value !== undefined) {
297
- // If it did, record that some dependencies were resolved
298
- atLastOneDependencyResolvedInAnyNode = true
299
-
300
- if (isChoice) {
301
- if (value === true) {
302
- resolvedNodes.set(node, true)
303
- nodeResolved = true
304
- break
305
- }
306
- } else {
307
- if (value === false) {
308
- resolvedNodes.set(node, false)
309
- nodeResolved = true
310
- break
311
- }
312
- }
313
-
314
- dependencies.delete(dependency)
315
- }
316
- }
317
-
318
- if (nodeResolved) {
319
- nodesToDelete.push(node)
320
- } else if (dependencies.size === 0) {
321
- if (isChoice) {
322
- resolvedNodes.set(node, false)
323
- } else {
324
- resolvedNodes.set(node, true)
325
- }
326
-
327
- nodesToDelete.push(node)
328
- }
329
- }
330
-
331
- for (const node of nodesToDelete) {
332
- unresolvedNodes.delete(node)
333
- }
334
-
335
- // If not even one dependency was eliminated for any node,
336
- // it means that only mutually cyclic nodes are left unresolved, so exit the loop.
337
- if (!atLastOneDependencyResolvedInAnyNode) {
338
- break
339
- }
340
- }
341
-
342
- // All remaining unresolved nodes must now be optional,
343
- // since they are all mutually cyclic and all their non-cyclic grammar elements are known to be optional.
344
- for (const node of unresolvedNodes.keys()) {
345
- resolvedNodes.set(node, true)
346
- unresolvedNodes.delete(node)
347
- }
348
-
349
- // Finally set the 'optional' property of all nodes based on the detected values.
350
- for (const [node, isOptional] of resolvedNodes) {
351
- node.optional = isOptional
352
- }
353
- }
354
-
355
- function detectAndErrorOnLeftRecursion(rootNode: GrammarElement) {
356
- const currentlyIteratedNodes = new Set<GrammarElement>()
357
-
358
- function detect(node: GrammarElement) {
359
- if (currentlyIteratedNodes.has(node)) {
360
- if (node.type === 'Nonterminal') {
361
- throw new Error(`Detected left recursion for nonterminal '${node.name}'.`)
362
- } else {
363
- throw new Error(`Detected left recursion for node: ${JSON.stringify(node, undefined, 4)}`)
364
- }
365
- }
366
-
367
- currentlyIteratedNodes.add(node)
368
-
369
- switch (node.type) {
370
- case 'Nonterminal':
371
- case 'Repetition': {
372
- detect(node.content)
373
-
374
- break
375
- }
376
-
377
- case 'Sequence': {
378
- for (const member of node.members) {
379
- detect(member)
380
-
381
- if (!member.optional) {
382
- break
383
- }
384
- }
385
-
386
- break
387
- }
388
-
389
- case 'Choice': {
390
- for (const member of node.members) {
391
- detect(member)
392
- }
393
-
394
- break
395
- }
396
- }
397
-
398
- currentlyIteratedNodes.delete(node)
399
- }
400
-
401
- detect(rootNode)
402
- }
403
-
404
- /////////////////////////////////////////////////////////////////////////////////////////////////
405
- // Exported builder methods
174
+ // Exported AST builder functions
406
175
  /////////////////////////////////////////////////////////////////////////////////////////////////
407
176
  export function zeroOrMore(content: Production): Repetition {
408
177
  return {
@@ -423,7 +192,7 @@ export function oneOrMore(content: Production): Repetition {
423
192
  }
424
193
 
425
194
  export function anyOf(...members: Production[]): Choice {
426
- if (members.length == 0) {
195
+ if (members.length === 0) {
427
196
  throw new Error(`'anyOf' requires at least one member.`)
428
197
  }
429
198
 
@@ -439,7 +208,7 @@ export function anyOf(...members: Production[]): Choice {
439
208
  }
440
209
 
441
210
  export function bestOf(...members: Production[]): Choice {
442
- if (members.length == 0) {
211
+ if (members.length === 0) {
443
212
  throw new Error(`'bestOf' requires at least one member.`)
444
213
  }
445
214
 
@@ -466,6 +235,8 @@ export function pattern(pattern: Pattern): PatternTerminal {
466
235
  pattern = [inputStart, pattern]
467
236
  }
468
237
 
238
+ validatePatternCaptureGroups(pattern)
239
+
469
240
  const regExp = buildRegExp(pattern)
470
241
  const optional = isPatternOptional(pattern)
471
242
 
@@ -488,7 +259,7 @@ export function uncached<T extends Production>(content: Production): T {
488
259
  }
489
260
 
490
261
  /////////////////////////////////////////////////////////////////////////////////////////////////
491
- // Internal builder methods
262
+ // Internal AST builder methods
492
263
  /////////////////////////////////////////////////////////////////////////////////////////////////
493
264
  function stringTerminal(content: string): StringTerminal {
494
265
  if (content.length < 1) {
@@ -504,7 +275,7 @@ function stringTerminal(content: string): StringTerminal {
504
275
  }
505
276
  }
506
277
 
507
- function nonterminal(name: string, content: GrammarElement): Nonterminal {
278
+ function nonterminal(name: string, content: GrammarElement, unwrapped: boolean): Nonterminal {
508
279
  if (name.length < 1) {
509
280
  throw new Error(`A nonterminal name must include at least 1 character.`)
510
281
  }
@@ -513,9 +284,10 @@ function nonterminal(name: string, content: GrammarElement): Nonterminal {
513
284
  type: 'Nonterminal',
514
285
  name,
515
286
  content,
516
- optional: false,
517
287
 
288
+ optional: false,
518
289
  cached: false,
290
+ unwrapped,
519
291
  }
520
292
  }
521
293
 
@@ -554,7 +326,7 @@ function productionToGrammarElement(production: Production): GrammarElement {
554
326
  }
555
327
 
556
328
  /////////////////////////////////////////////////////////////////////////////////////////////////
557
- // Type definitions
329
+ // Grammar class
558
330
  /////////////////////////////////////////////////////////////////////////////////////////////////
559
331
  export class Grammar<T> {
560
332
  readonly productions: Record<keyof T, any>
@@ -577,6 +349,9 @@ export class Grammar<T> {
577
349
  }
578
350
  }
579
351
 
352
+ /////////////////////////////////////////////////////////////////////////////////////////////////
353
+ // Type definitions
354
+ /////////////////////////////////////////////////////////////////////////////////////////////////
580
355
  export type Production = string | GrammarElement | (() => Production) | Production[]
581
356
 
582
357
  export type GrammarElement =
@@ -594,6 +369,10 @@ interface GrammarElementBase {
594
369
 
595
370
  cached: boolean
596
371
  cacheId?: number
372
+
373
+ // The name of the grammar property this element was assigned to, if any.
374
+ // Used by the parser to refer to the element by name in error messages.
375
+ name?: string
597
376
  }
598
377
 
599
378
  export type Terminal = StringTerminal | PatternTerminal
@@ -613,6 +392,12 @@ export interface Nonterminal extends GrammarElementBase {
613
392
  type: 'Nonterminal'
614
393
  name: string
615
394
  content: GrammarElement
395
+ unwrapped: boolean
396
+
397
+ // The canonical nonterminal as defined in the grammar. Clones created for
398
+ // optional references and cached references keep a reference to the original,
399
+ // so identity comparisons work across clones.
400
+ grammarNonterminal?: Nonterminal
616
401
  }
617
402
 
618
403
  export interface Sequence extends GrammarElementBase {
@@ -635,3 +420,12 @@ export interface NonterminalReference extends GrammarElementBase {
635
420
  type: 'NonterminalReference'
636
421
  reference: Function
637
422
  }
423
+
424
+ /////////////////////////////////////////////////////////////////////////////////////////////////
425
+ // Builder type definitions
426
+ /////////////////////////////////////////////////////////////////////////////////////////////////
427
+ export interface GrammarBuilderOptions<T> {
428
+ unwrappedNonterminalNames?: GrammarNonterminalNames<T>
429
+ }
430
+
431
+ export type GrammarNonterminalNames<T> = (keyof T)[]