@thomasfosterau/effect-parse 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 (148) hide show
  1. package/README.md +40 -14
  2. package/dist/Ascii.d.ts +11 -4
  3. package/dist/Ascii.d.ts.map +1 -1
  4. package/dist/Ascii.js +7 -1
  5. package/dist/Ascii.js.map +1 -1
  6. package/dist/Binary.d.ts.map +1 -1
  7. package/dist/Binary.js +64 -35
  8. package/dist/Binary.js.map +1 -1
  9. package/dist/Bits.d.ts.map +1 -1
  10. package/dist/Bits.js +7 -4
  11. package/dist/Bits.js.map +1 -1
  12. package/dist/Compiler.d.ts +74 -2
  13. package/dist/Compiler.d.ts.map +1 -1
  14. package/dist/Compiler.js +78 -1
  15. package/dist/Compiler.js.map +1 -1
  16. package/dist/Expression.d.ts.map +1 -1
  17. package/dist/Expression.js +86 -74
  18. package/dist/Expression.js.map +1 -1
  19. package/dist/Input.d.ts +37 -4
  20. package/dist/Input.d.ts.map +1 -1
  21. package/dist/Input.js +42 -0
  22. package/dist/Input.js.map +1 -1
  23. package/dist/Nom.d.ts +5 -15
  24. package/dist/Nom.d.ts.map +1 -1
  25. package/dist/Nom.js.map +1 -1
  26. package/dist/ParseError.d.ts +2 -2
  27. package/dist/ParseError.d.ts.map +1 -1
  28. package/dist/ParseError.js +1 -1
  29. package/dist/ParseError.js.map +1 -1
  30. package/dist/Parser.d.ts +370 -28
  31. package/dist/Parser.d.ts.map +1 -1
  32. package/dist/Parser.js +390 -19
  33. package/dist/Parser.js.map +1 -1
  34. package/dist/ParserSchema.js +2 -2
  35. package/dist/ParserSchema.js.map +1 -1
  36. package/dist/ParserStream.d.ts +3 -2
  37. package/dist/ParserStream.d.ts.map +1 -1
  38. package/dist/ParserStream.js +15 -10
  39. package/dist/ParserStream.js.map +1 -1
  40. package/dist/Syntax.d.ts +23 -5
  41. package/dist/Syntax.d.ts.map +1 -1
  42. package/dist/Syntax.js.map +1 -1
  43. package/dist/Text.d.ts.map +1 -1
  44. package/dist/Text.js +9 -9
  45. package/dist/Text.js.map +1 -1
  46. package/dist/Token.d.ts +55 -10
  47. package/dist/Token.d.ts.map +1 -1
  48. package/dist/Token.js +59 -21
  49. package/dist/Token.js.map +1 -1
  50. package/dist/internal/compiler/closure.d.ts.map +1 -1
  51. package/dist/internal/compiler/closure.js +257 -97
  52. package/dist/internal/compiler/closure.js.map +1 -1
  53. package/dist/internal/compiler/entries.d.ts +3 -1
  54. package/dist/internal/compiler/entries.d.ts.map +1 -1
  55. package/dist/internal/compiler/entries.js +3 -2
  56. package/dist/internal/compiler/entries.js.map +1 -1
  57. package/dist/internal/compiler/first.d.ts.map +1 -1
  58. package/dist/internal/compiler/first.js +7 -1
  59. package/dist/internal/compiler/first.js.map +1 -1
  60. package/dist/internal/compiler/graph.d.ts +17 -0
  61. package/dist/internal/compiler/graph.d.ts.map +1 -0
  62. package/dist/internal/compiler/graph.js +56 -0
  63. package/dist/internal/compiler/graph.js.map +1 -0
  64. package/dist/internal/compiler/jit.d.ts +5 -0
  65. package/dist/internal/compiler/jit.d.ts.map +1 -1
  66. package/dist/internal/compiler/jit.js +185 -96
  67. package/dist/internal/compiler/jit.js.map +1 -1
  68. package/dist/internal/compiler/registry.d.ts +10 -2
  69. package/dist/internal/compiler/registry.d.ts.map +1 -1
  70. package/dist/internal/compiler/registry.js.map +1 -1
  71. package/dist/internal/compiler/search.d.ts +32 -0
  72. package/dist/internal/compiler/search.d.ts.map +1 -0
  73. package/dist/internal/compiler/search.js +68 -0
  74. package/dist/internal/compiler/search.js.map +1 -0
  75. package/dist/internal/compiler/sets.d.ts.map +1 -1
  76. package/dist/internal/compiler/sets.js +1 -1
  77. package/dist/internal/compiler/sets.js.map +1 -1
  78. package/dist/internal/compiler/wasm.d.ts +8 -0
  79. package/dist/internal/compiler/wasm.d.ts.map +1 -0
  80. package/dist/internal/compiler/wasm.js +2769 -0
  81. package/dist/internal/compiler/wasm.js.map +1 -0
  82. package/dist/internal/compiler/wasmAsm.d.ts +199 -0
  83. package/dist/internal/compiler/wasmAsm.d.ts.map +1 -0
  84. package/dist/internal/compiler/wasmAsm.js +427 -0
  85. package/dist/internal/compiler/wasmAsm.js.map +1 -0
  86. package/dist/internal/entries.d.ts +3 -1
  87. package/dist/internal/entries.d.ts.map +1 -1
  88. package/dist/internal/entries.js +1 -2
  89. package/dist/internal/entries.js.map +1 -1
  90. package/dist/internal/node.d.ts +58 -2
  91. package/dist/internal/node.d.ts.map +1 -1
  92. package/dist/internal/node.js +13 -3
  93. package/dist/internal/node.js.map +1 -1
  94. package/dist/internal/numbers.d.ts.map +1 -1
  95. package/dist/internal/numbers.js.map +1 -1
  96. package/dist/internal/run.d.ts +12 -0
  97. package/dist/internal/run.d.ts.map +1 -1
  98. package/dist/internal/run.js +51 -11
  99. package/dist/internal/run.js.map +1 -1
  100. package/dist/internal/stack.d.ts +6 -0
  101. package/dist/internal/stack.d.ts.map +1 -1
  102. package/dist/internal/stack.js +8 -2
  103. package/dist/internal/stack.js.map +1 -1
  104. package/dist/internal/syntax.d.ts +4 -4
  105. package/dist/internal/syntax.d.ts.map +1 -1
  106. package/dist/internal/syntax.js +18 -10
  107. package/dist/internal/syntax.js.map +1 -1
  108. package/dist/internal/text.d.ts +32 -0
  109. package/dist/internal/text.d.ts.map +1 -1
  110. package/dist/internal/text.js +68 -14
  111. package/dist/internal/text.js.map +1 -1
  112. package/dist/internal/tokens.d.ts +13 -0
  113. package/dist/internal/tokens.d.ts.map +1 -1
  114. package/dist/internal/tokens.js +59 -39
  115. package/dist/internal/tokens.js.map +1 -1
  116. package/package.json +5 -4
  117. package/src/Ascii.ts +42 -9
  118. package/src/Binary.ts +151 -71
  119. package/src/Bits.ts +12 -6
  120. package/src/Compiler.ts +101 -10
  121. package/src/Expression.ts +94 -81
  122. package/src/Input.ts +65 -6
  123. package/src/Nom.ts +15 -5
  124. package/src/ParseError.ts +9 -4
  125. package/src/Parser.ts +682 -129
  126. package/src/ParserSchema.ts +1 -1
  127. package/src/ParserStream.ts +40 -19
  128. package/src/Syntax.ts +60 -36
  129. package/src/Text.ts +16 -15
  130. package/src/Token.ts +138 -29
  131. package/src/internal/compiler/closure.ts +362 -135
  132. package/src/internal/compiler/entries.ts +4 -2
  133. package/src/internal/compiler/first.ts +10 -0
  134. package/src/internal/compiler/graph.ts +68 -0
  135. package/src/internal/compiler/jit.ts +284 -154
  136. package/src/internal/compiler/registry.ts +10 -2
  137. package/src/internal/compiler/search.ts +68 -0
  138. package/src/internal/compiler/sets.ts +7 -3
  139. package/src/internal/compiler/wasm.ts +3203 -0
  140. package/src/internal/compiler/wasmAsm.ts +484 -0
  141. package/src/internal/entries.ts +10 -4
  142. package/src/internal/node.ts +54 -4
  143. package/src/internal/numbers.ts +3 -1
  144. package/src/internal/run.ts +71 -12
  145. package/src/internal/stack.ts +8 -1
  146. package/src/internal/syntax.ts +39 -18
  147. package/src/internal/text.ts +97 -9
  148. package/src/internal/tokens.ts +96 -14
