@thomasfosterau/effect-parse 0.4.1 → 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 (157) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +56 -14
  3. package/dist/Ascii.d.ts +11 -4
  4. package/dist/Ascii.d.ts.map +1 -1
  5. package/dist/Ascii.js +12 -4
  6. package/dist/Ascii.js.map +1 -1
  7. package/dist/Binary.d.ts +5 -2
  8. package/dist/Binary.d.ts.map +1 -1
  9. package/dist/Binary.js +83 -37
  10. package/dist/Binary.js.map +1 -1
  11. package/dist/Bits.d.ts.map +1 -1
  12. package/dist/Bits.js +11 -4
  13. package/dist/Bits.js.map +1 -1
  14. package/dist/Compiler.d.ts +74 -2
  15. package/dist/Compiler.d.ts.map +1 -1
  16. package/dist/Compiler.js +78 -1
  17. package/dist/Compiler.js.map +1 -1
  18. package/dist/Expression.d.ts.map +1 -1
  19. package/dist/Expression.js +86 -74
  20. package/dist/Expression.js.map +1 -1
  21. package/dist/Input.d.ts +56 -5
  22. package/dist/Input.d.ts.map +1 -1
  23. package/dist/Input.js +70 -1
  24. package/dist/Input.js.map +1 -1
  25. package/dist/Nom.d.ts +5 -15
  26. package/dist/Nom.d.ts.map +1 -1
  27. package/dist/Nom.js.map +1 -1
  28. package/dist/ParseError.d.ts +69 -11
  29. package/dist/ParseError.d.ts.map +1 -1
  30. package/dist/ParseError.js +129 -28
  31. package/dist/ParseError.js.map +1 -1
  32. package/dist/Parser.d.ts +375 -29
  33. package/dist/Parser.d.ts.map +1 -1
  34. package/dist/Parser.js +394 -37
  35. package/dist/Parser.js.map +1 -1
  36. package/dist/ParserSchema.d.ts.map +1 -1
  37. package/dist/ParserSchema.js +2 -3
  38. package/dist/ParserSchema.js.map +1 -1
  39. package/dist/ParserStream.d.ts +122 -14
  40. package/dist/ParserStream.d.ts.map +1 -1
  41. package/dist/ParserStream.js +293 -62
  42. package/dist/ParserStream.js.map +1 -1
  43. package/dist/Syntax.d.ts +23 -5
  44. package/dist/Syntax.d.ts.map +1 -1
  45. package/dist/Syntax.js +1 -1
  46. package/dist/Syntax.js.map +1 -1
  47. package/dist/Text.d.ts.map +1 -1
  48. package/dist/Text.js +21 -15
  49. package/dist/Text.js.map +1 -1
  50. package/dist/Token.d.ts +65 -11
  51. package/dist/Token.d.ts.map +1 -1
  52. package/dist/Token.js +73 -26
  53. package/dist/Token.js.map +1 -1
  54. package/dist/internal/compiler/closure.d.ts +1 -1
  55. package/dist/internal/compiler/closure.d.ts.map +1 -1
  56. package/dist/internal/compiler/closure.js +263 -113
  57. package/dist/internal/compiler/closure.js.map +1 -1
  58. package/dist/internal/compiler/entries.d.ts +3 -1
  59. package/dist/internal/compiler/entries.d.ts.map +1 -1
  60. package/dist/internal/compiler/entries.js +3 -2
  61. package/dist/internal/compiler/entries.js.map +1 -1
  62. package/dist/internal/compiler/first.d.ts.map +1 -1
  63. package/dist/internal/compiler/first.js +9 -2
  64. package/dist/internal/compiler/first.js.map +1 -1
  65. package/dist/internal/compiler/graph.d.ts +39 -0
  66. package/dist/internal/compiler/graph.d.ts.map +1 -0
  67. package/dist/internal/compiler/graph.js +78 -0
  68. package/dist/internal/compiler/graph.js.map +1 -0
  69. package/dist/internal/compiler/jit.d.ts +5 -0
  70. package/dist/internal/compiler/jit.d.ts.map +1 -1
  71. package/dist/internal/compiler/jit.js +208 -106
  72. package/dist/internal/compiler/jit.js.map +1 -1
  73. package/dist/internal/compiler/registry.d.ts +11 -3
  74. package/dist/internal/compiler/registry.d.ts.map +1 -1
  75. package/dist/internal/compiler/registry.js.map +1 -1
  76. package/dist/internal/compiler/search.d.ts +32 -0
  77. package/dist/internal/compiler/search.d.ts.map +1 -0
  78. package/dist/internal/compiler/search.js +68 -0
  79. package/dist/internal/compiler/search.js.map +1 -0
  80. package/dist/internal/compiler/sets.d.ts.map +1 -1
  81. package/dist/internal/compiler/sets.js +1 -1
  82. package/dist/internal/compiler/sets.js.map +1 -1
  83. package/dist/internal/compiler/wasm.d.ts +8 -0
  84. package/dist/internal/compiler/wasm.d.ts.map +1 -0
  85. package/dist/internal/compiler/wasm.js +2840 -0
  86. package/dist/internal/compiler/wasm.js.map +1 -0
  87. package/dist/internal/compiler/wasmAsm.d.ts +199 -0
  88. package/dist/internal/compiler/wasmAsm.d.ts.map +1 -0
  89. package/dist/internal/compiler/wasmAsm.js +427 -0
  90. package/dist/internal/compiler/wasmAsm.js.map +1 -0
  91. package/dist/internal/entries.d.ts +3 -1
  92. package/dist/internal/entries.d.ts.map +1 -1
  93. package/dist/internal/entries.js.map +1 -1
  94. package/dist/internal/node.d.ts +77 -3
  95. package/dist/internal/node.d.ts.map +1 -1
  96. package/dist/internal/node.js +54 -10
  97. package/dist/internal/node.js.map +1 -1
  98. package/dist/internal/numbers.d.ts.map +1 -1
  99. package/dist/internal/numbers.js.map +1 -1
  100. package/dist/internal/run.d.ts +14 -0
  101. package/dist/internal/run.d.ts.map +1 -1
  102. package/dist/internal/run.js +53 -11
  103. package/dist/internal/run.js.map +1 -1
  104. package/dist/internal/stack.d.ts +14 -0
  105. package/dist/internal/stack.d.ts.map +1 -1
  106. package/dist/internal/stack.js +24 -2
  107. package/dist/internal/stack.js.map +1 -1
  108. package/dist/internal/syntax.d.ts +4 -4
  109. package/dist/internal/syntax.d.ts.map +1 -1
  110. package/dist/internal/syntax.js +18 -10
  111. package/dist/internal/syntax.js.map +1 -1
  112. package/dist/internal/text.d.ts +32 -0
  113. package/dist/internal/text.d.ts.map +1 -1
  114. package/dist/internal/text.js +68 -14
  115. package/dist/internal/text.js.map +1 -1
  116. package/dist/internal/tokens.d.ts +20 -0
  117. package/dist/internal/tokens.d.ts.map +1 -1
  118. package/dist/internal/tokens.js +72 -39
  119. package/dist/internal/tokens.js.map +1 -1
  120. package/dist/internal/tuple.d.ts +6 -0
  121. package/dist/internal/tuple.d.ts.map +1 -0
  122. package/dist/internal/tuple.js +170 -0
  123. package/dist/internal/tuple.js.map +1 -0
  124. package/package.json +18 -10
  125. package/src/Ascii.ts +47 -12
  126. package/src/Binary.ts +174 -73
  127. package/src/Bits.ts +16 -6
  128. package/src/Compiler.ts +101 -10
  129. package/src/Expression.ts +94 -81
  130. package/src/Input.ts +99 -7
  131. package/src/Nom.ts +15 -5
  132. package/src/ParseError.ts +209 -52
  133. package/src/Parser.ts +684 -143
  134. package/src/ParserSchema.ts +1 -2
  135. package/src/ParserStream.ts +361 -68
  136. package/src/Syntax.ts +60 -36
  137. package/src/Text.ts +32 -27
  138. package/src/Token.ts +152 -34
  139. package/src/internal/compiler/closure.ts +371 -153
  140. package/src/internal/compiler/entries.ts +4 -2
  141. package/src/internal/compiler/first.ts +16 -0
  142. package/src/internal/compiler/graph.ts +96 -0
  143. package/src/internal/compiler/jit.ts +319 -169
  144. package/src/internal/compiler/registry.ts +10 -2
  145. package/src/internal/compiler/search.ts +68 -0
  146. package/src/internal/compiler/sets.ts +7 -3
  147. package/src/internal/compiler/wasm.ts +3276 -0
  148. package/src/internal/compiler/wasmAsm.ts +484 -0
  149. package/src/internal/entries.ts +8 -2
  150. package/src/internal/node.ts +103 -7
  151. package/src/internal/numbers.ts +3 -1
  152. package/src/internal/run.ts +73 -12
  153. package/src/internal/stack.ts +25 -1
  154. package/src/internal/syntax.ts +39 -18
  155. package/src/internal/text.ts +97 -9
  156. package/src/internal/tokens.ts +106 -14
  157. package/src/internal/tuple.ts +141 -0
