@json-schema-engine/core 0.0.1

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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/coverage.d.ts +30 -0
  4. package/dist/coverage.d.ts.map +1 -0
  5. package/dist/coverage.js +81 -0
  6. package/dist/coverage.js.map +1 -0
  7. package/dist/cursor.d.ts +16 -0
  8. package/dist/cursor.d.ts.map +1 -0
  9. package/dist/cursor.js +28 -0
  10. package/dist/cursor.js.map +1 -0
  11. package/dist/dialect.d.ts +316 -0
  12. package/dist/dialect.d.ts.map +1 -0
  13. package/dist/dialect.js +135 -0
  14. package/dist/dialect.js.map +1 -0
  15. package/dist/engine.d.ts +203 -0
  16. package/dist/engine.d.ts.map +1 -0
  17. package/dist/engine.js +493 -0
  18. package/dist/engine.js.map +1 -0
  19. package/dist/index.d.ts +182 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +370 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/json.d.ts +49 -0
  24. package/dist/json.d.ts.map +1 -0
  25. package/dist/json.js +128 -0
  26. package/dist/json.js.map +1 -0
  27. package/dist/keywords/applicator.d.ts +57 -0
  28. package/dist/keywords/applicator.d.ts.map +1 -0
  29. package/dist/keywords/applicator.js +789 -0
  30. package/dist/keywords/applicator.js.map +1 -0
  31. package/dist/keywords/core.d.ts +42 -0
  32. package/dist/keywords/core.d.ts.map +1 -0
  33. package/dist/keywords/core.js +139 -0
  34. package/dist/keywords/core.js.map +1 -0
  35. package/dist/keywords/format.d.ts +43 -0
  36. package/dist/keywords/format.d.ts.map +1 -0
  37. package/dist/keywords/format.js +101 -0
  38. package/dist/keywords/format.js.map +1 -0
  39. package/dist/keywords/metaschemas2019.d.ts +3 -0
  40. package/dist/keywords/metaschemas2019.d.ts.map +1 -0
  41. package/dist/keywords/metaschemas2019.js +406 -0
  42. package/dist/keywords/metaschemas2019.js.map +1 -0
  43. package/dist/keywords/metaschemas2020.d.ts +3 -0
  44. package/dist/keywords/metaschemas2020.d.ts.map +1 -0
  45. package/dist/keywords/metaschemas2020.js +441 -0
  46. package/dist/keywords/metaschemas2020.js.map +1 -0
  47. package/dist/keywords/metaschemas6.d.ts +3 -0
  48. package/dist/keywords/metaschemas6.d.ts.map +1 -0
  49. package/dist/keywords/metaschemas6.js +221 -0
  50. package/dist/keywords/metaschemas6.js.map +1 -0
  51. package/dist/keywords/metaschemas7.d.ts +3 -0
  52. package/dist/keywords/metaschemas7.d.ts.map +1 -0
  53. package/dist/keywords/metaschemas7.js +248 -0
  54. package/dist/keywords/metaschemas7.js.map +1 -0
  55. package/dist/keywords/unevaluated.d.ts +14 -0
  56. package/dist/keywords/unevaluated.d.ts.map +1 -0
  57. package/dist/keywords/unevaluated.js +296 -0
  58. package/dist/keywords/unevaluated.js.map +1 -0
  59. package/dist/keywords/validation.d.ts +21 -0
  60. package/dist/keywords/validation.d.ts.map +1 -0
  61. package/dist/keywords/validation.js +332 -0
  62. package/dist/keywords/validation.js.map +1 -0
  63. package/dist/keywords/vocab2019.d.ts +36 -0
  64. package/dist/keywords/vocab2019.d.ts.map +1 -0
  65. package/dist/keywords/vocab2019.js +585 -0
  66. package/dist/keywords/vocab2019.js.map +1 -0
  67. package/dist/keywords/vocab2020.d.ts +20 -0
  68. package/dist/keywords/vocab2020.d.ts.map +1 -0
  69. package/dist/keywords/vocab2020.js +55 -0
  70. package/dist/keywords/vocab2020.js.map +1 -0
  71. package/dist/keywords/vocab7.d.ts +46 -0
  72. package/dist/keywords/vocab7.d.ts.map +1 -0
  73. package/dist/keywords/vocab7.js +305 -0
  74. package/dist/keywords/vocab7.js.map +1 -0
  75. package/dist/loader.d.ts +41 -0
  76. package/dist/loader.d.ts.map +1 -0
  77. package/dist/loader.js +13 -0
  78. package/dist/loader.js.map +1 -0
  79. package/dist/lowering.d.ts +371 -0
  80. package/dist/lowering.d.ts.map +1 -0
  81. package/dist/lowering.js +59 -0
  82. package/dist/lowering.js.map +1 -0
  83. package/dist/output.d.ts +251 -0
  84. package/dist/output.d.ts.map +1 -0
  85. package/dist/output.js +357 -0
  86. package/dist/output.js.map +1 -0
  87. package/dist/records.d.ts +71 -0
  88. package/dist/records.d.ts.map +1 -0
  89. package/dist/records.js +150 -0
  90. package/dist/records.js.map +1 -0
  91. package/dist/ref.d.ts +10 -0
  92. package/dist/ref.d.ts.map +1 -0
  93. package/dist/ref.js +5 -0
  94. package/dist/ref.js.map +1 -0
  95. package/dist/regex.d.ts +45 -0
  96. package/dist/regex.d.ts.map +1 -0
  97. package/dist/regex.js +163 -0
  98. package/dist/regex.js.map +1 -0
  99. package/dist/registry.d.ts +141 -0
  100. package/dist/registry.d.ts.map +1 -0
  101. package/dist/registry.js +407 -0
  102. package/dist/registry.js.map +1 -0
  103. package/dist/result.d.ts +109 -0
  104. package/dist/result.d.ts.map +1 -0
  105. package/dist/result.js +112 -0
  106. package/dist/result.js.map +1 -0
  107. package/dist/uri.d.ts +18 -0
  108. package/dist/uri.d.ts.map +1 -0
  109. package/dist/uri.js +27 -0
  110. package/dist/uri.js.map +1 -0
  111. package/dist/walk.d.ts +25 -0
  112. package/dist/walk.d.ts.map +1 -0
  113. package/dist/walk.js +65 -0
  114. package/dist/walk.js.map +1 -0
  115. package/package.json +32 -0
  116. package/src/coverage.ts +88 -0
  117. package/src/cursor.ts +42 -0
  118. package/src/dialect.ts +428 -0
  119. package/src/engine.ts +741 -0
  120. package/src/index.ts +699 -0
  121. package/src/json.ts +137 -0
  122. package/src/keywords/applicator.ts +895 -0
  123. package/src/keywords/core.ts +168 -0
  124. package/src/keywords/format.ts +143 -0
  125. package/src/keywords/metaschemas2019.ts +410 -0
  126. package/src/keywords/metaschemas2020.ts +443 -0
  127. package/src/keywords/metaschemas6.ts +226 -0
  128. package/src/keywords/metaschemas7.ts +253 -0
  129. package/src/keywords/unevaluated.ts +329 -0
  130. package/src/keywords/validation.ts +627 -0
  131. package/src/keywords/vocab2019.ts +676 -0
  132. package/src/keywords/vocab2020.ts +82 -0
  133. package/src/keywords/vocab7.ts +393 -0
  134. package/src/loader.ts +60 -0
  135. package/src/lowering.ts +414 -0
  136. package/src/output.ts +651 -0
  137. package/src/records.ts +226 -0
  138. package/src/ref.ts +14 -0
  139. package/src/regex.ts +171 -0
  140. package/src/registry.ts +481 -0
  141. package/src/result.ts +252 -0
  142. package/src/uri.ts +35 -0
  143. package/src/walk.ts +88 -0