@@ -24,18 +24,77 @@ export const runOn = <A>(
24
24
  state: State<any>,
25
25
  diagnostics: boolean = true
26
26
  ): A | Fail => {
27
- if (state.tracer === undefined && state.recovered === undefined && state.syntax === undefined) {
28
- const compiled = Registry.stepFor(p, state, state.partial || state.resume !== undefined)
29
- if (compiled !== undefined) {
30
- const start = state.pos
31
- const a = Stack.run(compiled, state)
32
- if (a !== Fail) {
33
- // SAFETY: the compiled step returns what the parser's step returns
34
- return a as A
35
- }
36
- state.rewind(start)
37
- if (!diagnostics) return Fail
27
+ if (state.tracer !== undefined || state.recovered !== undefined || state.syntax !== undefined) {
28
+ return Stack.run(step, state)
29
+ }
30
+ const compiled = Registry.compiled(p, state.inputKind, state.partial || state.resume !== undefined)
31
+ return compiled === undefined ? Stack.run(step, state) : attempt(compiled, step, state, diagnostics)
32
+ }
33
+
34
+ /**
35
+ * Runs a compiled step on `state`: directly when it guards against
36
+ * call-stack overflows itself (the JIT's), else under `Stack.run`.
37
+ */
38
+ export const runCompiled = <A>(step: (state: State<any>) => A, guarded: boolean, state: State<any>): A | Fail =>
39
+ guarded ? step(state) : Stack.run(step, state)
40
+
41
+ /**
42
+ * Runs the compiled step of `compiled` (or, if compiling failed, the
43
+ * parser's); if it fails, rewinds and, with `diagnostics`, runs the
44
+ * parser's step to record the failure.
45
+ */
46
+ const attempt = <A>(
47
+ compiled: Registry.Compiled,
48
+ step: (state: State<any>) => A | Fail,
49
+ state: State<any>,
50
+ diagnostics: boolean
51
+ ): A | Fail => {
52
+ const c = compiled.step
53
+ if (c === undefined) return Stack.run(step, state)
54
+ const start = state.pos
55
+ const a = runCompiled(c, compiled.guarded, state)
56
+ if (a !== Fail) {
57
+ // SAFETY: the compiled step returns what the parser's step returns
58
+ return a as A
59
+ }
60
+ state.rewind(start)
61
+ return diagnostics ? Stack.run(step, state) : Fail
62
+ }
63
+
64
+ /** What `prepare` holds before its first run: nothing compiled. */
65
+ const uncompiled: Registry.Compiled = {
66
+ backend: "interpreted",
67
+ step: undefined,
68
+ guarded: false,
69
+ fallbacks: [],
70
+ error: undefined
71
+ }
72
+
73
+ /**
74
+ * `runOn(p, p.step, state, diagnostics)` as a function of the state, with
75
+ * the per-run lookup done once: the compiled step is looked up again only
76
+ * when the input kind or the build (plain or resumable) changes from the
77
+ * previous run.
78
+ */
79
+ export const prepare = <A>(
80
+ p: ParserLike,
81
+ step: (state: State<any>) => A | Fail,
82
+ diagnostics: boolean
83
+ ): ((state: State<any>) => A | Fail) => {
84
+ // The build of the previous run: its input kind (-1 before the first run) and whether it is resumable
85
+ let kind = -1
86
+ let resumable = false
87
+ let compiled = uncompiled
88
+ return (state) => {
89
+ if (state.tracer !== undefined || state.recovered !== undefined || state.syntax !== undefined) {
90
+ return Stack.run(step, state)
91
+ }
92
+ const r = state.partial || state.resume !== undefined
93
+ if (state.inputKind !== kind || r !== resumable) {
94
+ compiled = Registry.compiled(p, state.inputKind, r) ?? uncompiled
95
+ kind = state.inputKind
96
+ resumable = r
38
97
  }
98
+ return attempt(compiled, step, state, diagnostics)
39
99
  }
40
- return Stack.run(step, state)
41
100
  }
@@ -36,6 +36,13 @@ export const run = <S extends State<any>, A>(step: (state: S) => A, state: S): A
36
36
  return step(state)
37
37
  } catch (e) {
38
38
  if (!isStackOverflow(e)) throw e
39
- return state.failMessage("Cut", "TooDeep", tooDeepMessage)
39
+ return tooDeep(state)
40
40
  }
41
41
  }
