@jxsuite/runtime 4.1.0 → 5.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.
- package/dist/runtime.js +720 -442
- package/dist/runtime.js.map +12 -12
- package/package.json +5 -4
- package/src/cookie.ts +4 -2
- package/src/css.ts +31 -18
- package/src/expression.ts +321 -232
- package/src/pointer.ts +10 -4
- package/src/runtime.ts +1318 -566
- package/src/statements.ts +58 -48
- package/src/types.ts +114 -13
package/src/expression.ts
CHANGED
|
@@ -13,25 +13,65 @@ import {
|
|
|
13
13
|
INTL_HELPER_PATHS,
|
|
14
14
|
INTL_LOCALE_PARAM,
|
|
15
15
|
} from "@jxsuite/schema/intl";
|
|
16
|
-
import
|
|
17
|
-
import type {
|
|
16
|
+
import { isFunction, isNonNullObject, isString } from "@jxsuite/schema/guards";
|
|
17
|
+
import type {
|
|
18
|
+
CemParameter,
|
|
19
|
+
JxExpressionNode,
|
|
20
|
+
JxExpressionOperand,
|
|
21
|
+
JxRef,
|
|
22
|
+
} from "@jxsuite/schema/types";
|
|
23
|
+
import type { JxScope, JxScopeValue } from "./types.ts";
|
|
18
24
|
import { readPath, refAccessor, refSegments } from "./pointer.ts";
|
|
19
25
|
|
|
20
|
-
/** The
|
|
21
|
-
export type
|
|
22
|
-
|
|
26
|
+
/** The published names of the schema's expression types, kept for existing importers. */
|
|
27
|
+
export type {
|
|
28
|
+
JxExpressionNode as ExpressionNode,
|
|
29
|
+
JxExpressionOperand as ExpressionOperand,
|
|
30
|
+
} from "@jxsuite/schema/types";
|
|
23
31
|
|
|
24
32
|
/** The `$map` iteration context object stored in scope during mapped rendering. */
|
|
25
33
|
interface ScopeMapCtx {
|
|
26
|
-
item?:
|
|
34
|
+
item?: JxScopeValue;
|
|
27
35
|
index?: number;
|
|
28
|
-
[key: string]:
|
|
36
|
+
[key: string]: JxScopeValue;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/** View the scope's `$map` iteration context, if present. */
|
|
32
40
|
function scopeMap(state: JxScope): ScopeMapCtx | undefined {
|
|
33
41
|
const map = state.$map;
|
|
34
|
-
|
|
42
|
+
/* SAFETY: the guard proved a non-null object, and the runtime binds `$map` only to the
|
|
43
|
+
`{ item, index }` record a mapped row renders under; every member is read as a scope
|
|
44
|
+
member. */
|
|
45
|
+
return isNonNullObject(map) ? (map as ScopeMapCtx) : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Spec §19.2 shape detection: an operand object carrying `operator` is a nested node, never a
|
|
50
|
+
* literal.
|
|
51
|
+
*/
|
|
52
|
+
function isNodeOperand(operand: JxExpressionOperand): operand is JxExpressionNode {
|
|
53
|
+
return isNonNullObject(operand) && !Array.isArray(operand) && "operator" in operand;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Spec §19.2 shape detection: an operand object carrying `$ref` is a pointer, never a literal. */
|
|
57
|
+
function isRefOperand(operand: JxExpressionOperand): operand is JxRef {
|
|
58
|
+
return isNonNullObject(operand) && !Array.isArray(operand) && "$ref" in operand;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A scope callable, a blessed global or a standard-library method as a formula invokes it: the
|
|
63
|
+
* resolved arguments go in untyped, as they do in the compiled `fn(...args)`, and whatever the
|
|
64
|
+
* callee returns comes back.
|
|
65
|
+
*/
|
|
66
|
+
type FormulaCallable = (...args: JxScopeValue[]) => JxScopeValue;
|
|
67
|
+
|
|
68
|
+
/** Named-formula arguments bound for one `call` body, read through `$args/<name>` refs. */
|
|
69
|
+
type FormulaArgs = Record<string, JxScopeValue>;
|
|
70
|
+
|
|
71
|
+
/** A named-formula def met in scope unlowered: its body, and its parameters as written. */
|
|
72
|
+
interface RawFormulaDef {
|
|
73
|
+
$expression: JxExpressionNode;
|
|
74
|
+
parameters?: readonly (string | CemParameter | null)[];
|
|
35
75
|
}
|
|
36
76
|
|
|
37
77
|
interface CompileOpts {
|
|
@@ -189,56 +229,84 @@ export const BLESSED_GLOBALS = new Set([
|
|
|
189
229
|
...INTL_HELPER_PATHS,
|
|
190
230
|
]);
|
|
191
231
|
|
|
232
|
+
/** What a synthetic helper returns: a formatted string, a comparison or a list of segments. */
|
|
233
|
+
type BlessedHelperResult = string | number | string[] | undefined;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* One synthetic helper. Each declares the arguments its Intl constructor expects; the table holds
|
|
237
|
+
* them as `(...args: never[])`, the parameter list every function is assignable to, so helpers of
|
|
238
|
+
* different arities share one map.
|
|
239
|
+
*/
|
|
240
|
+
type BlessedHelper = (...args: never[]) => BlessedHelperResult;
|
|
241
|
+
|
|
192
242
|
/**
|
|
193
243
|
* Synthetic blessed helpers (spec §19.4c): pure functions that have no direct `window` global
|
|
194
244
|
* because the underlying standard-library API is a constructor. The interpreter dispatches to
|
|
195
245
|
* these; the compiler emits the equivalent inline construct-then-format expression.
|
|
196
246
|
*/
|
|
197
|
-
const BLESSED_HELPERS
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
247
|
+
const BLESSED_HELPERS = new Map<string, BlessedHelper>([
|
|
248
|
+
[
|
|
249
|
+
"Intl/compare",
|
|
250
|
+
(a: string, b: string, locale?: string, options?: Intl.CollatorOptions) =>
|
|
251
|
+
new Intl.Collator(locale ?? DEFAULT_FORMAT_LOCALE, options).compare(a, b),
|
|
252
|
+
],
|
|
253
|
+
[
|
|
254
|
+
"Intl/displayName",
|
|
255
|
+
(
|
|
256
|
+
code: string,
|
|
257
|
+
type?: Intl.DisplayNamesType,
|
|
258
|
+
locale?: string,
|
|
259
|
+
options?: Intl.DisplayNamesOptions,
|
|
260
|
+
) =>
|
|
261
|
+
new Intl.DisplayNames(locale ?? DEFAULT_FORMAT_LOCALE, {
|
|
262
|
+
...options,
|
|
263
|
+
type: type ?? "language",
|
|
264
|
+
}).of(code),
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
"Intl/formatDate",
|
|
268
|
+
(value: string | number | Date, locale?: string, options?: Intl.DateTimeFormatOptions) =>
|
|
269
|
+
new Intl.DateTimeFormat(locale ?? DEFAULT_FORMAT_LOCALE, {
|
|
270
|
+
timeZone: DEFAULT_TIME_ZONE,
|
|
271
|
+
...options,
|
|
272
|
+
}).format(new Date(value)),
|
|
273
|
+
],
|
|
274
|
+
[
|
|
275
|
+
"Intl/formatList",
|
|
276
|
+
(values: string[], locale?: string, options?: Intl.ListFormatOptions) =>
|
|
277
|
+
new Intl.ListFormat(locale ?? DEFAULT_FORMAT_LOCALE, options).format(values),
|
|
278
|
+
],
|
|
279
|
+
[
|
|
280
|
+
"Intl/formatNumber",
|
|
281
|
+
(value: number, locale?: string, options?: Intl.NumberFormatOptions) =>
|
|
282
|
+
new Intl.NumberFormat(locale ?? DEFAULT_FORMAT_LOCALE, options).format(value),
|
|
283
|
+
],
|
|
284
|
+
[
|
|
285
|
+
"Intl/formatRelativeTime",
|
|
286
|
+
(
|
|
287
|
+
value: number,
|
|
288
|
+
unit: Intl.RelativeTimeFormatUnit,
|
|
289
|
+
locale?: string,
|
|
290
|
+
options?: Intl.RelativeTimeFormatOptions,
|
|
291
|
+
) => new Intl.RelativeTimeFormat(locale ?? DEFAULT_FORMAT_LOCALE, options).format(value, unit),
|
|
292
|
+
],
|
|
293
|
+
[
|
|
294
|
+
"Intl/plural",
|
|
295
|
+
(value: number, locale?: string, options?: Intl.PluralRulesOptions) =>
|
|
296
|
+
new Intl.PluralRules(locale ?? DEFAULT_FORMAT_LOCALE, options).select(value),
|
|
297
|
+
],
|
|
233
298
|
/* Returns the segments as plain strings, because a formula's value has to be JSON — the
|
|
234
299
|
Segmenter's own iterator of {segment, index, isWordLike} records is not. */
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
300
|
+
[
|
|
301
|
+
"Intl/segment",
|
|
302
|
+
(value: string, granularity?: "grapheme" | "sentence" | "word", locale?: string) =>
|
|
303
|
+
[
|
|
304
|
+
...new Intl.Segmenter(locale ?? DEFAULT_FORMAT_LOCALE, {
|
|
305
|
+
granularity: granularity ?? "grapheme",
|
|
306
|
+
}).segment(value),
|
|
307
|
+
].map((part) => part.segment),
|
|
308
|
+
],
|
|
309
|
+
]);
|
|
242
310
|
|
|
243
311
|
/**
|
|
244
312
|
* Fill in a helper's omitted `locale` argument from the page's own locale.
|
|
@@ -255,18 +323,17 @@ const BLESSED_HELPERS: Record<string, (...a: unknown[]) => unknown> = {
|
|
|
255
323
|
* used standalone — keeps the fixed default.
|
|
256
324
|
*
|
|
257
325
|
* @param {string} path - Helper callee path, e.g. `Intl/formatNumber`
|
|
258
|
-
* @param {
|
|
326
|
+
* @param {JxScopeValue[]} args - Resolved arguments, in declaration order
|
|
259
327
|
* @param {JxScope} state
|
|
260
|
-
* @returns {
|
|
328
|
+
* @returns {JxScopeValue[]}
|
|
261
329
|
*/
|
|
262
|
-
function withPageLocale(path: string, args:
|
|
330
|
+
function withPageLocale(path: string, args: JxScopeValue[], state: JxScope): JxScopeValue[] {
|
|
263
331
|
const index = INTL_LOCALE_PARAM[path];
|
|
264
332
|
if (index === undefined || index < 0 || args[index] != null) {
|
|
265
333
|
return args;
|
|
266
334
|
}
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
if (typeof locale !== "string" || locale === "") {
|
|
335
|
+
const locale = readPath(state.$page, "locale");
|
|
336
|
+
if (!isString(locale) || locale === "") {
|
|
270
337
|
return args;
|
|
271
338
|
}
|
|
272
339
|
const filled = [...args];
|
|
@@ -290,11 +357,11 @@ export function isMutating(op: string) {
|
|
|
290
357
|
// ─── Runtime Evaluation ──────────────────────────────────────────────────────
|
|
291
358
|
|
|
292
359
|
interface IterCtx {
|
|
293
|
-
acc?:
|
|
294
|
-
item?:
|
|
360
|
+
acc?: JxScopeValue;
|
|
361
|
+
item?: JxScopeValue;
|
|
295
362
|
index?: number;
|
|
296
363
|
/** Named-formula arguments bound for the current `call` body ($args/<name> refs). */
|
|
297
|
-
args?:
|
|
364
|
+
args?: FormulaArgs;
|
|
298
365
|
/** Call-nesting depth; capped at MAX_CALL_DEPTH to bound recursive formulas. */
|
|
299
366
|
callDepth?: number;
|
|
300
367
|
}
|
|
@@ -309,7 +376,7 @@ export const MAX_CALL_DEPTH = 64;
|
|
|
309
376
|
* can badge every node. Absent (production), evaluation is untouched.
|
|
310
377
|
*/
|
|
311
378
|
export interface ExpressionTrace {
|
|
312
|
-
report: (path: (string | number)[], value:
|
|
379
|
+
report: (path: (string | number)[], value: JxScopeValue) => void;
|
|
313
380
|
/** Current node path — internal; leave unset at the call boundary. */
|
|
314
381
|
path?: (string | number)[];
|
|
315
382
|
/** Recursion depth — internal; reporting stops beyond MAX_REPORT_DEPTH. */
|
|
@@ -340,33 +407,31 @@ function subTrace(
|
|
|
340
407
|
|
|
341
408
|
/** Resolve an operand to its runtime value. */
|
|
342
409
|
function resolveOperand(
|
|
343
|
-
operand:
|
|
410
|
+
operand: JxExpressionOperand | undefined,
|
|
344
411
|
state: JxScope,
|
|
345
412
|
event: Event | null,
|
|
346
413
|
iterCtx?: IterCtx,
|
|
347
414
|
trace?: ExpressionTrace,
|
|
348
|
-
):
|
|
415
|
+
): JxScopeValue {
|
|
349
416
|
if (operand === null || operand === undefined) {
|
|
350
417
|
return operand;
|
|
351
418
|
}
|
|
352
419
|
|
|
353
420
|
// Nested expression node
|
|
354
|
-
if (
|
|
355
|
-
return evaluateExpression(operand
|
|
421
|
+
if (isNodeOperand(operand)) {
|
|
422
|
+
return evaluateExpression(operand, state, event, iterCtx, trace);
|
|
356
423
|
}
|
|
357
424
|
|
|
358
425
|
// $ref pointer
|
|
359
|
-
if (
|
|
360
|
-
const resolved = resolveExprRef(
|
|
426
|
+
if (isRefOperand(operand)) {
|
|
427
|
+
const resolved = resolveExprRef(operand.$ref, state, event, iterCtx);
|
|
361
428
|
trace?.report(trace.path ?? [], resolved);
|
|
362
429
|
return resolved;
|
|
363
430
|
}
|
|
364
431
|
|
|
365
432
|
// Array of operands (e.g., splice args)
|
|
366
433
|
if (Array.isArray(operand)) {
|
|
367
|
-
return operand
|
|
368
|
-
resolveOperand(o, state, event, iterCtx, subTrace(trace, i)),
|
|
369
|
-
);
|
|
434
|
+
return resolveOperands(operand, state, event, iterCtx, trace);
|
|
370
435
|
}
|
|
371
436
|
|
|
372
437
|
// Literal
|
|
@@ -374,14 +439,29 @@ function resolveOperand(
|
|
|
374
439
|
return operand;
|
|
375
440
|
}
|
|
376
441
|
|
|
377
|
-
/** Resolve a
|
|
442
|
+
/** Resolve each operand of a list, tracing member `i` at segment `i` beneath `trace`. */
|
|
443
|
+
function resolveOperands(
|
|
444
|
+
operands: readonly JxExpressionOperand[],
|
|
445
|
+
state: JxScope,
|
|
446
|
+
event: Event | null,
|
|
447
|
+
iterCtx: IterCtx | undefined,
|
|
448
|
+
trace: ExpressionTrace | undefined,
|
|
449
|
+
): JxScopeValue[] {
|
|
450
|
+
return operands.map((o, i) => resolveOperand(o, state, event, iterCtx, subTrace(trace, i)));
|
|
451
|
+
}
|
|
452
|
+
|
|
378
453
|
/**
|
|
379
454
|
* The receiver a `call` binds: the value the pointer's parent path names. `#/state/x` and
|
|
380
455
|
* `parent#/x` are owned by the scope itself; `$map/item/fn` by the item; a pointer with no parent
|
|
381
456
|
* segment (`$map/item`, `$args/x`) has no owner.
|
|
382
457
|
*/
|
|
383
|
-
function calleeOwner(
|
|
384
|
-
|
|
458
|
+
function calleeOwner(
|
|
459
|
+
ref: string,
|
|
460
|
+
state: JxScope,
|
|
461
|
+
event: Event | null,
|
|
462
|
+
iterCtx?: IterCtx,
|
|
463
|
+
): JxScopeValue {
|
|
464
|
+
const parentRef = (prefix: string): JxScopeValue => {
|
|
385
465
|
const rest = ref.slice(prefix.length);
|
|
386
466
|
if (!rest.includes("/")) {
|
|
387
467
|
return state;
|
|
@@ -402,6 +482,7 @@ function calleeOwner(ref: string, state: JxScope, event: Event | null, iterCtx?:
|
|
|
402
482
|
return undefined;
|
|
403
483
|
}
|
|
404
484
|
|
|
485
|
+
/** Resolve a $ref string within expression context. */
|
|
405
486
|
function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCtx?: IterCtx) {
|
|
406
487
|
if (ref === "$reduce/acc") {
|
|
407
488
|
return iterCtx?.acc;
|
|
@@ -412,11 +493,11 @@ function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCt
|
|
|
412
493
|
if (ref.startsWith("$args/")) {
|
|
413
494
|
const parts = ref.slice("$args/".length).split("/");
|
|
414
495
|
const base = iterCtx?.args?.[parts[0]!];
|
|
415
|
-
return parts.length > 1 ?
|
|
496
|
+
return parts.length > 1 ? readPath(base, parts.slice(1).join("/")) : base;
|
|
416
497
|
}
|
|
417
498
|
if (ref.startsWith("event#/")) {
|
|
418
499
|
const path = ref.slice("event#/".length);
|
|
419
|
-
return
|
|
500
|
+
return readPath(event, path);
|
|
420
501
|
}
|
|
421
502
|
if (ref.startsWith("$map/")) {
|
|
422
503
|
const parts = ref.split("/");
|
|
@@ -430,7 +511,7 @@ function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCt
|
|
|
430
511
|
} else {
|
|
431
512
|
base = map?.[key!] ?? state[`$map/${key}`];
|
|
432
513
|
}
|
|
433
|
-
return parts.length > 2 ?
|
|
514
|
+
return parts.length > 2 ? readPath(base, parts.slice(2).join("/")) : base;
|
|
434
515
|
}
|
|
435
516
|
if (ref.startsWith("#/state/")) {
|
|
436
517
|
// One call, not a hand-split leading token: slicing at the first `/` skipped unescaping it, so
|
|
@@ -445,21 +526,27 @@ function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCt
|
|
|
445
526
|
return readPath(state, ref.slice("parent#/".length));
|
|
446
527
|
}
|
|
447
528
|
if (ref.startsWith("window#/")) {
|
|
448
|
-
return
|
|
529
|
+
return readPath(globalThis.window, ref.slice("window#/".length));
|
|
449
530
|
}
|
|
450
531
|
if (ref.startsWith("document#/")) {
|
|
451
|
-
return
|
|
532
|
+
return readPath(globalThis.document, ref.slice("document#/".length));
|
|
452
533
|
}
|
|
453
534
|
return readPath(state, ref) ?? null;
|
|
454
535
|
}
|
|
455
536
|
|
|
537
|
+
/** Where an assignment writes: the member `key` of the object `obj`. */
|
|
538
|
+
interface WritableLocation {
|
|
539
|
+
obj: JxScope;
|
|
540
|
+
key: string;
|
|
541
|
+
}
|
|
542
|
+
|
|
456
543
|
/** Resolve a $ref to a writable location — returns { obj, key } for assignment. */
|
|
457
544
|
function resolveWritableRef(
|
|
458
545
|
ref: string,
|
|
459
546
|
state: JxScope,
|
|
460
547
|
_event: Event | null,
|
|
461
548
|
iterCtx?: IterCtx,
|
|
462
|
-
):
|
|
549
|
+
): WritableLocation {
|
|
463
550
|
if (ref.startsWith("$map/")) {
|
|
464
551
|
const parts = ref.split("/");
|
|
465
552
|
const [, key] = parts;
|
|
@@ -475,7 +562,11 @@ function resolveWritableRef(
|
|
|
475
562
|
if (parts.length > 2) {
|
|
476
563
|
const pathParts = parts.slice(2);
|
|
477
564
|
const lastKey = pathParts.pop();
|
|
478
|
-
const obj = pathParts.length > 0 ?
|
|
565
|
+
const obj = pathParts.length > 0 ? readPath(base, pathParts.join("/")) : base;
|
|
566
|
+
/* SAFETY: `parts.length > 2`, so `pathParts` held at least one segment and `pop()` returned a
|
|
567
|
+
string. A `$map/…` write names a member of an object in the item (spec §19.2 resolves it
|
|
568
|
+
like any `$ref`); a non-object here fails at the assignment as the compiled `_item.a = v`
|
|
569
|
+
does. */
|
|
479
570
|
return { key: lastKey as string, obj: obj as JxScope };
|
|
480
571
|
}
|
|
481
572
|
return {
|
|
@@ -497,10 +588,13 @@ function resolveWritableRef(
|
|
|
497
588
|
const parts = refSegments(sub);
|
|
498
589
|
const lastKey = parts.pop();
|
|
499
590
|
let obj = state;
|
|
500
|
-
// Pointer paths address objects by construction (validated by the schema).
|
|
501
591
|
for (const p of parts) {
|
|
592
|
+
/* SAFETY: a writable pointer's leading segments name the objects that hold its target (spec
|
|
593
|
+
§19.3). The runtime does not validate that, so a missing or primitive segment fails at the
|
|
594
|
+
next member access or at the write, as the compiled `s.a.b = v` does. */
|
|
502
595
|
obj = obj[p] as JxScope;
|
|
503
596
|
}
|
|
597
|
+
// SAFETY: `sub` contains a `/`, so `refSegments` returned at least two segments and `pop()` a string.
|
|
504
598
|
return { key: lastKey as string, obj };
|
|
505
599
|
}
|
|
506
600
|
return { key: ref, obj: state };
|
|
@@ -508,24 +602,69 @@ function resolveWritableRef(
|
|
|
508
602
|
|
|
509
603
|
/** Evaluate an expression node at runtime. */
|
|
510
604
|
export function evaluateExpression(
|
|
511
|
-
node:
|
|
605
|
+
node: JxExpressionNode,
|
|
512
606
|
state: JxScope,
|
|
513
607
|
event: Event | null,
|
|
514
608
|
iterCtx?: IterCtx,
|
|
515
609
|
trace?: ExpressionTrace,
|
|
516
|
-
):
|
|
610
|
+
): JxScopeValue {
|
|
517
611
|
const result = evaluateNode(node, state, event, iterCtx, trace);
|
|
518
612
|
trace?.report(trace.path ?? [], result);
|
|
519
613
|
return result;
|
|
520
614
|
}
|
|
521
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Apply one of the binary operators that convert their own operands: the arithmetic ones (ECMA-262
|
|
618
|
+
* §13.15.3 ApplyStringOrNumericBinaryOperator) and the relational ones (§7.2.13 IsLessThan). The
|
|
619
|
+
* interpreter hands them the resolved values unconverted, exactly as the compiled `(left op right)`
|
|
620
|
+
* does, so `+` still concatenates when either side is a string; `number` is what lets TypeScript
|
|
621
|
+
* accept the operators, not a conversion.
|
|
622
|
+
*
|
|
623
|
+
* @param {string} operator - One of `+ - * / %` or `< <= > >=`
|
|
624
|
+
* @param {number} left
|
|
625
|
+
* @param {number} right
|
|
626
|
+
* @returns {number | boolean}
|
|
627
|
+
*/
|
|
628
|
+
function applyCoercingOperator(operator: string, left: number, right: number): number | boolean {
|
|
629
|
+
switch (operator) {
|
|
630
|
+
case "+": {
|
|
631
|
+
return left + right;
|
|
632
|
+
}
|
|
633
|
+
case "-": {
|
|
634
|
+
return left - right;
|
|
635
|
+
}
|
|
636
|
+
case "*": {
|
|
637
|
+
return left * right;
|
|
638
|
+
}
|
|
639
|
+
case "/": {
|
|
640
|
+
return left / right;
|
|
641
|
+
}
|
|
642
|
+
case "%": {
|
|
643
|
+
return left % right;
|
|
644
|
+
}
|
|
645
|
+
case "<": {
|
|
646
|
+
return left < right;
|
|
647
|
+
}
|
|
648
|
+
case "<=": {
|
|
649
|
+
return left <= right;
|
|
650
|
+
}
|
|
651
|
+
case ">": {
|
|
652
|
+
return left > right;
|
|
653
|
+
}
|
|
654
|
+
default: {
|
|
655
|
+
// `>=`: the binary operator set leaves nothing else that reaches here.
|
|
656
|
+
return left >= right;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
522
661
|
function evaluateNode(
|
|
523
|
-
node:
|
|
662
|
+
node: JxExpressionNode,
|
|
524
663
|
state: JxScope,
|
|
525
664
|
event: Event | null,
|
|
526
665
|
iterCtx?: IterCtx,
|
|
527
666
|
trace?: ExpressionTrace,
|
|
528
|
-
):
|
|
667
|
+
): JxScopeValue {
|
|
529
668
|
const { operator, target, value, initial } = node;
|
|
530
669
|
|
|
531
670
|
if (!BLESSED_OPERATORS.has(operator)) {
|
|
@@ -534,25 +673,20 @@ function evaluateNode(
|
|
|
534
673
|
|
|
535
674
|
// ─── Unary ───
|
|
536
675
|
if (UNARY_OPS.has(operator) && !("value" in node)) {
|
|
537
|
-
const operand
|
|
538
|
-
target,
|
|
539
|
-
state,
|
|
540
|
-
event,
|
|
541
|
-
iterCtx,
|
|
542
|
-
subTrace(trace, "target"),
|
|
543
|
-
);
|
|
676
|
+
const operand = resolveOperand(target, state, event, iterCtx, subTrace(trace, "target"));
|
|
544
677
|
if (operator === "!") {
|
|
545
678
|
return !operand;
|
|
546
679
|
}
|
|
547
680
|
if (operator === "-") {
|
|
681
|
+
// SAFETY: unary minus applies ToNumeric itself (ECMA-262 §13.5.4); `number` only lets TypeScript accept the operator.
|
|
548
682
|
return -(operand as number);
|
|
549
683
|
}
|
|
550
684
|
}
|
|
551
685
|
|
|
552
686
|
// ─── Call (spec §19.4c) ───
|
|
553
687
|
if (operator === "call") {
|
|
554
|
-
const calleeRef = (target
|
|
555
|
-
if (
|
|
688
|
+
const calleeRef = isRefOperand(target) ? target.$ref : undefined;
|
|
689
|
+
if (!isString(calleeRef)) {
|
|
556
690
|
throw new TypeError("$expression: call target must be a $ref pointer");
|
|
557
691
|
}
|
|
558
692
|
const callDepth = (iterCtx?.callDepth ?? 0) + 1;
|
|
@@ -560,9 +694,7 @@ function evaluateNode(
|
|
|
560
694
|
throw new Error(`$expression: call depth exceeded (${MAX_CALL_DEPTH})`);
|
|
561
695
|
}
|
|
562
696
|
const argValues = Array.isArray(value)
|
|
563
|
-
? value.map((o
|
|
564
|
-
resolveOperand(o, state, event, iterCtx, subTrace(trace, ["value", i])),
|
|
565
|
-
)
|
|
697
|
+
? value.map((o, i) => resolveOperand(o, state, event, iterCtx, subTrace(trace, ["value", i])))
|
|
566
698
|
: [];
|
|
567
699
|
|
|
568
700
|
// Blessed pure global via the window#/ scheme (Math.max, JSON.parse, …).
|
|
@@ -571,16 +703,23 @@ function evaluateNode(
|
|
|
571
703
|
throw new Error(`$expression: "${calleeRef}" is not a blessed pure global`);
|
|
572
704
|
}
|
|
573
705
|
const globalPath = calleeRef.slice("window#/".length);
|
|
574
|
-
const helper = BLESSED_HELPERS
|
|
706
|
+
const helper = BLESSED_HELPERS.get(globalPath);
|
|
575
707
|
if (helper) {
|
|
576
|
-
|
|
708
|
+
/* SAFETY: formula arguments carry no runtime types; each helper hands them to an Intl
|
|
709
|
+
constructor that coerces or throws itself, exactly as the inline call
|
|
710
|
+
`compileHelperCall` emits does. The parameter types document that contract. */
|
|
711
|
+
return (helper as (...args: JxScopeValue[]) => BlessedHelperResult)(
|
|
712
|
+
...withPageLocale(globalPath, argValues, state),
|
|
713
|
+
);
|
|
577
714
|
}
|
|
578
|
-
|
|
715
|
+
/* SAFETY: `isBlessedGlobal` admitted `globalPath`, and every BLESSED_GLOBALS entry without a
|
|
716
|
+
synthetic helper names a standard-library function on `window` (Math.max, JSON.parse, …). */
|
|
717
|
+
const fn = readPath(globalThis.window, globalPath) as FormulaCallable;
|
|
579
718
|
const lastSlash = globalPath.lastIndexOf("/");
|
|
580
719
|
const thisArg =
|
|
581
720
|
lastSlash === -1
|
|
582
721
|
? globalThis.window
|
|
583
|
-
:
|
|
722
|
+
: readPath(globalThis.window, globalPath.slice(0, lastSlash));
|
|
584
723
|
return fn.apply(thisArg, argValues);
|
|
585
724
|
}
|
|
586
725
|
|
|
@@ -588,24 +727,28 @@ function evaluateNode(
|
|
|
588
727
|
// A scope callable (buildScope lowers parameterized formula entries to functions). The receiver
|
|
589
728
|
// Is the object that owns the pointer's last segment — what the compiled lowering binds when it
|
|
590
729
|
// Emits `s.commands.run(...)` — so a host method handed in through the scope keeps its `this`.
|
|
591
|
-
if (
|
|
592
|
-
|
|
730
|
+
if (isFunction(callee)) {
|
|
731
|
+
// SAFETY: the guard proved `callee` callable; a scope callable takes the resolved arguments untyped, as the compiled call does.
|
|
732
|
+
return (callee as FormulaCallable).apply(
|
|
593
733
|
calleeOwner(calleeRef, state, event, iterCtx),
|
|
594
734
|
argValues,
|
|
595
735
|
);
|
|
596
736
|
}
|
|
597
737
|
// A raw named-formula def (standalone evaluation, e.g. editor preview).
|
|
598
|
-
if (callee &&
|
|
599
|
-
|
|
600
|
-
|
|
738
|
+
if (isNonNullObject(callee) && "$expression" in callee) {
|
|
739
|
+
/* SAFETY: a scope member carrying `$expression` is a named-formula def (spec §19.4c), left
|
|
740
|
+
unlowered only when the document is evaluated standalone. Its parameters are read as the
|
|
741
|
+
document wrote them, so a malformed `null` entry stays possible and is skipped below. */
|
|
742
|
+
const def = callee as RawFormulaDef;
|
|
743
|
+
const args: FormulaArgs = {};
|
|
601
744
|
for (const [i, p] of (def.parameters ?? []).entries()) {
|
|
602
|
-
const name =
|
|
745
|
+
const name = isString(p) ? p : (p?.name ?? "");
|
|
603
746
|
if (!name) {
|
|
604
747
|
continue;
|
|
605
748
|
}
|
|
606
749
|
args[name] =
|
|
607
|
-
argValues[i] === undefined &&
|
|
608
|
-
?
|
|
750
|
+
argValues[i] === undefined && isNonNullObject(p) && "default" in p
|
|
751
|
+
? p.default
|
|
609
752
|
: argValues[i];
|
|
610
753
|
}
|
|
611
754
|
// The callee body's node paths are its own tree, not the call site's — no trace inside.
|
|
@@ -640,7 +783,7 @@ function evaluateNode(
|
|
|
640
783
|
const cases = node.cases ?? {};
|
|
641
784
|
const matched = Object.hasOwn(cases, key);
|
|
642
785
|
if (trace) {
|
|
643
|
-
let result:
|
|
786
|
+
let result: JxScopeValue;
|
|
644
787
|
for (const [k, caseOperand] of Object.entries(cases)) {
|
|
645
788
|
const v = resolveOperand(caseOperand, state, event, iterCtx, subTrace(trace, ["cases", k]));
|
|
646
789
|
if (k === key) {
|
|
@@ -663,49 +806,15 @@ function evaluateNode(
|
|
|
663
806
|
|
|
664
807
|
// ─── Binary (pure) ───
|
|
665
808
|
if (BINARY_OPS.has(operator)) {
|
|
666
|
-
const left = resolveOperand(
|
|
667
|
-
|
|
668
|
-
state,
|
|
669
|
-
event,
|
|
670
|
-
iterCtx,
|
|
671
|
-
subTrace(trace, "target"),
|
|
672
|
-
) as number & string;
|
|
673
|
-
const right = resolveOperand(value, state, event, iterCtx, subTrace(trace, "value")) as number &
|
|
674
|
-
string;
|
|
809
|
+
const left = resolveOperand(target, state, event, iterCtx, subTrace(trace, "target"));
|
|
810
|
+
const right = resolveOperand(value, state, event, iterCtx, subTrace(trace, "value"));
|
|
675
811
|
switch (operator) {
|
|
676
|
-
case "+": {
|
|
677
|
-
return left + right;
|
|
678
|
-
}
|
|
679
|
-
case "-": {
|
|
680
|
-
return left - right;
|
|
681
|
-
}
|
|
682
|
-
case "*": {
|
|
683
|
-
return left * right;
|
|
684
|
-
}
|
|
685
|
-
case "/": {
|
|
686
|
-
return left / right;
|
|
687
|
-
}
|
|
688
|
-
case "%": {
|
|
689
|
-
return left % right;
|
|
690
|
-
}
|
|
691
812
|
case "===": {
|
|
692
813
|
return left === right;
|
|
693
814
|
}
|
|
694
815
|
case "!==": {
|
|
695
816
|
return left !== right;
|
|
696
817
|
}
|
|
697
|
-
case "<": {
|
|
698
|
-
return left < right;
|
|
699
|
-
}
|
|
700
|
-
case "<=": {
|
|
701
|
-
return left <= right;
|
|
702
|
-
}
|
|
703
|
-
case ">": {
|
|
704
|
-
return left > right;
|
|
705
|
-
}
|
|
706
|
-
case ">=": {
|
|
707
|
-
return left >= right;
|
|
708
|
-
}
|
|
709
818
|
case "&&": {
|
|
710
819
|
return left && right;
|
|
711
820
|
}
|
|
@@ -713,64 +822,40 @@ function evaluateNode(
|
|
|
713
822
|
return left || right;
|
|
714
823
|
}
|
|
715
824
|
case "??": {
|
|
716
|
-
return
|
|
825
|
+
return left ?? right;
|
|
717
826
|
}
|
|
718
827
|
default: {
|
|
719
|
-
|
|
828
|
+
// SAFETY: the remaining binary operators coerce their own operands (see applyCoercingOperator).
|
|
829
|
+
return applyCoercingOperator(operator, left as number, right as number);
|
|
720
830
|
}
|
|
721
831
|
}
|
|
722
832
|
}
|
|
723
833
|
|
|
724
834
|
// ─── Assignment ───
|
|
725
835
|
if (ASSIGNMENT_OPS.has(operator)) {
|
|
726
|
-
const rhs = resolveOperand(value, state, event, iterCtx, subTrace(trace, "value"))
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
obj[key] = rhs;
|
|
736
|
-
break;
|
|
737
|
-
}
|
|
738
|
-
case "+=": {
|
|
739
|
-
const current = obj[key] as number;
|
|
740
|
-
obj[key] = current + rhs;
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
case "-=": {
|
|
744
|
-
const current = obj[key] as number;
|
|
745
|
-
obj[key] = current - rhs;
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
case "*=": {
|
|
749
|
-
const current = obj[key] as number;
|
|
750
|
-
obj[key] = current * rhs;
|
|
751
|
-
break;
|
|
752
|
-
}
|
|
753
|
-
case "/=": {
|
|
754
|
-
const current = obj[key] as number;
|
|
755
|
-
obj[key] = current / rhs;
|
|
756
|
-
break;
|
|
757
|
-
}
|
|
758
|
-
default: {
|
|
759
|
-
break;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
836
|
+
const rhs = resolveOperand(value, state, event, iterCtx, subTrace(trace, "value"));
|
|
837
|
+
// SAFETY: an assignment's target is a writable `$ref` (spec §19.3); the schema admits nothing else there.
|
|
838
|
+
const { obj, key } = resolveWritableRef((target as JxRef).$ref, state, event, iterCtx);
|
|
839
|
+
/* SAFETY: a compound `a op= b` is `a = a op b`, and `op` converts its own operands (see
|
|
840
|
+
applyCoercingOperator); plain `=` stores `rhs` untouched and never reads `obj[key]`. */
|
|
841
|
+
obj[key] =
|
|
842
|
+
operator === "="
|
|
843
|
+
? rhs
|
|
844
|
+
: applyCoercingOperator(operator.slice(0, -1), obj[key] as number, rhs as number);
|
|
762
845
|
return;
|
|
763
846
|
}
|
|
764
847
|
|
|
765
848
|
// ─── Array methods ───
|
|
766
849
|
if (ARRAY_METHOD_OPS.has(operator)) {
|
|
767
|
-
|
|
850
|
+
/* SAFETY: an array-mutation operator's receiver is an array (spec §19.3); a non-array fails at
|
|
851
|
+
the method call exactly as the compiled `receiver.push(v)` does. */
|
|
852
|
+
const arr = resolveOperand(
|
|
768
853
|
target,
|
|
769
854
|
state,
|
|
770
855
|
event,
|
|
771
856
|
iterCtx,
|
|
772
857
|
subTrace(trace, "target"),
|
|
773
|
-
) as
|
|
858
|
+
) as JxScopeValue[];
|
|
774
859
|
switch (operator) {
|
|
775
860
|
case "push": {
|
|
776
861
|
return arr.push(resolveOperand(value, state, event, iterCtx, subTrace(trace, "value")));
|
|
@@ -785,14 +870,15 @@ function evaluateNode(
|
|
|
785
870
|
return arr.shift();
|
|
786
871
|
}
|
|
787
872
|
case "splice": {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
873
|
+
/* SAFETY: splice's `value` is its argument list `[start, deleteCount, ...items]` (spec
|
|
874
|
+
§19.3); splice converts `start` and `deleteCount` itself (ToIntegerOrInfinity), as the
|
|
875
|
+
compiled `receiver.splice(...)` does. */
|
|
876
|
+
const args = resolveOperand(value, state, event, iterCtx, subTrace(trace, "value")) as [
|
|
877
|
+
start: number,
|
|
878
|
+
deleteCount: number,
|
|
879
|
+
...items: JxScopeValue[],
|
|
880
|
+
];
|
|
881
|
+
return arr.splice(...args);
|
|
796
882
|
}
|
|
797
883
|
default: {
|
|
798
884
|
break;
|
|
@@ -807,32 +893,38 @@ function evaluateNode(
|
|
|
807
893
|
value === undefined
|
|
808
894
|
? []
|
|
809
895
|
: Array.isArray(value)
|
|
810
|
-
? (
|
|
896
|
+
? resolveOperands(value, state, event, iterCtx, subTrace(trace, "value"))
|
|
811
897
|
: [resolveOperand(value, state, event, iterCtx, subTrace(trace, "value"))];
|
|
812
|
-
const method = (receiver
|
|
813
|
-
if (
|
|
898
|
+
const method = readPath(receiver, operator);
|
|
899
|
+
if (!isFunction(method)) {
|
|
814
900
|
// Null-safe, like path reads: a missing receiver or method yields undefined, not a throw.
|
|
815
901
|
return undefined;
|
|
816
902
|
}
|
|
817
|
-
|
|
903
|
+
// SAFETY: the guard proved `method` callable; a standard-library method coerces its own arguments.
|
|
904
|
+
return (method as FormulaCallable).apply(receiver, args);
|
|
818
905
|
}
|
|
819
906
|
|
|
820
907
|
// ─── Aggregates (pure) ───
|
|
821
908
|
if (AGGREGATE_OPS.has(operator)) {
|
|
822
|
-
|
|
909
|
+
/* SAFETY: an aggregate's source is an array (spec §19.4a); a non-array fails at the iteration
|
|
910
|
+
exactly as the compiled `source.map(...)` does. */
|
|
911
|
+
const arr = resolveOperand(
|
|
823
912
|
target,
|
|
824
913
|
state,
|
|
825
914
|
event,
|
|
826
915
|
iterCtx,
|
|
827
916
|
subTrace(trace, "target"),
|
|
828
|
-
) as
|
|
917
|
+
) as JxScopeValue[];
|
|
918
|
+
/* SAFETY: an aggregate's `value` is its per-item expression node (spec §19.4a), the only shape
|
|
919
|
+
the schema admits there; anything else fails evaluateExpression's operator check. */
|
|
920
|
+
const perItem = value as JxExpressionNode;
|
|
829
921
|
// Trace only the first iteration's per-item expression — one sample badge per node,
|
|
830
922
|
// Not one report per array element.
|
|
831
923
|
if (operator === "reduce") {
|
|
832
|
-
let acc
|
|
924
|
+
let acc = resolveOperand(initial, state, event, iterCtx, subTrace(trace, "initial"));
|
|
833
925
|
for (const [index, item] of arr.entries()) {
|
|
834
926
|
acc = evaluateExpression(
|
|
835
|
-
|
|
927
|
+
perItem,
|
|
836
928
|
state,
|
|
837
929
|
event,
|
|
838
930
|
{ acc, index, item },
|
|
@@ -842,9 +934,9 @@ function evaluateNode(
|
|
|
842
934
|
return acc;
|
|
843
935
|
}
|
|
844
936
|
if (operator === "map") {
|
|
845
|
-
return arr.map((item
|
|
937
|
+
return arr.map((item, index) =>
|
|
846
938
|
evaluateExpression(
|
|
847
|
-
|
|
939
|
+
perItem,
|
|
848
940
|
state,
|
|
849
941
|
event,
|
|
850
942
|
{ ...iterCtx, index, item },
|
|
@@ -853,9 +945,9 @@ function evaluateNode(
|
|
|
853
945
|
);
|
|
854
946
|
}
|
|
855
947
|
if (operator === "filter") {
|
|
856
|
-
return arr.filter((item
|
|
948
|
+
return arr.filter((item, index) =>
|
|
857
949
|
evaluateExpression(
|
|
858
|
-
|
|
950
|
+
perItem,
|
|
859
951
|
state,
|
|
860
952
|
event,
|
|
861
953
|
{ ...iterCtx, index, item },
|
|
@@ -871,7 +963,7 @@ function evaluateNode(
|
|
|
871
963
|
// ─── Compiler: Expression → JS Source ────────────────────────────────────────
|
|
872
964
|
|
|
873
965
|
/** Compile an operand to a JS source string. */
|
|
874
|
-
function compileOperand(operand:
|
|
966
|
+
function compileOperand(operand: JxExpressionOperand | undefined, opts: CompileOpts): string {
|
|
875
967
|
if (operand === null) {
|
|
876
968
|
return "null";
|
|
877
969
|
}
|
|
@@ -880,18 +972,18 @@ function compileOperand(operand: unknown, opts: CompileOpts): string {
|
|
|
880
972
|
}
|
|
881
973
|
|
|
882
974
|
// Nested expression node
|
|
883
|
-
if (
|
|
884
|
-
return compileExpression(operand
|
|
975
|
+
if (isNodeOperand(operand)) {
|
|
976
|
+
return compileExpression(operand, opts);
|
|
885
977
|
}
|
|
886
978
|
|
|
887
979
|
// $ref pointer
|
|
888
|
-
if (
|
|
889
|
-
return compileRef(
|
|
980
|
+
if (isRefOperand(operand)) {
|
|
981
|
+
return compileRef(operand.$ref, opts);
|
|
890
982
|
}
|
|
891
983
|
|
|
892
984
|
// Array of operands
|
|
893
985
|
if (Array.isArray(operand)) {
|
|
894
|
-
return operand.map((o
|
|
986
|
+
return operand.map((o) => compileOperand(o, opts)).join(", ");
|
|
895
987
|
}
|
|
896
988
|
|
|
897
989
|
// Literal
|
|
@@ -968,9 +1060,9 @@ function compileRef(ref: string, opts: CompileOpts) {
|
|
|
968
1060
|
}
|
|
969
1061
|
|
|
970
1062
|
/** Compile a writable $ref target to its JS equivalent (for LHS of assignment). */
|
|
971
|
-
function compileTarget(target:
|
|
972
|
-
if (
|
|
973
|
-
return compileRef(
|
|
1063
|
+
function compileTarget(target: JxExpressionOperand, opts: CompileOpts): string {
|
|
1064
|
+
if (isRefOperand(target)) {
|
|
1065
|
+
return compileRef(target.$ref, opts);
|
|
974
1066
|
}
|
|
975
1067
|
return compileOperand(target, opts);
|
|
976
1068
|
}
|
|
@@ -979,7 +1071,7 @@ function compileTarget(target: unknown, opts: CompileOpts): string {
|
|
|
979
1071
|
* Compile an expression node to a JS source string. Returns a statement for mutating ops, a value
|
|
980
1072
|
* expression for pure ops.
|
|
981
1073
|
*
|
|
982
|
-
* @param {
|
|
1074
|
+
* @param {JxExpressionNode} node
|
|
983
1075
|
* @param {CompileOpts} [opts]
|
|
984
1076
|
* @returns {string}
|
|
985
1077
|
*/
|
|
@@ -1042,7 +1134,7 @@ function compileHelperCall(path: string, args: string[], opts: CompileOpts): str
|
|
|
1042
1134
|
}
|
|
1043
1135
|
}
|
|
1044
1136
|
|
|
1045
|
-
export function compileExpression(node:
|
|
1137
|
+
export function compileExpression(node: JxExpressionNode, opts: CompileOpts = {}): string {
|
|
1046
1138
|
const { operator, target, value, initial } = node;
|
|
1047
1139
|
|
|
1048
1140
|
// ─── Unary ───
|
|
@@ -1058,8 +1150,8 @@ export function compileExpression(node: ExpressionNode, opts: CompileOpts = {}):
|
|
|
1058
1150
|
|
|
1059
1151
|
// ─── Call (spec §19.4c) ───
|
|
1060
1152
|
if (operator === "call") {
|
|
1061
|
-
const calleeRef = (target
|
|
1062
|
-
const args = Array.isArray(value) ? value.map((o
|
|
1153
|
+
const calleeRef = (isRefOperand(target) ? target.$ref : undefined) ?? "";
|
|
1154
|
+
const args = Array.isArray(value) ? value.map((o) => compileOperand(o, opts)) : [];
|
|
1063
1155
|
if (calleeRef.startsWith("window#/")) {
|
|
1064
1156
|
if (!isBlessedGlobal(calleeRef)) {
|
|
1065
1157
|
throw new Error(`$expression: "${calleeRef}" is not a blessed pure global`);
|
|
@@ -1133,7 +1225,7 @@ export function compileExpression(node: ExpressionNode, opts: CompileOpts = {}):
|
|
|
1133
1225
|
}
|
|
1134
1226
|
case "splice": {
|
|
1135
1227
|
const args: string = Array.isArray(value)
|
|
1136
|
-
? value.map((o
|
|
1228
|
+
? value.map((o) => compileOperand(o, opts)).join(", ")
|
|
1137
1229
|
: compileOperand(value, opts);
|
|
1138
1230
|
return `${arr}.splice(${args})`;
|
|
1139
1231
|
}
|
|
@@ -1150,7 +1242,7 @@ export function compileExpression(node: ExpressionNode, opts: CompileOpts = {}):
|
|
|
1150
1242
|
value === undefined
|
|
1151
1243
|
? ""
|
|
1152
1244
|
: Array.isArray(value)
|
|
1153
|
-
? value.map((o
|
|
1245
|
+
? value.map((o) => compileOperand(o, opts)).join(", ")
|
|
1154
1246
|
: compileOperand(value, opts);
|
|
1155
1247
|
// ?.method?.() matches the interpreter: missing receiver or method yields undefined.
|
|
1156
1248
|
return `(${receiver})?.${operator}?.(${args})`;
|
|
@@ -1159,7 +1251,8 @@ export function compileExpression(node: ExpressionNode, opts: CompileOpts = {}):
|
|
|
1159
1251
|
// ─── Aggregates (pure) ───
|
|
1160
1252
|
if (AGGREGATE_OPS.has(operator)) {
|
|
1161
1253
|
const arr: string = compileOperand(target, opts);
|
|
1162
|
-
|
|
1254
|
+
// SAFETY: an aggregate's `value` is its per-item expression node (spec §19.4a), the only shape the schema admits there.
|
|
1255
|
+
const itemExpr: string = compileExpression(value as JxExpressionNode, opts);
|
|
1163
1256
|
if (operator === "reduce") {
|
|
1164
1257
|
const seed: string = compileOperand(initial, opts);
|
|
1165
1258
|
return `${arr}.reduce((_acc, _item, _index) => ${itemExpr}, ${seed})`;
|
|
@@ -1175,13 +1268,6 @@ export function compileExpression(node: ExpressionNode, opts: CompileOpts = {}):
|
|
|
1175
1268
|
return "undefined";
|
|
1176
1269
|
}
|
|
1177
1270
|
|
|
1178
|
-
// ─── Utilities ───────────────────────────────────────────────────────────────
|
|
1179
|
-
|
|
1180
|
-
/** Resolve a `$ref` path on an object, through the one tokenizer (`pointer.ts`). */
|
|
1181
|
-
function getPath(obj: unknown, path: string): unknown {
|
|
1182
|
-
return readPath(obj, path);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
1271
|
// ─── Statement-Engine Surface (spec §20) ─────────────────────────────────────
|
|
1186
1272
|
|
|
1187
1273
|
/**
|
|
@@ -1189,16 +1275,19 @@ function getPath(obj: unknown, path: string): unknown {
|
|
|
1189
1275
|
* statement execution (spec §20) for `if` tests and `$switch` discriminants.
|
|
1190
1276
|
*/
|
|
1191
1277
|
export function evaluateOperand(
|
|
1192
|
-
operand:
|
|
1278
|
+
operand: JxExpressionOperand | undefined,
|
|
1193
1279
|
state: JxScope,
|
|
1194
1280
|
event: Event | null,
|
|
1195
1281
|
iterCtx?: IterCtx,
|
|
1196
|
-
):
|
|
1282
|
+
): JxScopeValue {
|
|
1197
1283
|
return resolveOperand(operand, state, event, iterCtx);
|
|
1198
1284
|
}
|
|
1199
1285
|
|
|
1200
1286
|
/** Compile a bare operand to JS source — the statement compiler's counterpart to evaluateOperand. */
|
|
1201
|
-
export function compileOperandSource(
|
|
1287
|
+
export function compileOperandSource(
|
|
1288
|
+
operand: JxExpressionOperand | undefined,
|
|
1289
|
+
opts: CompileOpts = {},
|
|
1290
|
+
): string {
|
|
1202
1291
|
return compileOperand(operand, opts);
|
|
1203
1292
|
}
|
|
1204
1293
|
|