@jarenjs/json 0.9.2 → 0.34.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 (74) hide show
  1. package/ARCHITECTURE.md +86 -13
  2. package/README.md +248 -23
  3. package/dist/types/canonical.d.ts +37 -0
  4. package/dist/types/cow.d.ts +28 -0
  5. package/dist/types/errors.d.ts +45 -0
  6. package/dist/types/index.d.ts +3 -0
  7. package/dist/types/jslt/errors.d.ts +15 -8
  8. package/dist/types/jslt/index.d.ts +22 -0
  9. package/dist/types/jslt/packs/finance.d.ts +119 -0
  10. package/dist/types/jslt/packs/index.d.ts +310 -0
  11. package/dist/types/jslt/packs/math.d.ts +159 -0
  12. package/dist/types/jslt/packs/stats.d.ts +48 -0
  13. package/dist/types/jslt/registry.d.ts +65 -0
  14. package/dist/types/jtlt/errors.d.ts +3 -6
  15. package/dist/types/option-variants.d.ts +29 -0
  16. package/dist/types/patch.d.ts +214 -0
  17. package/dist/types/path.d.ts +139 -9
  18. package/dist/types/pointer.d.ts +100 -9
  19. package/dist/types/query/compile.d.ts +12 -0
  20. package/dist/types/query/errors.d.ts +72 -8
  21. package/dist/types/query/index.d.ts +317 -25
  22. package/dist/types/query/normalize.d.ts +24 -0
  23. package/dist/types/query/operators.d.ts +241 -1
  24. package/dist/types/query/runtime.d.ts +5 -8
  25. package/dist/types/query/types.d.ts +34 -0
  26. package/dist/types/segments.d.ts +31 -0
  27. package/dist/types/write.d.ts +204 -0
  28. package/dist/types/xquery/parse.d.ts +2 -3
  29. package/docs/JSLT-FORMAT.md +74 -3
  30. package/docs/JSLT-PRELUDE.md +1 -1
  31. package/docs/QUERY-FORMAT.md +695 -33
  32. package/package.json +18 -4
  33. package/schemas/geojson.draft-07.schema.json +323 -0
  34. package/schemas/geojson.jaren.schema.json +863 -0
  35. package/schemas/geojson.schema.json +172 -0
  36. package/schemas/jaren-jslt.authoring.schema.json +142 -0
  37. package/schemas/jaren-jslt.draft-07.schema.json +152 -11
  38. package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
  39. package/schemas/jaren-jslt.schema.json +152 -11
  40. package/schemas/jaren-query.draft-07.schema.json +152 -11
  41. package/schemas/jaren-query.llm-profile.schema.json +619 -0
  42. package/schemas/jaren-query.schema.json +82 -15
  43. package/src/basic.js +1 -1
  44. package/src/canonical.js +170 -0
  45. package/src/cow.js +106 -0
  46. package/src/errors.js +68 -0
  47. package/src/index.js +3 -0
  48. package/src/jslt/dispatch.js +178 -28
  49. package/src/jslt/errors.js +19 -14
  50. package/src/jslt/index.js +37 -29
  51. package/src/jslt/packs/finance.js +49 -0
  52. package/src/jslt/packs/index.js +18 -0
  53. package/src/jslt/packs/math.js +46 -0
  54. package/src/jslt/packs/stats.js +65 -0
  55. package/src/jslt/registry.js +200 -0
  56. package/src/jslt/stylesheet.js +14 -23
  57. package/src/jtlt/desugar.js +2 -3
  58. package/src/jtlt/errors.js +6 -12
  59. package/src/jtlt/index.js +12 -29
  60. package/src/jtlt/template.js +9 -18
  61. package/src/option-variants.js +54 -0
  62. package/src/patch.js +1052 -0
  63. package/src/path.js +319 -52
  64. package/src/pointer.js +225 -44
  65. package/src/query/compile.js +790 -75
  66. package/src/query/errors.js +72 -12
  67. package/src/query/index.js +274 -42
  68. package/src/query/normalize.js +489 -78
  69. package/src/query/operators.js +620 -23
  70. package/src/query/runtime.js +5 -19
  71. package/src/query/types.js +213 -0
  72. package/src/segments.js +409 -64
  73. package/src/write.js +660 -0
  74. package/src/xquery/parse.js +37 -53
