@tabnas/transduce 0.1.2 → 0.2.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 (83) hide show
  1. package/README.md +18 -0
  2. package/dist/guard.d.ts +1 -3
  3. package/dist/guard.js +8 -9
  4. package/dist/guard.js.map +1 -1
  5. package/dist/lines.d.ts +1 -2
  6. package/dist/lines.js +46 -89
  7. package/dist/lines.js.map +1 -1
  8. package/dist/matcher.d.ts +1 -3
  9. package/dist/matcher.js +12 -13
  10. package/dist/matcher.js.map +1 -1
  11. package/dist/parser-source.d.ts +1 -2
  12. package/dist/parser-source.js +11 -13
  13. package/dist/parser-source.js.map +1 -1
  14. package/dist/route.d.ts +1 -32
  15. package/dist/route.js +8 -32
  16. package/dist/route.js.map +1 -1
  17. package/dist/routers.d.ts +2 -0
  18. package/dist/routers.js +24 -0
  19. package/dist/routers.js.map +1 -0
  20. package/dist/rule-events.d.ts +1 -4
  21. package/dist/rule-events.js +40 -44
  22. package/dist/rule-events.js.map +1 -1
  23. package/dist/scan.d.ts +1 -10
  24. package/dist/scan.js +4 -17
  25. package/dist/scan.js.map +1 -1
  26. package/dist/source.d.ts +1 -4
  27. package/dist/source.js +20 -22
  28. package/dist/source.js.map +1 -1
  29. package/dist/table-from-json.d.ts +1 -5
  30. package/dist/table-from-json.js +25 -29
  31. package/dist/table-from-json.js.map +1 -1
  32. package/dist/transduce.d.ts +23 -19
  33. package/dist/transduce.js +66 -56
  34. package/dist/transduce.js.map +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +12 -4
  37. package/src/guard.ts +10 -4
  38. package/src/lines.ts +45 -6
  39. package/src/matcher.ts +1 -6
  40. package/src/parser-source.ts +2 -4
  41. package/src/route.ts +18 -70
  42. package/src/routers.ts +64 -0
  43. package/src/rule-events.ts +17 -7
  44. package/src/scan.ts +1 -19
  45. package/src/source.ts +12 -6
  46. package/src/table-from-json.ts +20 -9
  47. package/src/transduce.ts +36 -19
  48. package/dist/datum.d.ts +0 -64
  49. package/dist/datum.js +0 -391
  50. package/dist/datum.js.map +0 -1
  51. package/dist/error.d.ts +0 -43
  52. package/dist/error.js +0 -169
  53. package/dist/error.js.map +0 -1
  54. package/dist/event.d.ts +0 -56
  55. package/dist/event.js +0 -101
  56. package/dist/event.js.map +0 -1
  57. package/dist/json.d.ts +0 -4
  58. package/dist/json.js +0 -92
  59. package/dist/json.js.map +0 -1
  60. package/dist/limits.d.ts +0 -36
  61. package/dist/limits.js +0 -133
  62. package/dist/limits.js.map +0 -1
  63. package/dist/selector.d.ts +0 -40
  64. package/dist/selector.js +0 -154
  65. package/dist/selector.js.map +0 -1
  66. package/dist/sink.d.ts +0 -29
  67. package/dist/sink.js +0 -184
  68. package/dist/sink.js.map +0 -1
  69. package/dist/table.d.ts +0 -81
  70. package/dist/table.js +0 -141
  71. package/dist/table.js.map +0 -1
  72. package/dist/value.d.ts +0 -16
  73. package/dist/value.js +0 -96
  74. package/dist/value.js.map +0 -1
  75. package/src/datum.ts +0 -407
  76. package/src/error.ts +0 -198
  77. package/src/event.ts +0 -134
  78. package/src/json.ts +0 -82
  79. package/src/limits.ts +0 -165
  80. package/src/selector.ts +0 -191
  81. package/src/sink.ts +0 -199
  82. package/src/table.ts +0 -213
  83. package/src/value.ts +0 -93