package/src/ParseError.ts CHANGED
@@ -79,37 +79,140 @@ export type ErrorKind =
79
79
  */
80
80
  export const TypeId = "~effect-parse/ParseError" as const
81
81
 
82
+ /** What a {@link ParseError} is built from. */
83
+ interface Fields<E> {
84
+ /** The input that was being parsed. */
85
+ readonly input: Input
86
+ /** The offset of the failure in the input. */
87
+ readonly offset: number
88
+ /** The kind of parser that failed. */
89
+ readonly kind: ErrorKind
90
+ /** How the error propagated before parsing stopped. */
91
+ readonly mode: ErrMode
92
+ /** Descriptions of what was expected at `offset`. */
93
+ readonly expected: ReadonlyArray<string>
94
+ /** Context labels (`Parser.context`), innermost first. */
95
+ readonly context: ReadonlyArray<string>
96
+ /** The user supplied cause, if any. */
97
+ readonly cause: E | undefined
98
+ /** An optional free-form description. */
99
+ readonly description: string | undefined
100
+ /**
101
+ * For byte inputs parsed by `Text` parsers, the text encoding. Errors
102
+ * then show the input as text, with line and column numbers.
103
+ */
104
+ readonly encoding?: Encoding<Uint8Array> | undefined
105
+ }
106
+
107
+ /**
108
+ * What a {@link ParseError} serialises as (`toJSON`): everything but the
109
+ * input it was parsing and the encoding it was read in.
110
+ *
111
+ * @since 0.6.0
112
+ * @category models
113
+ */
114
+ export interface ParseErrorJson<out E = never> {
115
+ readonly _tag: "ParseError"
116
+ /** The offset of the failure in the input. */
117
+ readonly offset: number
118
+ /** The 1-based line and column of the failure. */
119
+ readonly position: LineColumn
120
+ readonly kind: ErrorKind
121
+ readonly mode: ErrMode
122
+ readonly expected: ReadonlyArray<string>
123
+ readonly context: ReadonlyArray<string>
124
+ readonly cause: E | undefined
125
+ readonly description: string | undefined
126
+ /** The rendered message, which shows a bounded window of the input around the failure. */
127
+ readonly message: string
128
+ }
129
+
82
130
  /**
83
131
  * An error produced by running a parser.
84
132
  *
133
+ * The error keeps the whole input alive for as long as it is retained
134
+ * (`input`), but does not serialise it: `input` and `encoding` are
135
+ * accessors, not own properties, `toJSON` (which `JSON.stringify`,
136
+ * Effect's `Cause` rendering and Node's `inspect` use) leaves them out, and
137
+ * `message` shows a bounded window around the failure, never a whole line.
138
+ *
85
139
  * @since 0.1.0
86
140
  * @category models
87
141
  */