42
+
43
+ /**
44
+ * Records the `TooDeep` failure of a call-stack overflow. The JIT's
45
+ * generated entry catches overflows itself, with this, so that the runners
46
+ * can call it without {@link run}.
47
+ */
48
+ export const tooDeep = (state: State<any>): Fail => state.failMessage("Cut", "TooDeep", tooDeepMessage)
@@ -105,9 +105,13 @@ const rebase = (f: Failure, start: number): Failure =>
105
105
  /** Moves a failure stored relative to a node back to absolute offsets. */
106
106
  export const absolute = (f: Failure, start: number): Failure => rebase(f, -start)
107
107
 
108
+ /** The children, and their offsets, of every leaf: one shared empty array. */
109
+ const none: ReadonlyArray<never> = []
110
+
108
111
  /**
109
112
  * Builds a green node over `[start, end)` from the children waiting on the
110
- * stack.
113
+ * stack, `entries` from index `from` on (which the caller then removes):
114
+ * slicing them off first would allocate an array per node.
111
115
  */
112
116
  const build = (
113
117
  kind: string,
@@ -117,24 +121,40 @@ const build = (
117
121
  end: number,
118
122
  lookahead: number,
119
123
  entries: ReadonlyArray<Child>,
124
+ from: number,
120
125
  value: unknown,
121
126
  error: ErrorInfo | undefined,
122
127
  diagnostics?: Diagnostics
123
128
  ): Green => {
124
- const n = entries.length
129
+ const n = entries.length - from
130
+ const width = end - start
131
+ const looked = Math.max(lookahead, end) - start
132
+ if (n === 0)
133
+ return new Green(
134
+ kind,
135
+ named,
136
+ key,
137
+ width,
138
+ looked,
139
+ none,
140
+ none,
141
+ undefined,
142
+ value,
143
+ error,
144
+ error !== undefined,
145
+ diagnostics
146
+ )
125
147
  const children = new Array<Green>(n)
126
148
  const offsets = new Array<number>(n)
127
149
  let fields: Array<string | undefined> | undefined
128
150
  let hasError = error !== undefined
129
151
  for (let i = 0; i < n; i++) {
130
- const e = entries[i]
152
+ const e = entries[from + i]
131
153
  children[i] = e.green
132
154
  offsets[i] = e.start - start
133
155
  if (e.green.hasError) hasError = true
134
156
  if (e.field !== undefined) (fields ??= new Array(n).fill(undefined))[i] = e.field
135
157
  }
136
- const width = end - start
137
- const looked = Math.max(lookahead, end) - start
138
158
  return new Green(kind, named, key, width, looked, children, offsets, fields, value, error, hasError, diagnostics)
139
159
  }
140
160
 
@@ -162,8 +182,8 @@ export interface Previous {
162
182
 
163
183
  /** The hooks that `Syntax` combinators and `Parser.resumeAfter`/`retryAfter` call. */
164
184
  export interface Builder {
165
- node(state: State<any>, step: (state: State<any>) => unknown, key: Key, kind: string, named: boolean): unknown
166
- field(state: State<any>, step: (state: State<any>) => unknown, name: string): unknown
185
+ node<A>(state: State<any>, step: (state: State<any>) => A, key: Key, kind: string, named: boolean): A
186
+ field<A>(state: State<any>, step: (state: State<any>) => A, name: string): A
167
187
  /** After error recovery: `[start, pos)` was skipped after `failure`. */
168
188
  skipped(start: number, failure: Failure, mode: ErrMode): void
169
189
  /**
@@ -224,7 +244,7 @@ export class SyntaxBuilder implements Builder {
224
244
  return this.state.pos > this.highWater ? this.state.pos : this.highWater
225
245
  }
226
246
 
227
- node(state: State<any>, step: (state: State<any>) => unknown, key: Key, kind: string, named: boolean): unknown {
247
+ node<A>(state: State<any>, step: (state: State<any>) => A, key: Key, kind: string, named: boolean): A {
228
248
  const start = this.state.pos
229
249
  const stack = this.stack
230
250
  if (this.previous !== undefined) {
@@ -241,7 +261,8 @@ export class SyntaxBuilder implements Builder {
241
261
  if (d.hintExpected !== undefined) this.state.setHint(start + d.hintOffset, d.hintExpected)
242
262
  }
243
263
  stack.push(new Child(green, start, end, undefined))
244
- return green.value
264
+ // SAFETY: `reusable` returns only a node built under this `key`, by this parser's `step`, whose value it holds
265
+ return green.value as A
245
266
  }
246
267
  }
247
268
  const mark = stack.length
@@ -255,7 +276,7 @@ export class SyntaxBuilder implements Builder {
255
276
  const a = step(state)
256
277
  const looked = this.looked()
257
278
  this.highWater = outer > looked ? outer : looked
258
- const deepest = this.state.deepest as Failure | undefined
279
+ const deepest = this.state.deepest
259
280
  this.state.deepest = outerDeepest
260
281
  if (deepest !== undefined) this.deepen(deepest)
261
282
  if (a === Fail) {
@@ -275,10 +296,9 @@ export class SyntaxBuilder implements Builder {
275
296
  hintOffset: this.state.hintOffset - start,
276
297
  hintExpected: hinted ? this.state.hintExpected : undefined
277
298
  }
278
- const entries = stack.splice(mark)
279
- stack.push(
280
- new Child(build(kind, named, key, start, end, looked, entries, a, undefined, diagnostics), start, end, undefined)
281
- )
299
+ const green = build(kind, named, key, start, end, looked, stack, mark, a, undefined, diagnostics)
300
+ stack.length = mark
301
+ stack.push(new Child(green, start, end, undefined))
282
302
  return a
283
303
  }
284
304
 
@@ -292,7 +312,7 @@ export class SyntaxBuilder implements Builder {
292
312
  if (this.state.deepest === undefined || failure.offset >= this.state.deepest.offset) this.state.deepest = failure
293
313
  }
294
314
 
295
- field(state: State<any>, step: (state: State<any>) => unknown, name: string): unknown {
315
+ field<A>(state: State<any>, step: (state: State<any>) => A, name: string): A {
296
316
  const stack = this.stack
297
317
  const mark = stack.length
298
318
  const a = step(state)
@@ -305,10 +325,11 @@ export class SyntaxBuilder implements Builder {
305
325
  let i = stack.length
306
326
  while (i > 0 && stack[i - 1].start >= start && stack[i - 1].end > start) i--
307
327
  const end = this.state.pos
308
- const green = build(errorKind, true, undefined, start, end, this.looked(), stack.splice(i), undefined, {
328
+ const green = build(errorKind, true, undefined, start, end, this.looked(), stack, i, undefined, {
309
329
  failure: rebase(failure, start),
310
330
  mode
311
331
  })
332
+ stack.length = i
312
333
  stack.push(new Child(green, start, end, undefined))
313
334
  }
314
335
 
@@ -323,13 +344,13 @@ export class SyntaxBuilder implements Builder {
323
344
  if (fatal !== undefined) {
324
345
  const last = Arr.isArrayNonEmpty(entries) ? Arr.lastNonEmpty(entries).end : 0
325
346
  const start = Math.max(Math.min(errorStart, end), last)
326
- const error = build(errorKind, true, undefined, start, end, end, [], undefined, {
347
+ const error = build(errorKind, true, undefined, start, end, end, none, 0, undefined, {
327
348
  failure: rebase(fatal.failure, start),
328
349
  mode: fatal.mode
329
350
  })
330
351
  entries.push(new Child(error, start, end, undefined))
331
352
  }
332
- return build(kind, true, undefined, 0, end, Math.max(end, this.looked()), entries, value, undefined)
353
+ return build(kind, true, undefined, 0, end, Math.max(end, this.looked()), entries, 0, value, undefined)
333
354
  }
334
355
 
335
356
  /**
@@ -38,7 +38,18 @@ export const isEmptyUnits = (units: string | Uint8Array | ReadonlyArray<unknown>
38
38
 
39
39
  /** @internal */
40
40
  export const fastLimit = (enc: Encoding<any>): number =>
41
- (enc as any)[FastLimit] ?? (enc.asciiCompatible && enc.unit === 1 ? 0x80 : 0)
41
+ // SAFETY: the built-in encodings carry the internal `FastLimit` key; on any other encoding it reads `undefined`
42
+ (enc as Encoding<any> & { readonly [FastLimit]?: number })[FastLimit] ??
43
+ (enc.asciiCompatible && enc.unit === 1 ? 0x80 : 0)
44
+
45
+ /**
46
+ * `String.fromCharCode`, seen as taking its argument list through `apply` as
47
+ * any array-like (which `Function.prototype.apply` accepts), such as a view of
48
+ * bytes.
49
+ */
50
+ export interface FromCharCodes {
51
+ readonly apply: (thisArg: null, codes: ArrayLike<number>) => string
52
+ }
42
53
 
43
54
  const asciiChars: ReadonlyArray<string> = /*#__PURE__*/ Array.from({ length: 128 }, (_, i) => String.fromCharCode(i))
44
55
 
@@ -68,6 +79,7 @@ const decodeString = (s: string, i: number, end: number): number => {
68
79
  * JavaScript strings: UTF-16 code units. Offsets are code-unit indices and
69
80
  * slices are `String.prototype.slice`s, so no decoding is needed.
70
81
  */
82
+ // SAFETY: an `Encoding` with the internal `FastLimit` key, which its type doesn't declare
71
83
  export const string: Encoding<string> = {
72
84
  name: "UTF-16",
73
85
  unit: 1,
@@ -112,9 +124,38 @@ const decodeUtf8 = (b: Uint8Array, i: number, end: number): number => {
112
124
  return Invalid
113
125
  }
114
126
 
115
- // Building short strings by hand is several times faster than a
116
- // `TextDecoder` call, which has a high fixed cost.
117
- const SHORT = 24
127
+ const fromCharCode = String.fromCharCode
128
+
129
+ /** How many of the (up to eight) bytes from `i` are ASCII; at least one, as `b[i]` is. */
130
+ const asciiPrefix = (b: Uint8Array, i: number, end: number): number => {
131
+ const n = end - i < 8 ? end - i : 8
132
+ let k = 1
133
+ while (k < n && b[i + k] < 0x80) k++
134
+ return k
135
+ }
136
+
137
+ /** The `n` (1 to 8) ASCII bytes from `i`, as a string. */
138
+ const asciiText = (b: Uint8Array, i: number, n: number): string => {
139
+ if (n === 8) return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7])
140
+ if (n === 1) return asciiChars[b[i]]
141
+ if (n === 2) return fromCharCode(b[i], b[i + 1])
142
+ if (n === 3) return fromCharCode(b[i], b[i + 1], b[i + 2])
143
+ if (n === 4) return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3])
144
+ if (n === 5) return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4])
145
+ if (n === 6) return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5])
146
+ return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6])
147
+ }
148
+
149
+ /** The longest UTF-16 range, in bytes, built by hand rather than by a `TextDecoder` call. */
150
+ const ShortUtf16 = 48
151
+
152
+ /**
153
+ * The longest UTF-8 range built by hand: built eight ASCII bytes at a
154
+ * time, shorter ones are faster than a `TextDecoder` call, which costs
155
+ * about 120 ns whatever the length. Raising it past about 50 would also
156
+ * keep typical strings from {@link utf8Text}'s ASCII runs.
157
+ */
158
+ export const ShortText = 40
118
159
 
