@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.
- package/ARCHITECTURE.md +86 -13
- package/README.md +248 -23
- package/dist/types/canonical.d.ts +37 -0
- package/dist/types/cow.d.ts +28 -0
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/jslt/errors.d.ts +15 -8
- package/dist/types/jslt/index.d.ts +22 -0
- package/dist/types/jslt/packs/finance.d.ts +119 -0
- package/dist/types/jslt/packs/index.d.ts +310 -0
- package/dist/types/jslt/packs/math.d.ts +159 -0
- package/dist/types/jslt/packs/stats.d.ts +48 -0
- package/dist/types/jslt/registry.d.ts +65 -0
- package/dist/types/jtlt/errors.d.ts +3 -6
- package/dist/types/option-variants.d.ts +29 -0
- package/dist/types/patch.d.ts +214 -0
- package/dist/types/path.d.ts +139 -9
- package/dist/types/pointer.d.ts +100 -9
- package/dist/types/query/compile.d.ts +12 -0
- package/dist/types/query/errors.d.ts +72 -8
- package/dist/types/query/index.d.ts +317 -25
- package/dist/types/query/normalize.d.ts +24 -0
- package/dist/types/query/operators.d.ts +241 -1
- package/dist/types/query/runtime.d.ts +5 -8
- package/dist/types/query/types.d.ts +34 -0
- package/dist/types/segments.d.ts +31 -0
- package/dist/types/write.d.ts +204 -0
- package/dist/types/xquery/parse.d.ts +2 -3
- package/docs/JSLT-FORMAT.md +74 -3
- package/docs/JSLT-PRELUDE.md +1 -1
- package/docs/QUERY-FORMAT.md +695 -33
- package/package.json +18 -4
- package/schemas/geojson.draft-07.schema.json +323 -0
- package/schemas/geojson.jaren.schema.json +863 -0
- package/schemas/geojson.schema.json +172 -0
- package/schemas/jaren-jslt.authoring.schema.json +142 -0
- package/schemas/jaren-jslt.draft-07.schema.json +152 -11
- package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
- package/schemas/jaren-jslt.schema.json +152 -11
- package/schemas/jaren-query.draft-07.schema.json +152 -11
- package/schemas/jaren-query.llm-profile.schema.json +619 -0
- package/schemas/jaren-query.schema.json +82 -15
- package/src/basic.js +1 -1
- package/src/canonical.js +170 -0
- package/src/cow.js +106 -0
- package/src/errors.js +68 -0
- package/src/index.js +3 -0
- package/src/jslt/dispatch.js +178 -28
- package/src/jslt/errors.js +19 -14
- package/src/jslt/index.js +37 -29
- package/src/jslt/packs/finance.js +49 -0
- package/src/jslt/packs/index.js +18 -0
- package/src/jslt/packs/math.js +46 -0
- package/src/jslt/packs/stats.js +65 -0
- package/src/jslt/registry.js +200 -0
- package/src/jslt/stylesheet.js +14 -23
- package/src/jtlt/desugar.js +2 -3
- package/src/jtlt/errors.js +6 -12
- package/src/jtlt/index.js +12 -29
- package/src/jtlt/template.js +9 -18
- package/src/option-variants.js +54 -0
- package/src/patch.js +1052 -0
- package/src/path.js +319 -52
- package/src/pointer.js +225 -44
- package/src/query/compile.js +790 -75
- package/src/query/errors.js +72 -12
- package/src/query/index.js +274 -42
- package/src/query/normalize.js +489 -78
- package/src/query/operators.js +620 -23
- package/src/query/runtime.js +5 -19
- package/src/query/types.js +213 -0
- package/src/segments.js +409 -64
- package/src/write.js +660 -0
- package/src/xquery/parse.js +37 -53
package/src/jslt/dispatch.js
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
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
|
package/src/jslt/errors.js
CHANGED
|
@@ -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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
25
|
-
constructor(code,
|
|
26
|
-
super(
|
|
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 =
|
|
73
|
+
const STYLESHEET_CACHE = createOptionVariantCache();
|
|
57
74
|
|
|
58
75
|
function cachedTransform(stylesheet, options) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
};
|