88
142
  export class ParseError<out E = never>
89
- extends /*#__PURE__*/ Data.TaggedError("ParseError")<{
90
- /** The input that was being parsed. */
91
- readonly input: Input
92
- /** The offset of the failure in the input. */
93
- readonly offset: number
94
- /** The kind of parser that failed. */
95
- readonly kind: ErrorKind
96
- /** How the error propagated before parsing stopped. */
97
- readonly mode: ErrMode
98
- /** Descriptions of what was expected at `offset`. */
99
- readonly expected: ReadonlyArray<string>
100
- /** Context labels (`Parser.context`), innermost first. */
101
- readonly context: ReadonlyArray<string>
102
- /** The user supplied cause, if any. */
103
- readonly cause: E | undefined
104
- /** An optional free-form description. */
105
- readonly description: string | undefined
106
- /**
107
- * For byte inputs parsed by `Text` parsers, the text encoding. Errors
108
- * then show the input as text, with line and column numbers.
109
- */
110
- readonly encoding?: Encoding<Uint8Array> | undefined
111
- }>
143
+ extends /*#__PURE__*/ Data.TaggedError("ParseError")<Omit<Fields<unknown>, "input" | "encoding">>
112
144
  {
145
+ /** The user supplied cause, if any. */
146
+ declare readonly cause: E | undefined
147
+
148
+ // Private fields, not properties: nothing that walks the error (its own
149
+ // keys, enumerable or not, or the arguments `Data.TaggedError` keeps for
150
+ // `toJSON` and structural equality) can reach the input (#117)
151
+ readonly #input: Input
152
+ readonly #encoding: Encoding<Uint8Array> | undefined
153
+ // Both are functions of the fields above, which never change, and both
154
+ // scan the input up to the failure: computed on first use, then kept
155
+ #position: LineColumn | undefined
156
+ #message: string | undefined
157
+
158
+ constructor(fields: Fields<E>) {
159
+ super({
160
+ offset: fields.offset,
161
+ kind: fields.kind,
162
+ mode: fields.mode,
163
+ expected: fields.expected,
164
+ context: fields.context,
165
+ cause: fields.cause,
166
+ description: fields.description
167
+ })
168
+ this.#input = fields.input
169
+ this.#encoding = fields.encoding
170
+ }
171
+
172
+ /**
173
+ * The input that was being parsed. The error holds it (for
174
+ * {@link ParseError.position} and the message) and keeps it alive, but it
175
+ * is not one of the error's own properties, so `JSON.stringify`, object
176
+ * spread, `Object.keys`, `toJSON` and loggers do not see it.
177
+ *
178
+ * @since 0.1.0
179
+ */
180
+ get input(): Input {
181
+ return this.#input
182
+ }
183
+
184
+ /**
185
+ * For byte inputs parsed by `Text` parsers, the text encoding. Errors
186
+ * then show the input as text, with line and column numbers. Like
187
+ * `input`, it is not an own property.
188
+ *
189
+ * @since 0.1.0
190
+ */
191
+ get encoding(): Encoding<Uint8Array> | undefined {
192
+ return this.#encoding
193
+ }
194
+
195
+ /**
196
+ * The error without its `input` and `encoding`: what `JSON.stringify`,
197
+ * Effect's `Cause` rendering and Node's `inspect` show.
198
+ *
199
+ * @since 0.6.0
200
+ */
201
+ override toJSON(): ParseErrorJson<E> {
202
+ return {
203
+ _tag: this._tag,
204
+ offset: this.offset,
205
+ position: this.position,
206
+ kind: this.kind,
207
+ mode: this.mode,
208
+ expected: this.expected,
209
+ context: this.context,
210
+ cause: this.cause,
211
+ description: this.description,
212
+ message: this.message
213
+ }
214
+ }
215
+
113
216
  /**
114
217
  * @since 0.1.0
115
218
  */
@@ -122,7 +225,7 @@ export class ParseError<out E = never>
122
225
  * @since 0.1.0
123
226
  */
124
227
  get position(): LineColumn {
125
- return lineColumn(this.input, this.offset, this.encoding)
228
+ return (this.#position ??= lineColumn(this.#input, this.offset, this.#encoding))
126
229
  }
127
230
 
128
231
  /**
@@ -131,7 +234,7 @@ export class ParseError<out E = never>
131
234
  * @since 0.1.0
132
235
  */
133
236
  override get message(): string {
134
- return render(this)
237
+ return (this.#message ??= render(this))
135
238
  }
136
239
 
137
240
  /**
@@ -167,6 +270,9 @@ export const fromFailure = <E = never>(
167
270
  mode,
168
271
  expected: failure.expected,
169
272
  context: failure.context,
273
+ // SAFETY: `E` is the user error type of the parser whose run recorded
274
+ // `failure`; a failure with `hasCause` carries a cause raised by that
275
+ // parser's `fail` / `mapError`
170
276
  cause: failure.hasCause ? (failure.cause as E) : undefined,
171
277
  description: failure.message,
172
278
  encoding: input instanceof Uint8Array ? encoding : undefined
@@ -300,43 +406,94 @@ const causeMessage = (cause: unknown): string | undefined => {
300
406
  return inspectToken(cause)
301
407
  }
302
408
 
303
- const renderSnippet = (input: Input, offset: number, encoding: Encoding<any> | undefined): Array<string> => {
409
+ /**
410
+ * How many characters of the failing line a message shows on each side of
411
+ * the failure. A line has no length limit (minified JSON is one line), and
412
+ * the input's sender chooses its length, so the message shows a window
413
+ * and marks what it left out with `…`.
414
+ */
415
+ const SnippetWindow = 60
416
+
417
+ const Ellipsis = "…"
418
+
419
+ /** The two lines of a text snippet: the window of the line, and a caret under the failure. */
420
+ const snippetLines = (line: number, window: string, before: number, cutStart: boolean, cutEnd: boolean) => {
421
+ const gutter = String(line)
422
+ const pad = " ".repeat(gutter.length)
423
+ const shown = window.endsWith("\r") && !cutEnd ? window.slice(0, -1) : window
424
+ return [
425
+ `${gutter} | ${cutStart ? Ellipsis : ""}${shown}${cutEnd ? Ellipsis : ""}`,
426
+ `${pad} | ${" ".repeat(before + (cutStart ? 1 : 0))}^`
427
+ ]
428
+ }
429
+
430
+ const isHighSurrogate = (c: number): boolean => c >= 0xd800 && c <= 0xdbff
431
+ const isLowSurrogate = (c: number): boolean => c >= 0xdc00 && c <= 0xdfff
432
+
433
+ const renderSnippet = (
434
+ input: Input,
435
+ offset: number,
436
+ encoding: Encoding<any> | undefined,
437
+ line: number
438
+ ): Array<string> => {
304
439
  if (encoding !== undefined && input instanceof Uint8Array) {
305
- // Find the line containing `offset`, then decode it
440
+ // The window is found in one walk: where the last `SnippetWindow`
441
+ // characters before the failure start (a ring of their offsets), and
442
+ // where the line or the window ends after it
443
+ const ring = new Array<number>(SnippetWindow)
444
+ let before = 0
306
445
  let lineStart = 0
446
+ let after = 0
307
447
  let end = input.length
308
- let column = 1
448
+ let cutEnd = false
309
449
  text.walk(encoding, input, 0, input.length, (cp, i, w) => {
310
- if (i >= offset && cp === 10) {
311
- end = i
312
- return false
313
- }
314
450
  if (i < offset) {
315
451
  if (cp === 10) {
452
+ before = 0
316
453
  lineStart = i + w
317
- column = 1
318
454
  } else {
319
- column++
455
+ ring[before % SnippetWindow] = i
456
+ before++
320
457
  }
458
+ return
321
459
  }
460
+ if (cp === 10) {
461
+ end = i
462
+ return false
463
+ }
464
+ if (after === SnippetWindow) {
465
+ end = i
466
+ cutEnd = true
467
+ return false
468
+ }
469
+ after++
322
470
  })
323
- let line = text.lenientText(encoding, input, lineStart, end)
324
- if (line.endsWith("\r")) line = line.slice(0, -1)
325
- const gutter = String(lineColumn(input, offset, encoding).line)
326
- const pad = " ".repeat(gutter.length)
327
- return [`${gutter} | ${line}`, `${pad} | ${" ".repeat(column - 1)}^`]
471
+ const cutStart = before > SnippetWindow
472
+ const start = cutStart ? ring[before % SnippetWindow] : lineStart
473
+ const shown = Math.min(before, SnippetWindow)
474
+ return snippetLines(line, text.lenientText(encoding, input, start, end), shown, cutStart, cutEnd)
328
475
  }
329
476
  if (Predicate.isString(input)) {
330
- const { column, line } = lineColumn(input, offset)
331
- let start = offset
332
- while (start > 0 && input.charCodeAt(start - 1) !== 10) start--
333
- let end = offset
334
- while (end < input.length && input.charCodeAt(end) !== 10) end++
335
- let text = input.slice(start, end)
336
- if (text.endsWith("\r")) text = text.slice(0, -1)
337
- const gutter = String(line)
338
- const pad = " ".repeat(gutter.length)
339
- return [`${gutter} | ${text}`, `${pad} | ${" ".repeat(column - 1)}^`]
477
+ const at = Math.min(offset, input.length)
478
+ let start = at
479
+ let before = 0
480
+ while (start > 0 && input.charCodeAt(start - 1) !== 10 && before < SnippetWindow) {
481
+ start--
482
+ // A surrogate pair is one character of the window, and is never split
483
+ if (start > 0 && isLowSurrogate(input.charCodeAt(start)) && isHighSurrogate(input.charCodeAt(start - 1))) start--
484
+ before++
485
+ }
486
+ const cutStart = start > 0 && input.charCodeAt(start - 1) !== 10
487
+ let end = at
488
+ let after = 0
489
+ while (end < input.length && input.charCodeAt(end) !== 10 && after < SnippetWindow) {
490
+ if (isHighSurrogate(input.charCodeAt(end)) && end + 1 < input.length && isLowSurrogate(input.charCodeAt(end + 1)))
491
+ end++
492
+ end++
493
+ after++
494
+ }
495
+ const cutEnd = end < input.length && input.charCodeAt(end) !== 10
496
+ return snippetLines(line, input.slice(start, end), before, cutStart, cutEnd)
340
497
  }
341
498
  if (input instanceof Uint8Array) {
342
499
  const start = Math.max(0, offset - 8)
@@ -353,15 +510,15 @@ const render = (error: ParseError<unknown>): string => {
353
510
  const { encoding, input, offset } = error
354
511
  const lines: Array<string> = []
355
512
  if (Predicate.isString(input)) {
356
- const { column, line } = lineColumn(input, offset)
513
+ const { column, line } = error.position
357
514
  lines.push(`Parse error at line ${line}, column ${column}:`)
358
515
  } else if (encoding !== undefined) {
359
- const { column, line } = lineColumn(input, offset, encoding)
516
+ const { column, line } = error.position
360
517
  lines.push(`Parse error at line ${line}, column ${column} (byte ${offset}):`)
361
518
  } else {
362
519
  lines.push(`Parse error at offset ${offset}:`)
363
520
  }
364
- lines.push(...renderSnippet(input, offset, encoding))
521
+ lines.push(...renderSnippet(input, offset, encoding, error.position.line))
365
522
  for (let i = error.context.length - 1; i >= 0; i--) {
366
523
  lines.push(`while parsing ${error.context[i]}`)
367
524
  }