package/src/datum.ts DELETED
@@ -1,407 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // The retained value type.
4
- //
5
- // What a capture materializes and what a projected cell holds. Distinct
6
- // from the engine's value on purpose: a datum keeps a number's lexeme,
7
- // keeps member order whatever the keys look like (a `Map`, since a plain
8
- // object enumerates integer-like keys first), measures its own size
9
- // against limits in UTF-8 bytes, and is owned by the transducer rather
10
- // than shared with a parse.
11
-
12
- import { Fail } from './error'
13
- import { Ev, JsonEvent } from './event'
14
- import { jsonNumber, jsonString } from './json'
15
- import { NODE_BYTES, utf8Bytes } from './limits'
16
- import { Segment } from './selector'
17
- import { Flow, Sink } from './sink'
18
- import { engineKeys, engineScalar } from './value'
19
-
20
- // A retained JSON-like value. Members of an object are in source order; a
21
- // repeated member replaces the earlier one in its place (last value wins)
22
- // unless the builder's policy rejected it first.
23
- export type Datum =
24
- | { readonly type: 'null' }
25
- | { readonly type: 'bool'; readonly value: boolean }
26
- | { readonly type: 'number'; readonly value: number; readonly lexeme: string | null }
27
- | { readonly type: 'string'; readonly value: string }
28
- | { readonly type: 'array'; readonly items: Datum[] }
29
- | { readonly type: 'object'; readonly members: Map<string, Datum> }
30
-
31
- const NULL: Datum = Object.freeze({ type: 'null' })
32
- const TRUE: Datum = Object.freeze({ type: 'bool', value: true })
33
- const FALSE: Datum = Object.freeze({ type: 'bool', value: false })
34
-
35
- export const Datum = Object.freeze({
36
- null: NULL,
37
- bool(value: boolean): Datum {
38
- return value ? TRUE : FALSE
39
- },
40
- number(value: number, lexeme?: string | null): Datum {
41
- return { type: 'number', value, lexeme: lexeme ?? null }
42
- },
43
- string(value: string): Datum {
44
- return { type: 'string', value }
45
- },
46
- array(items: Datum[] = []): Datum {
47
- return { type: 'array', items }
48
- },
49
- object(members: Map<string, Datum> | Iterable<[string, Datum]> = []): Datum {
50
- return { type: 'object', members: members instanceof Map ? members : new Map(members) }
51
- },
52
-
53
- // Payload bytes plus `NODE_BYTES` per node: the measure limits use.
54
- byteSize,
55
- getPath,
56
- takePath,
57
- isContainer(d: Datum): boolean {
58
- return 'array' === d.type || 'object' === d.type
59
- },
60
-
61
- // From an engine value: `undefined` is `null`, as the engine serializes
62
- // it; a number has no lexeme.
63
- fromTabnas,
64
-
65
- // As a plain JavaScript value, for oracles and tests. An object becomes a
66
- // plain object (integer-like keys reorder), a number its value.
67
- toJSON,
68
-
69
- // From a plain JavaScript value, for tests. A number's lexeme is its
70
- // shortest text.
71
- fromJSON,
72
-
73
- // Compact JSON, keeping number lexemes.
74
- toText,
75
- })
76
-
77
- export function byteSize(d: Datum): number {
78
- switch (d.type) {
79
- case 'null':
80
- case 'bool':
81
- return NODE_BYTES
82
- case 'number':
83
- return NODE_BYTES + (null != d.lexeme ? utf8Bytes(d.lexeme) : 8)
84
- case 'string':
85
- return NODE_BYTES + utf8Bytes(d.value)
86
- case 'array': {
87
- let n = NODE_BYTES
88
- for (const item of d.items) n += byteSize(item)
89
- return n
90
- }
91
- case 'object': {
92
- let n = NODE_BYTES
93
- for (const [k, v] of d.members) n += utf8Bytes(k) + byteSize(v)
94
- return n
95
- }
96
- }
97
- }
98
-
99
- // The value at a concrete path below this one.
100
- export function getPath(d: Datum, path: readonly Segment[]): Datum | undefined {
101
- let here: Datum | undefined = d
102
- for (const seg of path) {
103
- if (undefined === here) return undefined
104
- if ('string' === typeof seg) {
105
- here = 'object' === here.type ? here.members.get(seg) : undefined
106
- } else {
107
- here = 'array' === here.type ? here.items[seg] : undefined
108
- }
109
- }
110
- return here
111
- }
112
-
113
- // The value at a concrete path below this one, moved out and replaced by
114
- // `null` in its container. The root itself cannot be replaced in place, so
115
- // an empty path returns the datum and leaves it alone.
116
- export function takePath(d: Datum, path: readonly Segment[]): Datum | undefined {
117
- if (0 === path.length) return d
118
- const parent = getPath(d, path.slice(0, -1))
119
- if (undefined === parent) return undefined
120
- const last = path[path.length - 1]
121
- if ('string' === typeof last) {
122
- if ('object' !== parent.type || !parent.members.has(last)) return undefined
123
- const v = parent.members.get(last) as Datum
124
- parent.members.set(last, NULL)
125
- return v
126
- }
127
- if ('array' !== parent.type || last >= parent.items.length) return undefined
128
- const v = parent.items[last]
129
- parent.items[last] = NULL
130
- return v
131
- }
132
-
133
- export function fromTabnas(value: unknown): Datum {
134
- if (Array.isArray(value)) return Datum.array(value.map(fromTabnas))
135
- const scalar = engineScalar(value)
136
- if (undefined !== scalar) {
137
- switch (scalar.type) {
138
- case 'null':
139
- return NULL
140
- case 'bool':
141
- return Datum.bool(scalar.value)
142
- case 'number':
143
- return Datum.number(scalar.value)
144
- case 'string':
145
- return Datum.string(scalar.value)
146
- }
147
- }
148
- const members = new Map<string, Datum>()
149
- for (const k of engineKeys(value)) members.set(k, fromTabnas((value as any)[k]))
150
- return { type: 'object', members }
151
- }
152
-
153
- export function toJSON(d: Datum): unknown {
154
- switch (d.type) {
155
- case 'null':
156
- return null
157
- case 'bool':
158
- case 'string':
159
- return d.value
160
- case 'number':
161
- return null != d.lexeme ? Number(d.lexeme) : d.value
162
- case 'array':
163
- return d.items.map(toJSON)
164
- case 'object': {
165
- const out: Record<string, unknown> = {}
166
- for (const [k, v] of d.members) out[k] = toJSON(v)
167
- return out
168
- }
169
- }
170
- }
171
-
172
- export function fromJSON(value: unknown): Datum {
173
- if (null == value) return NULL
174
- if ('boolean' === typeof value) return Datum.bool(value)
175
- if ('number' === typeof value) return Datum.number(value, String(value))
176
- if ('string' === typeof value) return Datum.string(value)
177
- if (Array.isArray(value)) return Datum.array(value.map(fromJSON))
178
- const members = new Map<string, Datum>()
179
- if (value instanceof Map) {
180
- for (const [k, v] of value) members.set(String(k), fromJSON(v))
181
- } else {
182
- for (const k of Object.keys(value as object)) {
183
- members.set(k, fromJSON((value as any)[k]))
184
- }
185
- }
186
- return { type: 'object', members }
187
- }
188
-
189
- export function toText(d: Datum): string {
190
- switch (d.type) {
191
- case 'null':
192
- return 'null'
193
- case 'bool':
194
- return d.value ? 'true' : 'false'
195
- case 'number':
196
- return jsonNumber(d.value, d.lexeme)
197
- case 'string':
198
- return jsonString(d.value)
199
- case 'array': {
200
- let out = '['
201
- d.items.forEach((item, i) => {
202
- if (i > 0) out += ','
203
- out += toText(item)
204
- })
205
- return out + ']'
206
- }
207
- case 'object': {
208
- let out = '{'
209
- let i = 0
210
- for (const [k, v] of d.members) {
211
- if (i++ > 0) out += ','
212
- out += jsonString(k) + ':' + toText(v)
213
- }
214
- return out + '}'
215
- }
216
- }
217
- }
218
-
219
- // Emit a datum as `JsonEvents/1` (without the final `end`, so a datum can
220
- // stand in for any part of a document).
221
- export function walkDatum(d: Datum, sink: Sink): Flow {
222
- switch (d.type) {
223
- case 'null':
224
- return sink.event(Ev.null)
225
- case 'bool':
226
- return sink.event(Ev.bool(d.value))
227
- case 'number':
228
- return sink.event(Ev.number(d.value, d.lexeme))
229
- case 'string':
230
- return sink.event(Ev.string(d.value))
231
- case 'array':
232
- if ('stop' === sink.event(Ev.arrayStart)) return 'stop'
233
- for (const item of d.items) {
234
- if ('stop' === walkDatum(item, sink)) return 'stop'
235
- }
236
- return sink.event(Ev.arrayEnd)
237
- case 'object':
238
- if ('stop' === sink.event(Ev.objectStart)) return 'stop'
239
- for (const [k, v] of d.members) {
240
- if ('stop' === sink.event(Ev.key(k))) return 'stop'
241
- if ('stop' === walkDatum(v, sink)) return 'stop'
242
- }
243
- return sink.event(Ev.objectEnd)
244
- }
245
- }
246
-
247
- // How a builder treats a repeated member name: `reject` fails with
248
- // `DUPLICATE_MEMBER`, `last_wins` replaces the earlier value, `first_wins`
249
- // keeps it.
250
- export type Duplicates = 'reject' | 'last_wins' | 'first_wins'
251
-
252
- type Frame =
253
- | { array: true; items: Datum[] }
254
- | { array: false; members: Map<string, Datum>; key: string | null }
255
-
256
- // Builds one `Datum` from the events of one value, under a byte limit.
257
- //
258
- // Feed it every event from the value's first to its last; `finished()`
259
- // says when the value is complete. Bytes are counted as they arrive, and
260
- // the limit fails at the first byte over it rather than after the value is
261
- // whole. The builder knows nothing of where in a document its value sits:
262
- // a failure leaves `path` unset and the stage that placed the builder adds
263
- // it.
264
- export class DatumBuilder {
265
- private stack: Frame[] = []
266
- private done: Datum | undefined = undefined
267
- private held = 0
268
- private readonly limit: number
269
- private readonly limitName: string
270
- private readonly duplicates: Duplicates
271
-
272
- // A builder whose limit failure names `limitName` (a `Limits` field).
273
- constructor(limit: number, limitName: string, duplicates: Duplicates) {
274
- this.limit = limit
275
- this.limitName = limitName
276
- this.duplicates = duplicates
277
- }
278
-
279
- bytes(): number {
280
- return this.held
281
- }
282
-
283
- finished(): boolean {
284
- return undefined !== this.done
285
- }
286
-
287
- // The value, once finished. Probing an unfinished builder returns
288
- // `undefined` and leaves the charge for the partial value in place.
289
- take(): Datum | undefined {
290
- const done = this.done
291
- if (undefined !== done) {
292
- this.done = undefined
293
- this.held = 0
294
- }
295
- return done
296
- }
297
-
298
- private charge(n: number): void {
299
- this.held += n
300
- if (this.held > this.limit) {
301
- throw Fail.limit(
302
- this.limitName,
303
- this.limit,
304
- `a value is larger than ${this.limit} bytes`,
305
- )
306
- }
307
- }
308
-
309
- private place(value: Datum): void {
310
- const top = this.stack[this.stack.length - 1]
311
- if (undefined === top) {
312
- this.done = value
313
- return
314
- }
315
- if (top.array) {
316
- top.items.push(value)
317
- return
318
- }
319
- const key = top.key
320
- if (null === key) {
321
- throw Fail.protocol('a value arrived inside an object without a key')
322
- }
323
- top.key = null
324
- // A repeated member was charged in full as its events arrived, which is
325
- // right: for a moment both were held. Whichever one goes now gives its
326
- // bytes back, so an object that keeps repeating a small key does not
327
- // grow towards the limit while the value it holds stays the same size.
328
- const existing = top.members.get(key)
329
- let released = 0
330
- if (undefined !== existing) {
331
- switch (this.duplicates) {
332
- case 'reject':
333
- throw new Fail('DUPLICATE_MEMBER', `member ${JSON.stringify(key)} appears twice`)
334
- case 'first_wins':
335
- this.held = Math.max(0, this.held - (utf8Bytes(key) + byteSize(value)))
336
- return
337
- case 'last_wins':
338
- released = utf8Bytes(key) + byteSize(existing)
339
- }
340
- }
341
- top.members.set(key, value)
342
- this.held = Math.max(0, this.held - released)
343
- }
344
-
345
- // One event of the value being built.
346
- event(ev: JsonEvent): void {
347
- if (undefined !== this.done) {
348
- throw Fail.protocol('an event arrived after the value was complete')
349
- }
350
- switch (ev.type) {
351
- case 'object_start':
352
- this.charge(NODE_BYTES)
353
- this.stack.push({ array: false, members: new Map(), key: null })
354
- break
355
- case 'array_start':
356
- this.charge(NODE_BYTES)
357
- this.stack.push({ array: true, items: [] })
358
- break
359
- case 'key': {
360
- this.charge(utf8Bytes(ev.key))
361
- const top = this.stack[this.stack.length - 1]
362
- if (undefined === top || top.array || null !== top.key) {
363
- throw Fail.protocol('a key arrived where no member was expected')
364
- }
365
- top.key = ev.key
366
- break
367
- }
368
- case 'object_end': {
369
- const top = this.stack.pop()
370
- if (undefined === top || top.array) {
371
- throw Fail.protocol('an object ended that had not started')
372
- }
373
- if (null !== top.key) {
374
- throw Fail.protocol('an object ended after a key without its value')
375
- }
376
- this.place({ type: 'object', members: top.members })
377
- break
378
- }
379
- case 'array_end': {
380
- const top = this.stack.pop()
381
- if (undefined === top || !top.array) {
382
- throw Fail.protocol('an array ended that had not started')
383
- }
384
- this.place({ type: 'array', items: top.items })
385
- break
386
- }
387
- case 'null':
388
- this.charge(NODE_BYTES)
389
- this.place(NULL)
390
- break
391
- case 'bool':
392
- this.charge(NODE_BYTES)
393
- this.place(Datum.bool(ev.value))
394
- break
395
- case 'number':
396
- this.charge(NODE_BYTES + (null != ev.lexeme ? utf8Bytes(ev.lexeme) : 8))
397
- this.place(Datum.number(ev.value, ev.lexeme))
398
- break
399
- case 'string':
400
- this.charge(NODE_BYTES + utf8Bytes(ev.value))
401
- this.place(Datum.string(ev.value))
402
- break
403
- case 'end':
404
- throw Fail.protocol('the document ended inside a value being captured')
405
- }
406
- }
407
- }
package/src/error.ts DELETED
@@ -1,198 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // Failures, with stable codes.
4
- //
5
- // The code is the contract: scripts and agents branch on it, and every
6
- // renderer, transducer and host uses the same set. The message, path and
7
- // limit are informative. A code is never renamed, removed or repurposed;
8
- // one may be added.
9
-
10
- const CODES = [
11
- 'DSL_PARSE_ERROR',
12
- 'DSL_TYPE_ERROR',
13
- 'STREAM_REUSED',
14
- 'STREAMABILITY_UNKNOWN',
15
- 'INPUT_ORDER_VIOLATION',
16
- 'CAPTURE_OVERLAP_UNSUPPORTED',
17
- 'MISSING_VALUE',
18
- 'DUPLICATE_MEMBER',
19
- 'INVALID_NUMBER',
20
- 'PROTOCOL_ORDER_ERROR',
21
- 'TARGET_VALUE_UNREPRESENTABLE',
22
- 'RESOURCE_LIMIT_EXCEEDED',
23
- 'INPUT_INVALID',
24
- 'OUTPUT_FAILED',
25
- 'ABORTED',
26
- ] as const
27
-
28
- // One of the stable failure codes, written in `SCREAMING_SNAKE_CASE`.
29
- export type Code = (typeof CODES)[number]
30
-
31
- type CodeTable = { readonly [K in Code]: K } & {
32
- // Every code, in declaration order (the Rust `Code::ALL`).
33
- readonly ALL: readonly Code[]
34
- // A code by its written form, or `undefined`.
35
- parse(text: string): Code | undefined
36
- }
37
-
38
- const table: any = {}
39
- for (const c of CODES) table[c] = c
40
- table.ALL = Object.freeze([...CODES])
41
- table.parse = (text: string): Code | undefined =>
42
- (CODES as readonly string[]).includes(text) ? (text as Code) : undefined
43
-
44
- // The codes as constants: `Code.INPUT_INVALID`, and `Code.ALL`.
45
- export const Code: CodeTable = Object.freeze(table)
46
-
47
- // The limit a `RESOURCE_LIMIT_EXCEEDED` failure names.
48
- export type Limit = {
49
- // The `Limits` field, as written there (`max_record_bytes`).
50
- name: string
51
- value: number
52
- }
53
-
54
- // A failure: a code, and what is known about where and why.
55
- //
56
- // `committedOutput` says whether bytes had already been written when the
57
- // failure was found: an incremental export cannot take them back, and the
58
- // caller must be told the output may be partial.
59
- export class Fail extends Error {
60
- code: Code
61
- // The input path the failure concerns, in jq syntax, when one applies.
62
- path?: string
63
- limit?: Limit
64
- // 1-based source row and column, when the failure has a position.
65
- row?: number
66
- col?: number
67
- // The source file the position is in, when the program the failure came
68
- // from was compiled from several.
69
- file?: string
70
- committedOutput: boolean
71
-
72
- constructor(code: Code, message: string) {
73
- super(message)
74
- this.name = 'Fail'
75
- this.code = code
76
- this.committedOutput = false
77
- }
78
-
79
- // The failure with the source file its position is in.
80
- inFile(file: string): this {
81
- this.file = file
82
- return this
83
- }
84
-
85
- atPath(path: string): this {
86
- this.path = path
87
- return this
88
- }
89
-
90
- at(row: number, col: number): this {
91
- this.row = row
92
- this.col = col
93
- return this
94
- }
95
-
96
- committed(): this {
97
- this.committedOutput = true
98
- return this
99
- }
100
-
101
- // A limit failure, named after the `Limits` field that was passed.
102
- static limit(name: string, value: number, message: string): Fail {
103
- const f = new Fail('RESOURCE_LIMIT_EXCEEDED', message)
104
- f.limit = { name, value }
105
- return f
106
- }
107
-
108
- static protocol(message: string): Fail {
109
- return new Fail('PROTOCOL_ORDER_ERROR', message)
110
- }
111
-
112
- static input(message: string): Fail {
113
- return new Fail('INPUT_INVALID', message)
114
- }
115
-
116
- static output(message: string): Fail {
117
- return new Fail('OUTPUT_FAILED', message)
118
- }
119
-
120
- static aborted(): Fail {
121
- return new Fail('ABORTED', 'the run was cancelled')
122
- }
123
-
124
- // The engine's own error, as an input failure carrying its code,
125
- // position and report.
126
- static fromTabnas(e: any): Fail {
127
- const f = new Fail('INPUT_INVALID', `${engineCode(e)}: ${engineDetail(e)}`)
128
- const pos = enginePosition(e)
129
- if (pos) {
130
- f.row = pos.row
131
- f.col = pos.col
132
- }
133
- return f
134
- }
135
-
136
- // The failure as a plain object: `code`, `message`, and `path`, `limit`
137
- // (`{name, value}`), `row`, `col`, `file`, `output` ("partial" or
138
- // "none") when they apply. This is the shape hosts print.
139
- toJSON(): Record<string, unknown> {
140
- const out: Record<string, unknown> = {
141
- code: this.code,
142
- message: this.message,
143
- }
144
- if (null != this.path) out.path = this.path
145
- if (null != this.limit) out.limit = { name: this.limit.name, value: this.limit.value }
146
- if (null != this.row) out.row = this.row
147
- if (null != this.col) out.col = this.col
148
- if (null != this.file) out.file = this.file
149
- out.output = this.committedOutput ? 'partial' : 'none'
150
- return out
151
- }
152
-
153
- toString(): string {
154
- let s = `${this.code}: ${this.message}`
155
- if (null != this.path) s += ` at ${this.path}`
156
- if (null != this.file && null != this.row && null != this.col) {
157
- s += ` (${this.file}:${this.row}:${this.col})`
158
- } else if (null != this.row && null != this.col) {
159
- s += ` (${this.row}:${this.col})`
160
- } else if (null != this.file) {
161
- s += ` (in ${this.file})`
162
- }
163
- if (null != this.limit) s += ` [${this.limit.name} = ${this.limit.value}]`
164
- return s
165
- }
166
- }
167
-
168
- // Whether a thrown value is a `Fail`.
169
- export function isFail(e: unknown): e is Fail {
170
- return e instanceof Fail
171
- }
172
-
173
- // The engine error's code (`unexpected`, `cancel`, ...).
174
- export function engineCode(e: any): string {
175
- return 'string' === typeof e?.code ? e.code : 'unknown'
176
- }
177
-
178
- // The engine error's one-line description, without the code.
179
- export function engineDetail(e: any): string {
180
- let detail = ''
181
- try {
182
- if ('function' === typeof e?.toJSON) detail = e.toJSON()?.message ?? ''
183
- } catch (_err) {
184
- detail = ''
185
- }
186
- if ('' === detail) detail = String(e?.message ?? e)
187
- return detail.trimEnd()
188
- }
189
-
190
- // The engine error's 1-based position, when it has one.
191
- export function enginePosition(e: any): { row: number; col: number } | undefined {
192
- const row = e?.lineNumber
193
- const col = e?.columnNumber
194
- if ('number' === typeof row && 0 < row) {
195
- return { row, col: 'number' === typeof col ? col : 0 }
196
- }
197
- return undefined
198
- }