119
160
  const utf8Decoder = /*#__PURE__*/ new TextDecoder("utf-8", { ignoreBOM: true })
120
161
 
@@ -128,6 +169,46 @@ const decodeRange = (decoder: TextDecoder, b: Uint8Array, start: number, end: nu
128
169
  }
129
170
  const utf8Encoder = /*#__PURE__*/ new TextEncoder()
130
171
 
172
+ /** A run of ASCII bytes, `[start, end)` of a state's input, and its text. */
173
+ export interface AsciiRun {
174
+ readonly start: number
175
+ readonly end: number
176
+ readonly text: string
177
+ }
178
+
179
+ /** How far past a string {@link utf8Text} looks for the end of its ASCII run. */
180
+ const RunWindow = 16384
181
+
182
+ /**
183
+ * Decodes `[start, end)` of a state's UTF-8 input, as `utf8.text` does.
184
+ * Each `TextDecoder` call has a fixed cost of about 120 ns, more than
185
+ * slicing a string, so a long string inside a run of ASCII bytes (where a
186
+ * byte offset is a character offset) is sliced from the text of the whole
187
+ * run, decoded once and kept on the state; short strings, and those the run
188
+ * doesn't cover, are decoded as `utf8.text` decodes them.
189
+ */
190
+ export const utf8Text = (s: State<any>, start: number, end: number): string => {
191
+ // SAFETY: called only by `Text.utf8` parsers, whose input is bytes
192
+ const b = s.input as Uint8Array
193
+ // `textIn` and the JIT check this themselves before calling
194
+ if (end - start <= ShortText) return utf8.text(b, start, end)
195
+ const run = s.asciiRun
196
+ if (run !== undefined && start >= run.start && end <= run.end)
197
+ return run.text.slice(start - run.start, end - run.start)
198
+ let e = start
199
+ const limit = Math.min(s.end, end + RunWindow)
200
+ while (e < limit && b[e] < 0x80) e++
201
+ // Only worth it when the run holds more than this string
202
+ if (e < end || e - start < 4 * (end - start)) return decodeRange(utf8Decoder, b, start, end)
203
+ const text = decodeRange(utf8Decoder, b, start, e)
204
+ s.asciiRun = { start, end: e, text }
205
+ return text.slice(0, end - start)
206
+ }
207
+
208
+ /** Decodes `[start, end)` of a state's input in `enc`, through {@link utf8Text} for UTF-8. */
209
+ export const textIn = (enc: Encoding<any>, s: State<any>, start: number, end: number): string =>
210
+ enc === utf8 && end - start > ShortText ? utf8Text(s, start, end) : enc.text(s.input, start, end)
211
+
131
212
  /** UTF-8 bytes. */