@@ -14,6 +14,7 @@ import {
14
14
  import {
15
15
  CARD_MANY,
16
16
  normalizeQuery,
17
+ hostFailureText,
17
18
  } from '../query/normalize.js';
18
19
  import {
19
20
  compileNode,
@@ -33,6 +34,7 @@ import {
33
34
  JsltCompileError,
34
35
  JsltRuntimeError,
35
36
  } from './errors.js';
37
+ import { setObjectMember } from '@jarenjs/core/object';
36
38
 
37
39
  const hasOwn = Object.hasOwn;
38
40
  const NO_RULE = Symbol('Jslt.NoRule');
@@ -46,15 +48,15 @@ function composeDocPath(base, inner) {
46
48
  return inner.length === 0 ? base : base + inner;
47
49
  }
48
50
 
49
- function errorText(error) {
50
- return error instanceof Error ? error.message : String(error);
51
- }
51
+ // the one total host-failure projection of the package (see
52
+ // query/normalize.js): no raw .message read, no user coercion, no
53
+ // proxy-observable reflection
52
54
 
53
55
  // One compiled query per distinct match-path source string: rules across
54
56
  // modes matching the same path share the object, so the per-call
55
57
  // `.paths(root)` enumeration is computed once per transform (see
56
58
  // getQueryPaths), not once per mode.
57
- function compileMatchPath(rule, pathQueryCache) {
59
+ function compileMatchPath(rule, pathQueryCache, pathOptions) {
58
60
  const match = rule.match;
59
61
  if (match === null || match.path === null)
60
62
  return null;
@@ -62,7 +64,8 @@ function compileMatchPath(rule, pathQueryCache) {
62
64
  if (cached !== undefined)
63
65
  return cached;
64
66
  try {
65
- const query = compileJSONPath(match.path);
67
+ const query = compileJSONPath(match.path,
68
+ pathOptions === undefined ? undefined : { pathFunctions: pathOptions.pathFunctions });
66
69
  pathQueryCache.set(match.path, query);
67
70
  return query;
68
71
  }
@@ -89,7 +92,7 @@ function compileMatchSchema(rule, compileTypeTest) {
89
92
  }
90
93
  catch (error) {
91
94
  throw new JsltCompileError('JT0005',
92
- `invalid match schema: ${errorText(error)}`, match.schemaDocPath, error);
95
+ `invalid match schema: ${hostFailureText(error)}`, match.schemaDocPath, error);
93
96
  }
94
97
  if (typeof test !== 'function') {
95
98
  const cause = new TypeError('the type-test compiler did not return a predicate function');
@@ -112,7 +115,6 @@ function appendDispatched(acc, selected, targetMode, depth, tctx, dispatch) {
112
115
 
113
116
  function createApplyEntry(ruleBox, tableBox, targetModes) {
114
117
  return {
115
- result: () => CARD_MANY,
116
118
  normalize(arg, docPath, opPath, scope, ctx, helpers) {
117
119
  let selector;
118
120
  let targetMode = ruleBox.mode;
@@ -130,6 +132,7 @@ function createApplyEntry(ruleBox, tableBox, targetModes) {
130
132
  }
131
133
  }
132
134
  targetModes.add(targetMode);
135
+ ruleBox.targets.add(targetMode);
133
136
  const args = [selector];
134
137
  if (Array.isArray(arg) && arg.length === 2) // the explicit-mode form
135
138
  args.push(helpers.makeRaw(targetMode, opPath + '/1'));
@@ -186,12 +189,13 @@ function wrapBodyCompileError(rule, error) {
186
189
  composeDocPath(rule.bodyDocPath, error.docPath), error);
187
190
  }
188
191
 
189
- function compileBody(rule, compileTypeTest, tableBox, targetModes) {
192
+ function compileBody(rule, compileTypeTest, tableBox, targetModes, pathOptions) {
190
193
  const ruleBox = {
191
194
  mode: rule.mode,
192
195
  locSlot: -1,
193
196
  depthSlot: -1,
194
197
  tctxSlot: -1,
198
+ targets: new Set(), // the modes this body's $apply calls dispatch into
195
199
  };
196
200
  const applyEntry = createApplyEntry(ruleBox, tableBox, targetModes);
197
201
  let normalized;
@@ -199,7 +203,13 @@ function compileBody(rule, compileTypeTest, tableBox, targetModes) {
199
203
  try {
200
204
  normalized = normalizeQuery(rule.body, {
201
205
  compileTypeTest,
202
- extensions: { '$apply': applyEntry },
206
+ // the host's registered operators merge in, but $apply ALWAYS wins
207
+ // (a pack cannot shadow the dispatch operator)
208
+ extensions: pathOptions === undefined || pathOptions.extensions === undefined
209
+ ? { '$apply': applyEntry }
210
+ : { ...pathOptions.extensions, '$apply': applyEntry },
211
+ functions: pathOptions === undefined ? undefined : pathOptions.functions,
212
+ pathFunctions: pathOptions === undefined ? undefined : pathOptions.pathFunctions,
203
213
  });
204
214
  ruleBox.locSlot = normalized.frameSize;
205
215
  ruleBox.depthSlot = normalized.frameSize + 1;
@@ -241,9 +251,130 @@ function compileBody(rule, compileTypeTest, tableBox, targetModes) {
241
251
  pathSlot,
242
252
  readsPath,
243
253
  userExternals,
254
+ targets: ruleBox.targets,
244
255
  };
245
256
  }
246
257
 
258
+ //#region body memoization (the `memo` option)
259
+
260
+ // Ref-keyed body memoization: for an eligible rule, the same (location,
261
+ // value reference) pair MUST produce the same output, so the previous
262
+ // output can be returned by reference. Combined with copy-on-write state
263
+ // updates upstream and a reference-equality fast path downstream (the
264
+ // @jarenjs/view patcher), unchanged subtrees render in O(1) frame over
265
+ // frame. Eligibility is decided entirely at compile time:
266
+ //
267
+ // - the body reads neither $root, $path nor user externals (those make
268
+ // output depend on more than the matched value), and
269
+ // - every mode reachable through the body's $apply calls is STABLE: all
270
+ // of its rules are themselves body-clean, and their match paths do
271
+ // not reference the root inside a filter (a second '$' in the path
272
+ // source) - selection there depends only on (location, value), so
273
+ // the child dispatches frozen inside a cached output stay correct.
274
+ //
275
+ // The cache is generational (two maps, swapped per transform call):
276
+ // entries unused for one full transform are dropped, bounding retention
277
+ // to the size of the live output.
278
+
279
+ function computeMemoEligibility(temporary) {
280
+ const modeRules = new Map();
281
+ for (let i = 0; i < temporary.length; i++) {
282
+ const mode = temporary[i].rule.mode;
283
+ const list = modeRules.get(mode);
284
+ if (list === undefined)
285
+ modeRules.set(mode, [i]);
286
+ else
287
+ list.push(i);
288
+ }
289
+
290
+ const bodyClean = new Array(temporary.length);
291
+ const selectionStable = new Array(temporary.length);
292
+ for (let i = 0; i < temporary.length; i++) {
293
+ const body = temporary[i].body;
294
+ bodyClean[i] = body.rootSlot < 0 && !body.readsPath && body.userExternals.length === 0;
295
+ const match = temporary[i].rule.match;
296
+ const path = match === null ? null : match.path;
297
+ selectionStable[i] = path === null || path.indexOf('$', 1) === -1;
298
+ }
299
+
300
+ // modes with no rules fall to the built-in dispositions, which depend
301
+ // only on (location, value): vacuously stable
302
+ const modeOk = new Map();
303
+ for (const name of modeRules.keys())
304
+ modeOk.set(name, true);
305
+ const ok = (name) => modeOk.get(name) !== false;
306
+
307
+ let changed = true;
308
+ while (changed) {
309
+ changed = false;
310
+ for (const [name, indexes] of modeRules) {
311
+ if (modeOk.get(name) === false)
312
+ continue;
313
+ let good = true;
314
+ for (let j = 0; j < indexes.length && good; j++) {
315
+ const i = indexes[j];
316
+ if (!bodyClean[i] || !selectionStable[i]) {
317
+ good = false;
318
+ break;
319
+ }
320
+ for (const target of temporary[i].body.targets) {
321
+ if (!ok(target)) {
322
+ good = false;
323
+ break;
324
+ }
325
+ }
326
+ }
327
+ if (!good) {
328
+ modeOk.set(name, false);
329
+ changed = true;
330
+ }
331
+ }
332
+ }
333
+
334
+ const eligible = new Array(temporary.length);
335
+ for (let i = 0; i < temporary.length; i++) {
336
+ let good = bodyClean[i];
337
+ if (good) {
338
+ for (const target of temporary[i].body.targets) {
339
+ if (!ok(target)) {
340
+ good = false;
341
+ break;
342
+ }
343
+ }
344
+ }
345
+ eligible[i] = good;
346
+ }
347
+ return eligible;
348
+ }
349
+
350
+ function memoizeBodyEvaluator(evaluate, memoCaches) {
351
+ const box = { current: new Map(), previous: new Map() };
352
+ memoCaches.push(box);
353
+ return (value, loc, depth, tctx) => {
354
+ if (loc === null)
355
+ return evaluate(value, loc, depth, tctx);
356
+ let entry = box.current.get(loc);
357
+ if (entry === undefined) {
358
+ entry = box.previous.get(loc);
359
+ if (entry !== undefined)
360
+ box.current.set(loc, entry);
361
+ }
362
+ if (entry !== undefined && entry.value === value)
363
+ return entry.output;
364
+ const output = evaluate(value, loc, depth, tctx);
365
+ if (entry !== undefined) {
366
+ entry.value = value;
367
+ entry.output = output;
368
+ }
369
+ else {
370
+ box.current.set(loc, { value, output });
371
+ }
372
+ return output;
373
+ };
374
+ }
375
+
376
+ //#endregion
377
+
247
378
  function makeBodyEvaluator(rule, body, userSlots) {
248
379
  const {
249
380
  bodyGet,
@@ -290,7 +421,7 @@ function makeBodyEvaluator(rule, body, userSlots) {
290
421
  };
291
422
  }
292
423
 
293
- function compileRules(model, compileTypeTest, tableBox, targetModes) {
424
+ function compileRules(model, compileTypeTest, tableBox, targetModes, memoEnabled, pathOptions) {
294
425
  const rules = model.rules;
295
426
  const temporary = new Array(rules.length);
296
427
  const externalNames = [];
@@ -301,11 +432,11 @@ function compileRules(model, compileTypeTest, tableBox, targetModes) {
301
432
  let pathRuleCount = 0;
302
433
  for (let i = 0; i < rules.length; i++) {
303
434
  const rule = rules[i];
304
- const pathQuery = compileMatchPath(rule, pathQueryCache);
435
+ const pathQuery = compileMatchPath(rule, pathQueryCache, pathOptions);
305
436
  if (pathQuery !== null)
306
437
  pathRuleCount++;
307
438
  const test = compileMatchSchema(rule, compileTypeTest);
308
- const body = compileBody(rule, compileTypeTest, tableBox, targetModes);
439
+ const body = compileBody(rule, compileTypeTest, tableBox, targetModes, pathOptions);
309
440
  readsPath = readsPath || body.readsPath;
310
441
  const userExternals = body.userExternals;
311
442
  for (let j = 0; j < userExternals.length; j++) {
@@ -322,6 +453,9 @@ function compileRules(model, compileTypeTest, tableBox, targetModes) {
322
453
  for (let i = 0; i < externalNames.length; i++)
323
454
  externalIndexes.set(externalNames[i], i);
324
455
 
456
+ const memoEligible = memoEnabled ? computeMemoEligibility(temporary) : null;
457
+ const memoCaches = [];
458
+
325
459
  const compiled = new Array(rules.length);
326
460
  for (let i = 0; i < temporary.length; i++) {
327
461
  const item = temporary[i];
@@ -334,11 +468,14 @@ function compileRules(model, compileTypeTest, tableBox, targetModes) {
334
468
  }
335
469
  const frozenSlots = Object.freeze(userSlots);
336
470
  const frozenIndexes = Object.freeze(userIndexes);
471
+ let bodyEval = makeBodyEvaluator(item.rule, item.body, frozenSlots);
472
+ if (memoEligible !== null && memoEligible[i])
473
+ bodyEval = memoizeBodyEvaluator(bodyEval, memoCaches);
337
474
  compiled[i] = Object.freeze({
338
475
  index: item.rule.index,
339
476
  pathQuery: item.pathQuery,
340
477
  test: item.test,
341
- bodyEval: makeBodyEvaluator(item.rule, item.body, frozenSlots),
478
+ bodyEval,
342
479
  userSlots: frozenSlots,
343
480
  userIndexes: frozenIndexes,
344
481
  });
@@ -348,6 +485,7 @@ function compileRules(model, compileTypeTest, tableBox, targetModes) {
348
485
  rules: Object.freeze(compiled),
349
486
  externals: Object.freeze(externalNames),
350
487
  readsPath,
488
+ memoCaches,
351
489
  // rules sharing one match path (across modes, the TOC/render idiom)
352
490
  // enumerate it once per transform call through tctx.queryPaths
353
491
  sharedQueries: pathRuleCount > pathQueryCache.size,
@@ -588,20 +726,6 @@ function scanSetRules(mode, value, loc, depth, tctx, matched) {
588
726
  return NO_RULE;
589
727
  }
590
728
 
591
- function setObjectMember(out, name, value) {
592
- if (name === '__proto__') {
593
- Object.defineProperty(out, name, {
594
- value,
595
- enumerable: true,
596
- configurable: true,
597
- writable: true,
598
- });
599
- }
600
- else {
601
- out[name] = value;
602
- }
603
- }
604
-
605
729
  function describeLocation(loc) {
606
730
  return loc === null ? 'a location-less value' : loc;
607
731
  }
@@ -813,13 +937,30 @@ export function compileJsltDispatch(model, options = {}) {
813
937
  const maxDepth = options.maxDepth === undefined ? 1024 : options.maxDepth;
814
938
  if (!Number.isInteger(maxDepth) || maxDepth < 0)
815
939
  throw new TypeError('options.maxDepth must be a non-negative integer');
940
+ const memoEnabled = options.memo === true;
941
+ // JSONPath function extensions reach both places a stylesheet embeds
942
+ // a path: the rules' match paths, and the path strings inside rule
943
+ // bodies (through the query normalizer).
944
+ // options carried into each rule body's normalizeQuery: the RFC 9535
945
+ // path-function registry, plus the host's registered operators
946
+ // (options.extensions) and $call functions (options.functions) — the
947
+ // JSLT operator registry (registry.js) flows in exactly here. Built
948
+ // whenever ANY of the three is present.
949
+ const bodyOptions = (options.pathFunctions == null
950
+ && options.extensions == null && options.functions == null)
951
+ ? undefined
952
+ : {
953
+ pathFunctions: options.pathFunctions ?? undefined,
954
+ extensions: options.extensions ?? undefined,
955
+ functions: options.functions ?? undefined,
956
+ };
816
957
 
817
958
  const tableBox = {
818
959
  dispatch: null,
819
960
  needsLoc: false,
820
961
  };
821
962
  const targetModes = new Set();
822
- const compiled = compileRules(model, compileTypeTest, tableBox, targetModes);
963
+ const compiled = compileRules(model, compileTypeTest, tableBox, targetModes, memoEnabled, bodyOptions);
823
964
  tableBox.needsLoc = model.anyPathRule || compiled.readsPath;
824
965
  const built = buildModes(model, compiled.rules, targetModes);
825
966
  const modes = built.modes;
@@ -896,12 +1037,21 @@ export function compileJsltDispatch(model, options = {}) {
896
1037
  const externalNames = compiled.externals;
897
1038
  const compiledRules = compiled.rules;
898
1039
  const sharedQueries = compiled.sharedQueries;
1040
+ const memoCaches = compiled.memoCaches;
899
1041
  const rootLoc = tableBox.needsLoc ? '$' : null;
900
1042
  // the all-unbound resolution is a compile-time constant; calls without
901
1043
  // user bindings (the common case) share it instead of re-resolving
902
1044
  const unboundRuleValues = resolveExternalValues(externalNames, compiledRules, null);
903
1045
  return Object.freeze({
904
1046
  evaluate(data, ext) {
1047
+ // generation swap: entries unused for one full transform retire
1048
+ for (let i = 0; i < memoCaches.length; i++) {
1049
+ const box = memoCaches[i];
1050
+ const retired = box.previous;
1051
+ box.previous = box.current;
1052
+ retired.clear();
1053
+ box.current = retired;
1054
+ }
905
1055
  const tctx = {
906
1056
  root: data,
907
1057
  ruleExternalValues: ext == null || externalNames.length === 0
@@ -3,17 +3,26 @@
3
3
  // `code` and a `docPath`, an RFC 6901 JSON Pointer into the stylesheet
4
4
  // document. Wrapped parser/query/hook errors are exposed through `cause`.
5
5
 
6
+ import { CodedError } from '@jarenjs/core/errors';
7
+ import { CodedDocPathError } from '../errors.js';
8
+
6
9
  /**
7
10
  * Error thrown when a JSLT stylesheet is rejected at compile time
8
11
  * (`JT0xxx` codes).
9
12
  */
10
- export class JsltCompileError extends Error {
11
- constructor(code, message, docPath, cause = undefined) {
12
- super(`${code}: ${message} at ${docPath}`,
13
- cause === undefined ? undefined : { cause });
14
- this.name = 'JsltCompileError';
15
- this.code = code;
16
- this.docPath = docPath;
13
+ export class JsltCompileError extends CodedError {
14
+ /**
15
+ * @param {string} code
16
+ * @param {string} reason
17
+ * @param {string} docPath
18
+ * @param {...unknown} cause - When a fourth argument is passed AT
19
+ * ALL, it is retained as an own `cause` — even `undefined`, so a
20
+ * host hook that threw `undefined` stays distinguishable from "no
21
+ * cause".
22
+ */
23
+ constructor(code, reason, docPath, ...cause) {
24
+ super('JsltCompileError', code, reason, docPath,
25
+ cause.length > 0 ? { cause: cause[0] } : undefined);
17
26
  }
18
27
  }
19
28
 
@@ -21,13 +30,9 @@ export class JsltCompileError extends Error {
21
30
  * Error thrown when evaluating a compiled JSLT stylesheet fails
22
31
  * (`JT2xxx` codes).
23
32
  */
24
- export class JsltRuntimeError extends Error {
25
- constructor(code, message, docPath, cause = undefined) {
26
- super(`${code}: ${message} at ${docPath}`,
27
- cause === undefined ? undefined : { cause });
28
- this.name = 'JsltRuntimeError';
29
- this.code = code;
30
- this.docPath = docPath;
33
+ export class JsltRuntimeError extends CodedDocPathError {
34
+ constructor(code, reason, docPath, cause = undefined) {
35
+ super('JsltRuntimeError', code, reason, docPath, cause);
31
36
  }
32
37
  }
33
38
 
package/src/jslt/index.js CHANGED
@@ -5,10 +5,13 @@
5
5
 
6
6
  import { deepFreezeCopy } from '../query/normalize.js';
7
7
  import { EMPTY, Seq } from '../query/runtime.js';
8
+ import { createOptionVariantCache, identityOf } from '../option-variants.js';
8
9
  import { normalizeJsltStylesheet } from './stylesheet.js';
9
10
  import { compileJsltDispatch } from './dispatch.js';
10
11
 
11
12
  export { JsltCompileError, JsltRuntimeError } from './errors.js';
13
+ export { createJsltRegistry } from './registry.js';
14
+ export { mathPack, financePack, statsPack, allPacks } from './packs/index.js';
12
15
 
13
16
  /**
14
17
  * Compile a Jaren JSLT 0.1 stylesheet into a reusable transformation.
@@ -28,6 +31,20 @@ export { JsltCompileError, JsltRuntimeError } from './errors.js';
28
31
  * [options.compileTypeTest] - validator-agnostic hook compiling schema
29
32
  * match conditions and schema literals inside query bodies
30
33
  * @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
34
+ * @param {boolean} [options.memo=false] - memoize rule outputs by
35
+ * (location, value reference): across repeated transforms of
36
+ * copy-on-write-updated documents, unchanged subtrees return the
37
+ * PREVIOUS output by reference — the fuel for reference-equality
38
+ * fast paths downstream (the @jarenjs/view patcher). Only rules whose
39
+ * output provably depends on nothing but the matched value are cached
40
+ * (no $root/$path/user externals, transitively through $apply, and no
41
+ * root references inside match-path filters); everything else runs
42
+ * normally. Memoized outputs MUST be treated as immutable, and the
43
+ * cache retains the previous transform's outputs (two generations).
44
+ * @param {Record<string, import('../path.js').JSONPathFunction>}
45
+ * [options.pathFunctions] - custom JSONPath function extensions (RFC
46
+ * 9535 section 2.4), available in rule match paths and in the path
47
+ * strings inside rule bodies
31
48
  * @returns {function} reusable `transform(data, externals?)` function
32
49
  * @throws {import('./errors.js').JsltCompileError} when compilation fails
33
50
  * @example
@@ -53,41 +70,29 @@ export function compileJsltStylesheet(doc, options = {}) {
53
70
  return transform;
54
71
  }
55
72
 
56
- const STYLESHEET_CACHE = new WeakMap();
73
+ const STYLESHEET_CACHE = createOptionVariantCache();
57
74
 
58
75
  function cachedTransform(stylesheet, options) {
59
- let record = STYLESHEET_CACHE.get(stylesheet);
60
- if (record === undefined) {
61
- record = {
62
- defaultTransform: null,
63
- variants: null,
64
- };
65
- STYLESHEET_CACHE.set(stylesheet, record);
66
- }
67
-
76
+ // Every option that changes what compiles is part of the derived key,
77
+ // or a second call with different options silently reuses the first
78
+ // compilation. Hooks and registries compare by identity, so they are
79
+ // interned to per-process ids.
68
80
  const compileTypeTest = typeof options?.compileTypeTest === 'function'
69
81
  ? options.compileTypeTest
70
82
  : null;
71
83
  const maxDepth = options?.maxDepth === undefined ? 1024 : options.maxDepth;
72
- if (compileTypeTest === null && maxDepth === 1024) {
73
- if (record.defaultTransform === null)
74
- record.defaultTransform = compileJsltStylesheet(stylesheet);
75
- return record.defaultTransform;
76
- }
77
-
78
- let variants = record.variants;
79
- if (variants === null) {
80
- variants = [];
81
- record.variants = variants;
82
- }
83
- for (let i = 0; i < variants.length; i++) {
84
- const variant = variants[i];
85
- if (variant.compileTypeTest === compileTypeTest && variant.maxDepth === maxDepth)
86
- return variant.transform;
87
- }
88
- const transform = compileJsltStylesheet(stylesheet, options);
89
- variants.push({ compileTypeTest, maxDepth, transform });
90
- return transform;
84
+ const memo = options?.memo === true;
85
+ const pathFunctions = options?.pathFunctions == null ? null : options.pathFunctions;
86
+ // registered operators/functions (the JSLT operator registry) change
87
+ // what compiles, so they join the key by identity — a registry hands a
88
+ // STABLE extensions/functions object per instance (registry.js), so
89
+ // two transforms with the same registry still hit the cache.
90
+ const extensions = options?.extensions == null ? null : options.extensions;
91
+ const functions = options?.functions == null ? null : options.functions;
92
+ const key = `${identityOf(compileTypeTest)}|${maxDepth}|${memo ? 1 : 0}`
93
+ + `|${identityOf(pathFunctions)}|${identityOf(extensions)}|${identityOf(functions)}`;
94
+ return STYLESHEET_CACHE.getOrCompile(stylesheet, key,
95
+ () => compileJsltStylesheet(stylesheet, options));
91
96
  }
92
97
 
93
98
  /**
@@ -101,6 +106,9 @@ function cachedTransform(stylesheet, options) {
101
106
  * @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
102
107
  * [options.compileTypeTest] - schema type-test compiler
103
108
  * @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
109
+ * @param {boolean} [options.memo=false] - memoize rule outputs
110
+ * @param {Record<string, import('../path.js').JSONPathFunction>}
111
+ * [options.pathFunctions] - custom JSONPath function extensions
104
112
  * @returns {any} `undefined`, one JSON item, or an array of result items
105
113
  * @throws {import('./errors.js').JsltCompileError} when compilation fails
106
114
  * @throws {import('./errors.js').JsltRuntimeError} when dispatch fails
@@ -0,0 +1,49 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The finance pack — parameterized aggregators over a sequence of
4
+ * numbers, wrapping the pure functions of `@jarenjs/core/finance`. Each
5
+ * `agg` entry declares which operands are folded sequences (`seq`) and
6
+ * which are scalars; the registry gathers the `seq` operands into arrays
7
+ * before the call (the fold contract). Whole-series functions (`irr`,
8
+ * `npv`, moving averages) are `pushable: false` — they run in the engine
9
+ * or the db residual, never as an index-eligible SQLite scalar UDF.
10
+ */
11
+
12
+ import {
13
+ npv, irr, mirr, fv, pv, pmt, cagr,
14
+ sma, ema, wma, rsi, roc,
15
+ volatility, sharpe, maxDrawdown,
16
+ } from '@jarenjs/core/finance';
17
+
18
+ const aggNum = (signature, fn) => ({ kind: 'agg', signature, result: 'number', fn, pushable: false });
19
+ const aggSeq = (signature, fn) => ({ kind: 'agg', signature, result: 'seq<number>', fn, pushable: false });
20
+ const opNum = (n, fn) => ({ kind: 'op', signature: new Array(n).fill('number'), result: 'number', fn, pushable: false });
21
+
22
+ export const financePack = {
23
+ name: 'finance',
24
+ entries: {
25
+ // present value of a cashflow series at a rate
26
+ $npv: aggNum(['number', 'seq<number>'], (rate, cashflows) => npv(rate, cashflows)),
27
+ // internal rate of return of a cashflow series
28
+ $irr: aggNum(['seq<number>'], (cashflows) => irr(cashflows)),
29
+ // modified IRR: series, finance rate, reinvest rate
30
+ $mirr: aggNum(['seq<number>', 'number', 'number'],
31
+ (cashflows, financeRate, reinvestRate) => mirr(cashflows, financeRate, reinvestRate)),
32
+ // time value of money (scalar operands; pv/type default)
33
+ $fv: opNum(3, (rate, nper, pmtv) => fv(rate, nper, pmtv)),
34
+ $pv: opNum(3, (rate, nper, pmtv) => pv(rate, nper, pmtv)),
35
+ $pmt: opNum(3, (rate, nper, pval) => pmt(rate, nper, pval)),
36
+ // compound annual growth rate
37
+ $cagr: opNum(3, (begin, end, years) => cagr(begin, end, years)),
38
+ // moving averages / indicators — return a series (null during warm-up)
39
+ $sma: aggSeq(['seq<number>', 'number'], (values, period) => sma(values, period)),
40
+ $ema: aggSeq(['seq<number>', 'number'], (values, period) => ema(values, period)),
41
+ $wma: aggSeq(['seq<number>', 'number'], (values, period) => wma(values, period)),
42
+ $rsi: aggSeq(['seq<number>', 'number'], (values, period) => rsi(values, period)),
43
+ $roc: aggSeq(['seq<number>', 'number'], (values, period) => roc(values, period)),
44
+ // risk / return summaries over a returns series
45
+ $volatility: aggNum(['seq<number>'], (returns) => volatility(returns)),
46
+ $sharpe: aggNum(['seq<number>'], (returns) => sharpe(returns)),
47
+ $maxDrawdown: aggNum(['seq<number>'], (values) => maxDrawdown(values)),
48
+ },
49
+ };
@@ -0,0 +1,18 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The built-in operator packs, plain data wrapping `@jarenjs/core`.
4
+ * A caller composes them into a registry: `createJsltRegistry().use(
5
+ * mathPack).use(financePack)`. `allPacks` is the convenience array for
6
+ * "register everything".
7
+ */
8
+
9
+ export { mathPack } from './math.js';
10
+ export { financePack } from './finance.js';
11
+ export { statsPack } from './stats.js';
12
+
13
+ import { mathPack } from './math.js';
14
+ import { financePack } from './finance.js';
15
+ import { statsPack } from './stats.js';
16
+
17
+ /** Every built-in pack, in a stable order. */
18
+ export const allPacks = Object.freeze([mathPack, financePack, statsPack]);
@@ -0,0 +1,46 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The math pack — scalar `$`-operators the core query vocabulary
4
+ * lacks, wrapping the pure f64 functions of `@jarenjs/core/math`. Every
5
+ * entry is `kind: 'op'` (scalar operands, scalar result). `pushable:
6
+ * 'scalar'` marks the ones Ring 3 MAY register as a SQLite UDF; it is
7
+ * ignored in Rings 1–2.
8
+ */
9
+
10
+ import {
11
+ mathf64_sqrt, mathf64_cbrt, mathf64_pow, mathf64_hypot, mathf64_sign,
12
+ mathf64_abs, mathf64_sin, mathf64_cos, mathf64_tan,
13
+ mathf64_asin, mathf64_acos, mathf64_atan, mathf64_atan2,
14
+ mathf64_sinh, mathf64_cosh, mathf64_tanh,
15
+ mathf64_log, mathf64_log2, mathf64_log10, mathf64_exp, mathf64_expm1,
16
+ } from '@jarenjs/core/math';
17
+
18
+ const op1 = (fn) => ({ kind: 'op', signature: ['number'], result: 'number', fn, pushable: 'scalar' });
19
+ const op2 = (fn) => ({ kind: 'op', signature: ['number', 'number'], result: 'number', fn, pushable: 'scalar' });
20
+
21
+ export const mathPack = {
22
+ name: 'math',
23
+ entries: {
24
+ $abs: op1(mathf64_abs),
25
+ $sign: op1(mathf64_sign),
26
+ $sqrt: op1(mathf64_sqrt),
27
+ $cbrt: op1(mathf64_cbrt),
28
+ $pow: op2(mathf64_pow),
29
+ $hypot: op2(mathf64_hypot),
30
+ $exp: op1(mathf64_exp),
31
+ $expm1: op1(mathf64_expm1),
32
+ $log: op1(mathf64_log),
33
+ $log2: op1(mathf64_log2),
34
+ $log10: op1(mathf64_log10),
35
+ $sin: op1(mathf64_sin),
36
+ $cos: op1(mathf64_cos),
37
+ $tan: op1(mathf64_tan),
38
+ $asin: op1(mathf64_asin),
39
+ $acos: op1(mathf64_acos),
40
+ $atan: op1(mathf64_atan),
41
+ $atan2: op2(mathf64_atan2),
42
+ $sinh: op1(mathf64_sinh),
43
+ $cosh: op1(mathf64_cosh),
44
+ $tanh: op1(mathf64_tanh),
45
+ },
46
+ };