grammar-composer 0.4.0 → 0.6.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 (60) 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/dist/utilities/Timer.d.ts +7 -3
  45. package/dist/utilities/Timer.d.ts.map +1 -1
  46. package/dist/utilities/Timer.js +41 -39
  47. package/dist/utilities/Timer.js.map +1 -1
  48. package/package.json +3 -3
  49. package/src/exports/Exports.ts +2 -0
  50. package/src/parser-generator/Grammar.ts +59 -265
  51. package/src/parser-generator/ParseError.ts +337 -0
  52. package/src/parser-generator/StaticAnalysis.ts +340 -0
  53. package/src/parser-generator/TerminalToText.ts +196 -0
  54. package/src/parser-generator/TopDownParser.ts +67 -49
  55. package/src/tests/Test.ts +26 -24
  56. package/src/tests/test-grammars/JsonGrammar.ts +28 -10
  57. package/src/tests/test-grammars/RegExpGrammar.ts +65 -46
  58. package/src/tests/test-grammars/XmlGrammar.ts +20 -8
  59. package/src/utilities/LineAndColumn.ts +61 -0
  60. package/src/utilities/Timer.ts +50 -48
@@ -5,7 +5,14 @@ export class RegExpGrammar {
5
5
  //////////////////////////////////////////////////////////////////////////////////////////////
6
6
  // High-level productions
7
7
  //////////////////////////////////////////////////////////////////////////////////////////////
8
- root = () => this.sequenceOrDisjunction
8
+ root = () => this.disjunctionOrSequence
9
+
10
+ disjunctionOrSequence = () => [
11
+ G.anyOf(
12
+ this.disjunction,
13
+ this.sequence,
14
+ )
15
+ ]
9
16
 
10
17
  disjunction = () => [
11
18
  this.sequence,
@@ -20,11 +27,27 @@ export class RegExpGrammar {
20
27
  G.zeroOrMore(this.sequenceElement)
21
28
  )
22
29
 
23
- sequenceOrDisjunction = [
30
+ sequenceElement = () => G.anyOf(
31
+ this.anchor,
32
+ this.lookaround,
33
+ this.quantifiedExpression,
34
+ this.quantifiableExpression,
35
+ )
36
+
37
+ //////////////////////////////////////////////////////////////////////////////////////////////
38
+ // Quantifiable expression
39
+ //////////////////////////////////////////////////////////////////////////////////////////////
40
+ quantifiableExpression = () => G.cached(
24
41
  G.anyOf(
25
- this.disjunction,
26
- this.sequence,
42
+ this.group,
43
+ this.backReference,
44
+ this.singleCharExpression
27
45
  )
46
+ )
47
+
48
+ quantifiedExpression = () => [
49
+ this.quantifiableExpression,
50
+ this.quantifier,
28
51
  ]
29
52
 
30
53
  //////////////////////////////////////////////////////////////////////////////////////////////
@@ -86,7 +109,7 @@ export class RegExpGrammar {
86
109
  //////////////////////////////////////////////////////////////////////////////////////////////
87
110
  uncapturedGroup = () => [
88
111
  '(?:',
89
- this.sequenceOrDisjunction,
112
+ this.disjunctionOrSequence,
90
113
  ')',
91
114
  ]
92
115
 
@@ -96,17 +119,17 @@ export class RegExpGrammar {
96
119
  R.oneOrMore(identifierChar))
97
120
  ),
98
121
  '>',
99
- this.sequenceOrDisjunction,
122
+ this.disjunctionOrSequence,
100
123
  ')',
101
124
  ]
102
125
 
103
126
  unnamedCaptureGroup = () => [
104
127
  '(',
105
- this.sequenceOrDisjunction,
128
+ this.disjunctionOrSequence,
106
129
  ')',
107
130
  ]
108
131
 
109
- group = G.anyOf(
132
+ group = () => G.anyOf(
110
133
  this.uncapturedGroup,
111
134
  this.namedCaptureGroup,
112
135
  this.unnamedCaptureGroup,
@@ -132,7 +155,7 @@ export class RegExpGrammar {
132
155
  '>',
133
156
  ])
134
157
 
135
- backReference = G.anyOf(
158
+ backReference = () => G.anyOf(
136
159
  this.unnamedBackreference,
137
160
  this.namedBackreference,
138
161
  )
@@ -142,29 +165,29 @@ export class RegExpGrammar {
142
165
  //////////////////////////////////////////////////////////////////////////////////////////////
143
166
  positiveLookahead = () => [
144
167
  '(?=',
145
- this.sequenceOrDisjunction,
168
+ this.disjunctionOrSequence,
146
169
  ')',
147
170
  ]
148
171
 
149
172
  negativeLookahead = () => [
150
173
  '(?!',
151
- this.sequenceOrDisjunction,
174
+ this.disjunctionOrSequence,
152
175
  ')',
153
176
  ]
154
177
 
155
178
  positiveLookbehind = () => [
156
179
  '(?<=',
157
- this.sequenceOrDisjunction,
180
+ this.disjunctionOrSequence,
158
181
  ')',
159
182
  ]
160
183
 
161
184
  negativeLookbehind = () => [
162
185
  '(?<!',
163
- this.sequenceOrDisjunction,
186
+ this.disjunctionOrSequence,
164
187
  ')',
165
188
  ]
166
189
 
167
- lookaround = G.anyOf(
190
+ lookaround = () => G.anyOf(
168
191
  this.positiveLookahead,
169
192
  this.negativeLookahead,
170
193
  this.positiveLookbehind,
@@ -244,17 +267,17 @@ export class RegExpGrammar {
244
267
  '}',
245
268
  ])
246
269
 
247
- unicodeProperty = () => G.pattern([
270
+ unicodeProperty = () => [
248
271
  '\\p',
249
272
  this.unicodePropertyBodyPattern,
250
- ])
273
+ ]
251
274
 
252
- notUnicodeProperty = () => G.pattern([
275
+ notUnicodeProperty = () => [
253
276
  '\\P',
254
277
  this.unicodePropertyBodyPattern,
255
- ])
278
+ ]
256
279
 
257
- unicodePropertyBodyPattern: R.Pattern = [
280
+ unicodePropertyBodyPattern = () => G.pattern([
258
281
  '{',
259
282
  R.captureAs('property',
260
283
  R.oneOrMore(R.anyOf(letter, digit, '_', '-'))
@@ -266,7 +289,7 @@ export class RegExpGrammar {
266
289
  ),
267
290
  ]),
268
291
  '}',
269
- ]
292
+ ])
270
293
 
271
294
  charClass = () => [
272
295
  '[',
@@ -283,7 +306,7 @@ export class RegExpGrammar {
283
306
  ']',
284
307
  ]
285
308
 
286
- charcodeOrEscapedChar = G.anyOf(
309
+ charcodeOrEscapedChar = () => G.anyOf(
287
310
  this.codepoint,
288
311
  this.hexCharcode,
289
312
  this.controlCharcode,
@@ -297,7 +320,7 @@ export class RegExpGrammar {
297
320
  )
298
321
  )
299
322
 
300
- charRangeElement = G.anyOf(
323
+ charRangeElement = () => G.anyOf(
301
324
  this.charcodeOrEscapedChar,
302
325
  this.charClassLiteral,
303
326
  )
@@ -321,7 +344,8 @@ export class RegExpGrammar {
321
344
 
322
345
  anyCharWildcard = () => '.'
323
346
 
324
- anchor = G.anyOf(this.inputStartAnchor, this.inputEndAnchor)
347
+ anchor = () =>
348
+ G.anyOf(this.inputStartAnchor, this.inputEndAnchor)
325
349
 
326
350
  escapedCharacterClass = () => G.pattern(R.anyOf(
327
351
  R.captureAs('whitespace', escapedChars.whitespace),
@@ -339,29 +363,6 @@ export class RegExpGrammar {
339
363
  R.captureAs('verticalTab', escapedChars.verticalTab),
340
364
  R.captureAs('backwardSlash', escapedChars.backwardSlash),
341
365
  ))
342
-
343
- //////////////////////////////////////////////////////////////////////////////////////////////
344
- // Sequence elements (positioned here due to TypeScript class member ordering requirements)
345
- //////////////////////////////////////////////////////////////////////////////////////////////
346
- quantifiableExpression = G.cached(
347
- G.anyOf(
348
- this.group,
349
- this.backReference,
350
- this.singleCharExpression
351
- )
352
- )
353
-
354
- quantifiedExpression = () => [
355
- this.quantifiableExpression,
356
- this.quantifier,
357
- ]
358
-
359
- sequenceElement = G.anyOf(
360
- this.anchor,
361
- this.lookaround,
362
- this.quantifiedExpression,
363
- this.quantifiableExpression,
364
- )
365
366
  }
366
367
 
367
368
  //////////////////////////////////////////////////////////////////////////////////////////////
@@ -391,3 +392,21 @@ const escapedChars = {
391
392
  verticalTab: '\\v',
392
393
  backwardSlash: '\\\\'
393
394
  }
395
+
396
+ //////////////////////////////////////////////////////////////////////////////////////////////
397
+ // Wrapped nonterminal names
398
+ //////////////////////////////////////////////////////////////////////////////////////////////
399
+ export const regExpGrammarUnwrappedNonterminalNames: G.GrammarNonterminalNames<RegExpGrammar> = [
400
+ 'disjunctionOrSequence',
401
+ 'group',
402
+ 'backReference',
403
+ 'lookaround',
404
+ 'unicodeProperty',
405
+ 'notUnicodeProperty',
406
+ 'unicodePropertyBodyPattern',
407
+ 'charcodeOrEscapedChar',
408
+ 'charRangeElement',
409
+ 'anchor',
410
+ 'quantifiableExpression',
411
+ 'sequenceElement',
412
+ ]
@@ -14,7 +14,7 @@ export class XmlGrammar {
14
14
  )
15
15
  ]
16
16
 
17
- textFragment = G.pattern([
17
+ textFragment = () => G.pattern([
18
18
  R.oneOrMore(R.notAnyOfChars('<'))
19
19
  ])
20
20
 
@@ -26,7 +26,7 @@ export class XmlGrammar {
26
26
  this.tagEnd
27
27
  ]
28
28
 
29
- openingTagStart = G.pattern([
29
+ openingTagStart = () => G.pattern([
30
30
  '<',
31
31
 
32
32
  R.possibly('?'),
@@ -38,7 +38,7 @@ export class XmlGrammar {
38
38
  R.zeroOrMore(R.whitespace),
39
39
  ])
40
40
 
41
- tagEnd = G.pattern([
41
+ tagEnd = () => G.pattern([
42
42
  R.zeroOrMore(R.whitespace),
43
43
 
44
44
  R.possibly(R.anyOf('/', '?')),
@@ -46,7 +46,7 @@ export class XmlGrammar {
46
46
  '>'
47
47
  ])
48
48
 
49
- attribute = G.pattern([
49
+ attribute = () => G.pattern([
50
50
  R.zeroOrMore(R.whitespace),
51
51
 
52
52
  R.captureAs('attributeName',
@@ -66,7 +66,7 @@ export class XmlGrammar {
66
66
  ])
67
67
  ])
68
68
 
69
- closingTag = G.pattern([
69
+ closingTag = () => G.pattern([
70
70
  '</',
71
71
 
72
72
  R.zeroOrMore(R.whitespace),
@@ -88,7 +88,7 @@ export class XmlGrammar {
88
88
  this.tagEnd
89
89
  ]
90
90
 
91
- declarationTagOpening = G.pattern([
91
+ declarationTagOpening = () => G.pattern([
92
92
  '<!',
93
93
 
94
94
  R.captureAs('tagName',
@@ -98,7 +98,7 @@ export class XmlGrammar {
98
98
  R.zeroOrMore(R.whitespace)
99
99
  ])
100
100
 
101
- declarationTagAttribute = G.pattern([
101
+ declarationTagAttribute = () => G.pattern([
102
102
  R.zeroOrMore(R.whitespace),
103
103
 
104
104
  R.anyOf(
@@ -112,7 +112,7 @@ export class XmlGrammar {
112
112
  R.zeroOrMore(R.whitespace),
113
113
  ])
114
114
 
115
- comment = G.pattern([
115
+ comment = () => G.pattern([
116
116
  '<!--',
117
117
 
118
118
  R.captureAs('commentBody',
@@ -123,6 +123,9 @@ export class XmlGrammar {
123
123
  ])
124
124
  }
125
125
 
126
+ //////////////////////////////////////////////////////////////////////////////////////////////
127
+ // Helper regular expressions
128
+ //////////////////////////////////////////////////////////////////////////////////////////////
126
129
  const quotedString = R.anyOf(
127
130
  [
128
131
  '"',
@@ -139,3 +142,12 @@ const quotedString = R.anyOf(
139
142
  "'"
140
143
  ],
141
144
  )
145
+
146
+ //////////////////////////////////////////////////////////////////////////////////////////////
147
+ // Wrapped nonterminal names
148
+ //////////////////////////////////////////////////////////////////////////////////////////////
149
+ export const xmlGrammarUnwrappedNonterminalNames: G.GrammarNonterminalNames<XmlGrammar> = [
150
+ 'openingTagStart',
151
+ 'tagEnd',
152
+ 'declarationTagOpening',
153
+ ]
@@ -0,0 +1,61 @@
1
+ /////////////////////////////////////////////////////////////////////////////////////////////////
2
+ // Position helpers
3
+ /////////////////////////////////////////////////////////////////////////////////////////////////
4
+ export function getLineAndColumn(input: string, offset: number): { line: number, column: number } {
5
+ let line = 1
6
+ let lineStartOffset = 0
7
+
8
+ for (let i = 0; i < offset && i < input.length; i++) {
9
+ if (input[i] === '\n') {
10
+ line += 1
11
+ lineStartOffset = i + 1
12
+ }
13
+ }
14
+
15
+ return {
16
+ line,
17
+ column: offset - lineStartOffset + 1
18
+ }
19
+ }
20
+
21
+ const tabSize = 4
22
+
23
+ export function expandTabs(text: string): string {
24
+ let result = ''
25
+ let column = 0
26
+
27
+ for (const char of text) {
28
+ if (char === '\t') {
29
+ const spacesToNextTabStop = tabSize - (column % tabSize)
30
+
31
+ result += ' '.repeat(spacesToNextTabStop)
32
+ column += spacesToNextTabStop
33
+ } else {
34
+ result += char
35
+ column += 1
36
+ }
37
+ }
38
+
39
+ return result
40
+ }
41
+
42
+ export function buildCaretSpacing(text: string, caretColumn: number): string {
43
+ let spacing = ''
44
+ let column = 0
45
+
46
+ for (let i = 0; i < caretColumn - 1 && i < text.length; i++) {
47
+ const char = text[i]
48
+
49
+ if (char === '\t') {
50
+ const spacesToNextTabStop = tabSize - (column % tabSize)
51
+
52
+ spacing += ' '.repeat(spacesToNextTabStop)
53
+ column += spacesToNextTabStop
54
+ } else {
55
+ spacing += ' '
56
+ column += 1
57
+ }
58
+ }
59
+
60
+ return spacing
61
+ }
@@ -1,95 +1,97 @@
1
- import { roundToDigits } from './Utilities.js'
1
+ export class Timer {
2
+ private readonly logger: TimerLogger
2
3
 
3
- declare const chrome: any
4
- declare const process: any
4
+ private startTime = 0
5
5
 
6
- export class Timer {
7
- startTime = 0
6
+ constructor(logger?: TimerLogger) {
7
+ if (logger) {
8
+ this.logger = logger
9
+ } else {
10
+ this.logger = console.log
11
+ }
8
12
 
9
- constructor() {
10
13
  this.restart()
11
14
  }
12
15
 
13
- restart() {
16
+ // Resets the timer to the current time.
17
+ restart(): void {
14
18
  this.startTime = Timer.currentTime
15
19
  }
16
20
 
21
+ // Elapsed time in milliseconds (monotonic where supported).
17
22
  get elapsedTime(): number {
18
- // Elapsed time (milliseconds)
19
23
  return Timer.currentTime - this.startTime
20
24
  }
21
25
 
26
+ // Elapsed time in seconds.
22
27
  get elapsedTimeSeconds(): number {
23
- // Elapsed time (seconds)
24
28
  return this.elapsedTime / 1000
25
29
  }
26
30
 
31
+ // Returns elapsed ms and restarts the timer.
27
32
  getElapsedTimeAndRestart(): number {
28
- const elapsedTime = this.elapsedTime
33
+ const elapsed = this.elapsedTime
29
34
  this.restart()
30
35
 
31
- return elapsedTime
36
+ return elapsed
32
37
  }
33
38
 
39
+ // Logs elapsed time (in ms) and restarts the timer.
34
40
  logAndRestart(title: string, timePrecision = 3): number {
35
- const elapsedTime = this.elapsedTime
36
-
37
- //
38
- const message = `${title}: ${roundToDigits(elapsedTime, timePrecision)}ms`
39
-
40
- console.log(message)
41
- //
42
-
41
+ const elapsedMs = this.elapsedTime
42
+ this.logger(`${title}: ${roundToDigits(elapsedMs, timePrecision)}ms`)
43
43
  this.restart()
44
44
 
45
- return elapsedTime
45
+ return elapsedMs
46
46
  }
47
47
 
48
+ // Current high-resolution timestamp in milliseconds since Unix epoch.
48
49
  static get currentTime(): number {
49
- if (!this.timestampFunc) {
50
- this.createGlobalTimestampFunction()
51
- }
52
-
53
50
  return this.timestampFunc()
54
51
  }
55
52
 
53
+ // Current timestamp in microseconds (integer).
56
54
  static get microsecondTimestamp(): number {
57
55
  return Math.floor(Timer.currentTime * 1000)
58
56
  }
59
57
 
60
- private static createGlobalTimestampFunction() {
61
- if (typeof process === 'object' && typeof process.hrtime === 'function') {
62
- let baseTimestamp = 0
58
+ // Private Static Clock Setup
59
+ private static timestampFunc: () => number = Timer.createTimestampFunction()
63
60
 
64
- this.timestampFunc = () => {
65
- const nodeTimeStamp = process.hrtime()
66
- const millisecondTime = (nodeTimeStamp[0] * 1000) + (nodeTimeStamp[1] / 1000000)
61
+ private static createTimestampFunction(): () => number {
62
+ const g = globalThis as any
67
63
 
68
- return baseTimestamp + millisecondTime
69
- }
64
+ // 1. Modern standard: performance.now() (Browsers & Node 16+)
65
+ if (typeof g.performance === 'object' && typeof g.performance.now === 'function') {
66
+ const timeOrigin =
67
+ g.performance.timeOrigin ?? (Date.now() - g.performance.now())
70
68
 
71
- baseTimestamp = Date.now() - this.timestampFunc()
69
+ return () => timeOrigin + g.performance.now()
72
70
  }
73
- else if (typeof chrome === 'object' && chrome.Interval) {
74
- const baseTimestamp = Date.now()
75
71
 
76
- const chromeIntervalObject = new chrome.Interval()
77
- chromeIntervalObject.start()
72
+ // 2. Node.js high resolution timer (BigInt variant, Node 10.4+)
73
+ if (typeof g.process === 'object' && typeof g.process.hrtime === 'function') {
74
+ const startNs = g.process.hrtime.bigint()
78
75
 
79
- this.timestampFunc = () => baseTimestamp + chromeIntervalObject.microseconds() / 1000
80
- }
81
- else if (typeof performance === 'object' && performance.now) {
82
- const baseTimestamp = Date.now() - performance.now()
76
+ const epochBaseMs = Date.now() - (Number(startNs) / 1e6)
83
77
 
84
- this.timestampFunc = () => baseTimestamp + performance.now()
85
- }
86
- else if (Date.now) {
87
- this.timestampFunc = () => Date.now()
78
+ return () =>
79
+ epochBaseMs + Number(g.process.hrtime.bigint()) / 1e6
88
80
  }
89
- else {
90
- this.timestampFunc = () => (new Date()).getTime()
81
+
82
+ // 3. Last-resort fallback (non-monotonic)
83
+ if (typeof Date.now === 'function') {
84
+ return () => Date.now()
91
85
  }
86
+
87
+ return () => new Date().getTime()
92
88
  }
89
+ }
90
+
91
+ export function roundToDigits(value: number, digits: number): number {
92
+ const factor = 10 ** digits
93
93
 
94
- private static timestampFunc: () => number
94
+ return Math.round(value * factor) / factor
95
95
  }
96
+
97
+ type TimerLogger = (msg: string) => void