132
213
  // SAFETY: an `Encoding` with the internal `FastLimit` key, which its type doesn't declare
133
214
  export const utf8: Encoding<Uint8Array> = {
@@ -138,13 +219,16 @@ export const utf8: Encoding<Uint8Array> = {
138
219
  width: (cp) => (cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4),
139
220
  encode: (text) => utf8Encoder.encode(text),
140
221
  text: (b, start, end) => {
141
- if (end - start > SHORT) return decodeRange(utf8Decoder, b, start, end)
222
+ if (end - start > ShortText) return decodeRange(utf8Decoder, b, start, end)
142
223
  let out = ""
143
224
  for (let i = start; i < end;) {
144
225
  const c = b[i]
145
226
  if (c < 0x80) {
146
- out += asciiChars[c]
147
- i++
227
+ // Up to eight ASCII bytes in one call: a string a character at a
228
+ // time costs about 8 ns a character, eight in one call about 20 ns
229
+ const n = asciiPrefix(b, i, end)
230
+ out += asciiText(b, i, n)
231
+ i += n
148
232
  } else {
149
233
  const cp = decodeUtf8(b, i, end)
150
234
  out += cp < 0x10000 ? String.fromCharCode(cp) : String.fromCodePoint(cp)
@@ -187,7 +271,7 @@ const makeUtf16 = (littleEndian: boolean): Encoding<Uint8Array> => {
187
271
  return out
188
272
  },
189
273
  text: (b, start, end) => {
190
- if (end - start <= SHORT * 2) {
274
+ if (end - start <= ShortUtf16) {
191
275
  // The range is well formed, so surrogate pairs can be copied unit by unit
192
276
  let out = ""
193
277
  for (let i = start; i < end; i += 2) out += String.fromCharCode(b[i + lo] | (b[i + hi] << 8))
@@ -207,6 +291,7 @@ export const utf16le: Encoding<Uint8Array> = /*#__PURE__*/ makeUtf16(true)
207
291
  export const utf16be: Encoding<Uint8Array> = /*#__PURE__*/ makeUtf16(false)
208
292
 
209
293
  /** ISO-8859-1: every byte is the code point of the same value. */
294
+ // SAFETY: an `Encoding` with the internal `FastLimit` key, which its type doesn't declare
210
295
  export const latin1: Encoding<Uint8Array> = {
211
296
  name: "ISO-8859-1",
212
297
  unit: 1,
@@ -225,7 +310,8 @@ export const latin1: Encoding<Uint8Array> = {
225
310
  text: (b, start, end) => {
226
311
  let out = ""
227
312
  for (let i = start; i < end; i += 8192) {
228
- out += String.fromCharCode.apply(null, b.subarray(i, Math.min(end, i + 8192)) as unknown as Array<number>)
313
+ // SAFETY: `apply` takes any array-like argument list, so each byte of the view is passed as a char code
314
+ out += (String.fromCharCode as FromCharCodes).apply(null, b.subarray(i, Math.min(end, i + 8192)))
229
315
  }
230
316
  return out
231
317
  },
@@ -302,6 +388,7 @@ export const scan = (
302
388
  let count = 0
303
389
  let stop = 0
304
390
  if (state.inputKind === 0) {
391
+ // SAFETY: `inputKind` 0 is set only for a string input
305
392
  const s = state.input as string
306
393
  while (count < max) {
307
394
  if (i >= end) {
@@ -324,6 +411,7 @@ export const scan = (
324
411
  count++
325
412
  }
326
413
  } else {
414
+ // SAFETY: an encoding's input is a string or a `Uint8Array`, and `inputKind` is not 0, so a `Uint8Array`
327
415
  const b = state.input as Uint8Array
328
416
  while (count < max) {
329
417
  if (i >= end) {