@@ -0,0 +1,414 @@
1
+ // Compiler lowering IR (D1/D9, M6.1): the type vocabulary keyword behaviors
2
+ // use to describe their compiled form, and the LoweringContext service
3
+ // interface the compiler implements. Types only — core carries no compiler
4
+ // runtime. Keyword modules depend on this file, never on @json-schema-engine/compiler, so
5
+ // keyword knowledge stays in exactly one module per keyword.
6
+ //
7
+ // Two properties are load-bearing for security and semantics:
8
+ //
9
+ // 1. No IR node carries raw JavaScript text. Schema-derived data enters only
10
+ // as data nodes (constants, property keys, regex sources), which the
11
+ // compiler's gated serializer escapes. Injection is unrepresentable
12
+ // upstream of the serializer (D20/M6).
13
+ //
14
+ // 2. `combine` is EAGER: every part is evaluated, then the verdicts fold.
15
+ // This is channel rule 6 (DESIGN.md §4) in the IR contract — branches
16
+ // cannot short-circuit while a consumer or retained annotation could
17
+ // observe them. The serializer may emit short-circuits only where the
18
+ // compilation plan proves the region verdict-only.
19
+
20
+ import { JsonValue, JsonType } from "./json.js";
21
+
22
+ /** A value-producing IR expression. */
23
+ export type LowerExpr =
24
+ /** the instance value under evaluation at the lowering site */
25
+ | { readonly kind: "instance" }
26
+ /** a hoisted JSON constant (emitted via the gated formatter's json()) */
27
+ | { readonly kind: "const"; readonly value: JsonValue }
28
+ /** object member access by schema-derived key (bracket notation, escaped) */
29
+ | {
30
+ readonly kind: "member";
31
+ readonly target: LowerExpr;
32
+ readonly key: string;
33
+ }
34
+ /** array element access */
35
+ | {
36
+ readonly kind: "item";
37
+ readonly target: LowerExpr;
38
+ readonly index: LowerExpr;
39
+ }
40
+ /** a loop binding introduced by forEachKey/forEachIndex */
41
+ | { readonly kind: "binding"; readonly id: number }
42
+ /** JSON type test, including the "integer" refinement */
43
+ | {
44
+ readonly kind: "typeIs";
45
+ readonly target: LowerExpr;
46
+ readonly types: readonly (JsonType | "integer")[];
47
+ }
48
+ /** Object.hasOwn(target, key) — the only permitted membership test */
49
+ | {
50
+ readonly kind: "hasOwn";
51
+ readonly target: LowerExpr;
52
+ readonly key: LowerExpr | string;
53
+ }
54
+ /** numeric/string comparison of two expressions */
55
+ | {
56
+ readonly kind: "cmp";
57
+ readonly op: "<" | "<=" | ">" | ">=" | "===" | "!==";
58
+ readonly left: LowerExpr;
59
+ readonly right: LowerExpr;
60
+ }
61
+ /** call into the closed helper registry (never arbitrary code) */
62
+ | {
63
+ readonly kind: "helper";
64
+ readonly helper: LowerHelper;
65
+ readonly args: readonly LowerExpr[];
66
+ }
67
+ /** test a hoisted regex (compiled through the engine's RegexCache) */
68
+ | {
69
+ readonly kind: "regexTest";
70
+ readonly source: string;
71
+ readonly target: LowerExpr;
72
+ }
73
+ /** test a hoisted format definition (compiled through the engine's format table) */
74
+ | {
75
+ readonly kind: "formatTest";
76
+ readonly name: string;
77
+ readonly target: LowerExpr;
78
+ }
79
+ | { readonly kind: "not"; readonly expr: LowerExpr }
80
+ | {
81
+ readonly kind: "logic";
82
+ readonly op: "and" | "or";
83
+ readonly parts: readonly LowerExpr[];
84
+ }
85
+ /** the active combine-group tally (oneOf's match count) in a combineCheck message */
86
+ | { readonly kind: "tally" }
87
+ /**
88
+ * The passing branch INDEXES of the active combine group, as an array —
89
+ * combineCheck params only (oneOf reports which branches matched, not
90
+ * just how many; the ajv-compat mapping needs the indexes).
91
+ */
92
+ | { readonly kind: "tallyList" }
93
+ /**
94
+ * A subschema application used as a boolean expression rather than a
95
+ * statement (M6.4): `if`'s condition, `not`'s single negated apply, and
96
+ * `contains`'/`oneOf`'s per-branch probes all need the verdict as a value,
97
+ * not a verdict-folding statement. The serializer renders it as the same
98
+ * call expression `apply` statements use; `apply.fold` still governs how a
99
+ * caller that wraps this in a statement folds the result (e.g. `negate`,
100
+ * `exactlyOne`), while a bare `applyExpr` used purely for its value (e.g.
101
+ * as a `forEachIndex` counter guard) carries `fold: "discard"`.
102
+ */
103
+ | { readonly kind: "applyExpr"; readonly apply: LowerApply }
104
+ /**
105
+ * True when the folded coverage bound by a preceding `coverageFold`
106
+ * (identified by `fold`, its binding id) covers `target` — the swept
107
+ * property name (names half: set membership) or array index (indexes half:
108
+ * index below coveredPrefix, or index in coveredIdx). The consumer keyword's
109
+ * runtime sweep applies its subschema exactly where this is false, matching
110
+ * the interpreter's "skip already-evaluated" loop.
111
+ */
112
+ | {
113
+ readonly kind: "coverageCovers";
114
+ readonly fold: number;
115
+ readonly target: LowerExpr;
116
+ };
117
+
118
+ /**
119
+ * The closed set of runtime helpers emitted code may call. All are imported
120
+ * from the compiler's runtime module (re-exports of core functions) — never
121
+ * re-emitted per artifact, so compiled and interpreted tiers share one
122
+ * implementation of each semantic.
123
+ */
124
+ export type LowerHelper =
125
+ | "codePointLength"
126
+ | "jsonEqual"
127
+ | "canonicalKey"
128
+ | "escapeSegment"
129
+ | "keysOf" // Object.keys
130
+ | "lengthOf" // .length of a string or array (UTF-16 units / element count)
131
+ | "isMultipleOf"
132
+ | "hasDuplicateItems"
133
+ | "firstDuplicatePair";
134
+
135
+ /** A statement-level IR node. */
136
+ export type LowerStmt =
137
+ | {
138
+ readonly kind: "if";
139
+ readonly cond: LowerExpr;
140
+ readonly then: readonly LowerStmt[];
141
+ readonly else?: readonly LowerStmt[];
142
+ }
143
+ /** iterate own enumerable keys (Object.keys) binding each name */
144
+ | {
145
+ readonly kind: "forEachKey";
146
+ readonly target: LowerExpr;
147
+ readonly binding: number;
148
+ readonly body: readonly LowerStmt[];
149
+ }
150
+ /** iterate array indexes 0..length-1 (optionally from a constant start) */
151
+ | {
152
+ readonly kind: "forEachIndex";
153
+ readonly target: LowerExpr;
154
+ readonly binding: number;
155
+ readonly start?: number;
156
+ readonly body: readonly LowerStmt[];
157
+ }
158
+ /** report this keyword's assertion failure at the current cursor */
159
+ | {
160
+ readonly kind: "fail";
161
+ readonly message: LowerMessage;
162
+ readonly params?: LowerParams;
163
+ }
164
+ /**
165
+ * record this keyword's value as an annotation (channel rule 2); the
166
+ * serializer emits the schema constant, so the node carries no payload
167
+ */
168
+ | { readonly kind: "annotate" }
169
+ /** emit this keyword's dependency data (channel rule 2) */
170
+ | { readonly kind: "produce"; readonly value: LowerProduceValue }
171
+ /**
172
+ * Bind the folded coverage of the unit's runtime channel (consumer
173
+ * keywords only). The serializer supplies the channel — the flat array of
174
+ * raw dependency data from consumed producers, merged with mark/truncate
175
+ * at application boundaries — and folds it per `half`: "names" yields the
176
+ * evaluated-name set (foldNameCoverage), "indexes" yields the coveredPrefix/
177
+ * coveredIdx summary over the CURRENT instance array's length
178
+ * (foldIndexCoverage). The result is held in `binding` for a following
179
+ * `coverageCovers` to test. The compiled tier's alternative to
180
+ * `staticCoverage()`: emitted only when the planner licensed runtime
181
+ * evaluated-set tracking (LoweringContext.runtimeCoverage()).
182
+ */
183
+ | {
184
+ readonly kind: "coverageFold";
185
+ readonly half: "names" | "indexes";
186
+ readonly binding: number;
187
+ }
188
+ /**
189
+ * Apply a subschema and fold its verdict into the keyword verdict per
190
+ * `fold`. EAGER combine semantics (header note 2). `apply.cursor`
191
+ * identifies the instance position; the compiler owns frames, locations,
192
+ * and scope threading, exactly as the engine does for the interpreter.
193
+ */
194
+ | { readonly kind: "apply"; readonly apply: LowerApply }
195
+ /**
196
+ * Closes the immediately preceding run of anyMayPass/exactlyOne applies:
197
+ * the keyword fails (with `message`) when the run's combined verdict
198
+ * fails. Emitted by the keyword's lower() so failure text stays keyword
199
+ * knowledge (D1); the serializer folds it into the grouped check.
200
+ */
201
+ | {
202
+ readonly kind: "combineCheck";
203
+ readonly message: LowerMessage;
204
+ readonly params?: LowerParams;
205
+ }
206
+ /**
207
+ * `contains`'s shape: iterate array indexes 0..length-1 (like
208
+ * `forEachIndex`, binding each index), counting the iterations where
209
+ * `countWhen` holds true, then fail the keyword when the final count
210
+ * falls outside `[min, max]`. Every index is probed unconditionally (no
211
+ * short-circuit on reaching `max`), matching evaluate()'s full sweep.
212
+ * `countWhen` is typically an `applyExpr` with `fold: "discard"` (the
213
+ * per-item probe verdict feeds the count, never the keyword verdict
214
+ * directly — a failed probe is not itself a `contains` failure).
215
+ */
216
+ | {
217
+ readonly kind: "countRange";
218
+ readonly target: LowerExpr;
219
+ readonly binding: number;
220
+ readonly countWhen: LowerExpr;
221
+ /** accumulate the counted indexes for a following produce (contains' dependency data) */
222
+ readonly collectIndexes?: boolean;
223
+ readonly min: number;
224
+ readonly max: number;
225
+ readonly outOfRangeMessage: LowerMessage;
226
+ readonly outOfRangeParams?: LowerParams;
227
+ };
228
+
229
+ /** How a keyword's lowered body applies one subschema. */
230
+ export interface LowerApply {
231
+ /** subschema position relative to the keyword's schema object (matches StaticFacts.applications[].path, with loop bindings for dynamic segments) */
232
+ readonly path: readonly (string | number | { binding: number })[];
233
+ /**
234
+ * Set when the applied subschema is a sibling keyword's value (`if` →
235
+ * `then`/`else`), mirroring {@link SubschemaApplication.sibling} — the
236
+ * planner already resolves the edge this way (plan.ts); the serializer
237
+ * matches an apply back to its planned edge by keyword + sibling + path
238
+ * identity, so a keyword emitting more than one apply at the same `path`
239
+ * (e.g. `if`'s condition vs. its `then` edge, both `path: []`) MUST set
240
+ * this to disambiguate.
241
+ */
242
+ readonly sibling?: string;
243
+ /**
244
+ * For reference keywords: the reference value. The compiler resolves it
245
+ * at plan time against the unit's lexical base; `path` is ignored.
246
+ */
247
+ readonly ref?: string;
248
+ /** instance cursor for the application */
249
+ readonly cursor: LowerCursor;
250
+ /** failure message for folds that assert with their own error (negate) */
251
+ readonly message?: LowerMessage;
252
+ /** structured params accompanying `message` (D13) */
253
+ readonly params?: LowerParams;
254
+ /** how the application verdict folds into the keyword verdict */
255
+ readonly fold:
256
+ "allMustPass" | "anyMayPass" | "exactlyOne" | "negate" | "discard";
257
+ }
258
+
259
+ /** Instance cursor IR: the current node or a child of one. */
260
+ export type LowerCursor =
261
+ | { readonly kind: "here" }
262
+ | {
263
+ readonly kind: "child";
264
+ readonly of: LowerCursor;
265
+ readonly segment: LowerExpr | string | number;
266
+ }
267
+ /**
268
+ * `propertyNames`: the current loop binding's KEY STRING is the instance
269
+ * under evaluation, not a child of the object being iterated (there is no
270
+ * parent cursor to descend from — the property name itself is the value).
271
+ */
272
+ | { readonly kind: "key"; readonly binding: number };
273
+
274
+ /**
275
+ * An error message: literal parts joined with expression parts, escaped by
276
+ * the serializer. Mirrors the interpreter's message-builder output so the
277
+ * differential gate can compare error text exactly.
278
+ */
279
+ export type LowerMessage = readonly (string | LowerExpr)[];
280
+
281
+ /**
282
+ * Structured failure params (D13): each value is an expression so runtime
283
+ * pieces (a swept key, a tally, a duplicate pair) sit next to compile-time
284
+ * constants (`lowerIR.constant`). Mirrors the interpreter's
285
+ * `ctx.error(message, params)` so the differential gate can compare params
286
+ * exactly.
287
+ */
288
+ export type LowerParams = Readonly<Record<string, LowerExpr>>;
289
+
290
+ /**
291
+ * A dependency-data recipe: what a keyword's lowered body communicates to
292
+ * consumer keywords.
293
+ *
294
+ * Recipes read the accumulation the keyword's statements build against the
295
+ * instance. Within one keyword's lowered statement list, each executed
296
+ * child-of-here application (an `apply` or `applyExpr` whose cursor is
297
+ * `{kind:"child", of:{kind:"here"}, segment}`) contributes its segment —
298
+ * string segments to a name accumulation, numeric segments to an index
299
+ * accumulation — in execution order, deduplicated keeping the first
300
+ * occurrence. Applications count when ATTEMPTED, regardless of the
301
+ * subschema's verdict (the interpreter records the segment before applying).
302
+ */
303
+ export type LowerProduceValue =
304
+ /**
305
+ * The accumulated (deduped) name array, possibly empty. Object-shaped
306
+ * producers emit an empty array for an object and nothing for a non-object,
307
+ * so lower() must gate this produce behind an instance object-type check.
308
+ */
309
+ | { readonly kind: "collectedNames" }
310
+ /**
311
+ * The accumulated index array, rendered per `render`:
312
+ *
313
+ * - `"largestOrTrue"`: no accumulated indexes yields no production;
314
+ * otherwise `true` when the largest index + 1 equals the instance array
315
+ * length, else the largest accumulated index (a number).
316
+ * - `"appliedTrue"`: any accumulated index yields `true`; none yields no
317
+ * production.
318
+ * - `"matchedOrAllTrue"`: reads the keyword's countRange-counted indexes
319
+ * (see `collectIndexes`) — empty yields no production; a count equal to
320
+ * the instance array length yields `true`; otherwise the ascending index
321
+ * array.
322
+ */
323
+ | {
324
+ readonly kind: "collectedIndexes";
325
+ readonly render: "largestOrTrue" | "appliedTrue" | "matchedOrAllTrue";
326
+ };
327
+
328
+ /**
329
+ * Services available to one keyword's `lower()` (mirror of KeywordContext,
330
+ * D3): the only path to subschema application, hoisting, and the channel.
331
+ * Implemented by the compiler package; core defines the contract so keyword
332
+ * modules never import the compiler.
333
+ */
334
+ export interface LoweringContext {
335
+ /** the instance expression at this lowering site */
336
+ readonly instance: LowerExpr;
337
+ /** the keyword's containing schema object (plan-time data) */
338
+ readonly schema: Readonly<Record<string, JsonValue>>;
339
+ /**
340
+ * The static evaluated-coverage result for this schema object (D9a),
341
+ * for `unevaluated*` lowerings: null when any contributor is dynamic —
342
+ * the lowering must then fall back to runtime evaluated-set tracking.
343
+ */
344
+ staticCoverage(): {
345
+ names: readonly string[];
346
+ patterns: readonly string[];
347
+ coversAllNames: boolean;
348
+ prefixCount: number;
349
+ coversAllIndexes: boolean;
350
+ } | null;
351
+ /**
352
+ * True when the planner licensed RUNTIME evaluated-set tracking for this
353
+ * unit's consumer keywords: coverage flows through the unit's runtime
354
+ * channel and the keyword must emit coverageFold/coverageCovers instead of
355
+ * reading staticCoverage(). Exactly one of the two paths applies.
356
+ */
357
+ runtimeCoverage(): boolean;
358
+ /** append statements to the keyword's lowered body */
359
+ emit(...stmts: LowerStmt[]): void;
360
+ /** allocate a loop binding id for forEachKey/forEachIndex */
361
+ binding(): number;
362
+ }
363
+
364
+ /** Constructor shorthands for the common IR shapes. */
365
+ export const lowerIR = {
366
+ instance: { kind: "instance" } as LowerExpr,
367
+ constant: (value: JsonValue): LowerExpr => ({ kind: "const", value }),
368
+ typeIs: (
369
+ target: LowerExpr,
370
+ ...types: readonly (JsonType | "integer")[]
371
+ ): LowerExpr => ({ kind: "typeIs", target, types }),
372
+ not: (expr: LowerExpr): LowerExpr => ({ kind: "not", expr }),
373
+ and: (...parts: readonly LowerExpr[]): LowerExpr => ({
374
+ kind: "logic",
375
+ op: "and",
376
+ parts,
377
+ }),
378
+ or: (...parts: readonly LowerExpr[]): LowerExpr => ({
379
+ kind: "logic",
380
+ op: "or",
381
+ parts,
382
+ }),
383
+ regexTest: (source: string, target: LowerExpr): LowerExpr => ({
384
+ kind: "regexTest",
385
+ source,
386
+ target,
387
+ }),
388
+ formatTest: (name: string, target: LowerExpr): LowerExpr => ({
389
+ kind: "formatTest",
390
+ name,
391
+ target,
392
+ }),
393
+ helper: (helper: LowerHelper, ...args: readonly LowerExpr[]): LowerExpr => ({
394
+ kind: "helper",
395
+ helper,
396
+ args,
397
+ }),
398
+ cmp: (
399
+ op: "<" | "<=" | ">" | ">=" | "===" | "!==",
400
+ left: LowerExpr,
401
+ right: LowerExpr,
402
+ ): LowerExpr => ({ kind: "cmp", op, left, right }),
403
+ fail: (...message: LowerMessage): LowerStmt => ({ kind: "fail", message }),
404
+ failWith: (params: LowerParams, ...message: LowerMessage): LowerStmt => ({
405
+ kind: "fail",
406
+ message,
407
+ params,
408
+ }),
409
+ when: (
410
+ cond: LowerExpr,
411
+ then: readonly LowerStmt[],
412
+ elseStmts?: readonly LowerStmt[],
413
+ ): LowerStmt => ({ kind: "if", cond, then, else: elseStmts }),
414
+ };