@hyperscale0/hsx 5.2.0 → 5.4.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/CHANGELOG.md +40 -0
- package/README.md +2 -2
- package/bin/hsx.ts +0 -2
- package/dist/bin/hsx.js +0 -2
- package/dist/bin/hsx.js.map +1 -1
- package/dist/src/ast.d.ts +0 -1
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast.js +0 -11
- package/dist/src/ast.js.map +1 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +10 -5
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +204 -107
- package/dist/src/compile.js.map +1 -1
- package/dist/src/diagnostics.d.ts +14 -0
- package/dist/src/diagnostics.d.ts.map +1 -0
- package/dist/src/diagnostics.js +20 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/header-source.d.ts +3 -0
- package/dist/src/header-source.d.ts.map +1 -0
- package/dist/src/header-source.js +41 -0
- package/dist/src/header-source.js.map +1 -0
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +4 -6
- package/dist/src/headers.js.map +1 -1
- package/dist/src/keywords.d.ts +8 -2
- package/dist/src/keywords.d.ts.map +1 -1
- package/dist/src/keywords.js +13 -5
- package/dist/src/keywords.js.map +1 -1
- package/dist/src/lex.d.ts +1 -1
- package/dist/src/lex.d.ts.map +1 -1
- package/dist/src/lex.js +11 -3
- package/dist/src/lex.js.map +1 -1
- package/dist/src/parse.d.ts.map +1 -1
- package/dist/src/parse.js +22 -5
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.js +12 -12
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/tunables.d.ts.map +1 -1
- package/dist/src/tunables.js +2 -1
- package/dist/src/tunables.js.map +1 -1
- package/dist/src/version.d.ts +1 -2
- package/dist/src/version.d.ts.map +1 -1
- package/dist/src/version.js +1 -2
- package/dist/src/version.js.map +1 -1
- package/docs/README.md +54 -7
- package/docs/headers.md +1 -1
- package/package.json +3 -3
- package/src/ast.ts +0 -10
- package/src/cli.ts +13 -7
- package/src/compile.ts +296 -133
- package/src/diagnostics.ts +28 -0
- package/src/header-source.ts +54 -0
- package/src/headers.ts +4 -6
- package/src/keywords.ts +13 -5
- package/src/lex.ts +13 -5
- package/src/parse.ts +29 -5
- package/src/std-bundle.ts +12 -12
- package/src/tunables.ts +6 -1
- package/src/version.ts +1 -2
- package/std/cards.hsx +12 -0
- package/std/collections.hsx +11 -0
- package/std/escrow.hsx +16 -2
- package/std/financing.hsx +36 -5
- package/std/insurance.hsx +20 -5
- package/std/lending.hsx +14 -0
- package/std/marketplace.hsx +14 -0
- package/std/money.hsx +13 -0
- package/std/reporting.hsx +10 -0
- package/std/savings.hsx +28 -11
- package/std/travel.hsx +22 -4
- package/std/wallet.hsx +9 -0
package/src/compile.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CompileFailure, fail, failWithCode } from "./diagnostics.ts";
|
|
1
2
|
import { hash as sha256 } from "fast-sha256";
|
|
2
3
|
import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
|
|
3
4
|
import {
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
type AttachmentPartyBinding,
|
|
11
12
|
type SubjectPartyRole,
|
|
12
13
|
udlObjectFieldSchema,
|
|
14
|
+
udlInstrumentSchema,
|
|
13
15
|
type UdlAction,
|
|
14
16
|
type UdlActionSubject,
|
|
15
17
|
type UdlAdapterSubjectSnapshot,
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
} from "./binding-contract.ts";
|
|
30
32
|
import { tunableBounds } from "./tunables.ts";
|
|
31
33
|
import { parseProgram } from "./parse.ts";
|
|
34
|
+
import { parseHeader } from "./header-source.ts";
|
|
32
35
|
import {
|
|
33
36
|
lineColAt,
|
|
34
37
|
type AssignmentDecl,
|
|
@@ -71,43 +74,19 @@ export interface CompileResult {
|
|
|
71
74
|
originMap: CompileOriginMapEntry[];
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
message: string,
|
|
82
|
-
fix: string,
|
|
83
|
-
): never {
|
|
84
|
-
return failWithCode(expr, "HSX1001", message, fix);
|
|
85
|
-
}
|
|
86
|
-
function failWithCode(
|
|
87
|
-
expr: { span: Span; source?: string },
|
|
88
|
-
code: string,
|
|
89
|
-
message: string,
|
|
90
|
-
fix: string,
|
|
91
|
-
): never {
|
|
92
|
-
throw new CompileFailure({
|
|
93
|
-
code,
|
|
94
|
-
message,
|
|
95
|
-
fix,
|
|
96
|
-
span: expr.span,
|
|
97
|
-
...(expr.source ? { source: expr.source } : {}),
|
|
98
|
-
});
|
|
77
|
+
function fieldType(row: Entry): { type: Expr; sensitive: boolean } {
|
|
78
|
+
const type = row.value.kind === "default" ? row.value.type : row.value;
|
|
79
|
+
if (type.kind !== "call" || type.name !== "sensitive")
|
|
80
|
+
return { type, sensitive: false };
|
|
81
|
+
if (type.args.length !== 1)
|
|
82
|
+
fail(type, "sensitive needs one field type", "write sensitive(text)");
|
|
83
|
+
return { type: type.args[0]!, sensitive: true };
|
|
99
84
|
}
|
|
100
85
|
function lowerFieldShape(
|
|
101
86
|
row: Entry,
|
|
102
87
|
resolveExpr: (e: Expr) => Expr = (e) => e,
|
|
103
88
|
): Record<string, unknown> {
|
|
104
|
-
|
|
105
|
-
let isSensitive = false;
|
|
106
|
-
if (rawValue.kind === "call" && rawValue.name === "sensitive") {
|
|
107
|
-
isSensitive = true;
|
|
108
|
-
rawValue = rawValue.args[0]!;
|
|
109
|
-
}
|
|
110
|
-
const t = rawValue;
|
|
89
|
+
const { type: t, sensitive: isSensitive } = fieldType(row);
|
|
111
90
|
if (t.kind === "block") {
|
|
112
91
|
const b = entries(t);
|
|
113
92
|
if (b.has("family") || b.has("target") || b.has("instrument")) {
|
|
@@ -164,7 +143,22 @@ function lowerFieldShape(
|
|
|
164
143
|
f.minimum = literal(resolveExpr(t.args[0]!));
|
|
165
144
|
f.maximum = literal(resolveExpr(t.args[1]!));
|
|
166
145
|
}
|
|
146
|
+
if (
|
|
147
|
+
t.kind === "call" &&
|
|
148
|
+
!["enum", "text", "integer", "money", "list", "account"].includes(type)
|
|
149
|
+
)
|
|
150
|
+
fail(
|
|
151
|
+
t,
|
|
152
|
+
`unknown field constructor ${type}`,
|
|
153
|
+
"use a field type without arguments",
|
|
154
|
+
);
|
|
167
155
|
if (type === "list" && t.kind === "call") {
|
|
156
|
+
if (t.args.length < 1 || t.args.length > 2)
|
|
157
|
+
fail(
|
|
158
|
+
t,
|
|
159
|
+
"list needs an item type and optional bound",
|
|
160
|
+
"write list(text, 12)",
|
|
161
|
+
);
|
|
168
162
|
const item = t.args[0]!;
|
|
169
163
|
f.item = item.kind === "type" ? item.name : text(item);
|
|
170
164
|
if (item.kind === "type" && item.name === "ref") {
|
|
@@ -199,7 +193,7 @@ function lowerObjectField(
|
|
|
199
193
|
f.value =
|
|
200
194
|
f.type === "enum" && constant.kind === "name"
|
|
201
195
|
? constant.value
|
|
202
|
-
: literal(constant);
|
|
196
|
+
: literal(constant, String(f.type));
|
|
203
197
|
}
|
|
204
198
|
const result = udlObjectFieldSchema.safeParse(f);
|
|
205
199
|
if (!result.success)
|
|
@@ -233,7 +227,8 @@ const camel = (name: string) =>
|
|
|
233
227
|
name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
234
228
|
/** Clock and parent actors run without a caller, so they have no public name. */
|
|
235
229
|
const automatic = (actor: UdlAction["actor"]) =>
|
|
236
|
-
actor === "clock" ||
|
|
230
|
+
actor === "clock" ||
|
|
231
|
+
(actor !== null && typeof actor === "object" && "parent" in actor);
|
|
237
232
|
const title = (name: string) =>
|
|
238
233
|
name[0]!.toUpperCase() + name.slice(1).replaceAll("_", " ");
|
|
239
234
|
function entries(block: BlockExpr): Map<string, Expr> {
|
|
@@ -290,7 +285,9 @@ function decimal(raw: string, scale: number, expr: Expr): string {
|
|
|
290
285
|
BigInt(fraction.padEnd(scale, "0") || "0")
|
|
291
286
|
).toString();
|
|
292
287
|
}
|
|
293
|
-
function literal(expr: Expr): string | number | boolean {
|
|
288
|
+
function literal(expr: Expr, expectedType?: string): string | number | boolean {
|
|
289
|
+
if (expr.kind === "text" && expectedType === "duration")
|
|
290
|
+
return literal({ ...expr, kind: "duration" });
|
|
294
291
|
if (expr.kind === "money") {
|
|
295
292
|
const [raw, currency] = expr.value.split(" ");
|
|
296
293
|
if (currency !== "SAR")
|
|
@@ -330,8 +327,7 @@ function literal(expr: Expr): string | number | boolean {
|
|
|
330
327
|
}
|
|
331
328
|
if (
|
|
332
329
|
expr.kind === "duration" ||
|
|
333
|
-
(expr.kind === "name" && /^P(?:\d|T)/.test(expr.value))
|
|
334
|
-
(expr.kind === "text" && /^P(?:\d|T)/.test(expr.value))
|
|
330
|
+
(expr.kind === "name" && /^P(?:\d|T)/.test(expr.value))
|
|
335
331
|
) {
|
|
336
332
|
const short = /^(\d+)(ms|s|m|h|d|w)$/.exec(expr.value);
|
|
337
333
|
const units: Record<string, number> = {
|
|
@@ -343,7 +339,7 @@ function literal(expr: Expr): string | number | boolean {
|
|
|
343
339
|
w: 604800000,
|
|
344
340
|
};
|
|
345
341
|
const iso =
|
|
346
|
-
/^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(
|
|
342
|
+
/^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(
|
|
347
343
|
expr.value,
|
|
348
344
|
);
|
|
349
345
|
const n = short
|
|
@@ -367,10 +363,11 @@ function literal(expr: Expr): string | number | boolean {
|
|
|
367
363
|
const raw =
|
|
368
364
|
expr.value.length === 10 ? expr.value + "T00:00:00Z" : expr.value;
|
|
369
365
|
const n = Date.parse(raw);
|
|
366
|
+
const day = Date.parse(raw.slice(0, 10) + "T00:00:00Z");
|
|
370
367
|
if (
|
|
371
368
|
!Number.isFinite(n) ||
|
|
372
|
-
|
|
373
|
-
|
|
369
|
+
!Number.isFinite(day) ||
|
|
370
|
+
new Date(day).toISOString().slice(0, 10) !== raw.slice(0, 10)
|
|
374
371
|
)
|
|
375
372
|
fail(
|
|
376
373
|
expr,
|
|
@@ -407,6 +404,15 @@ export function compile(
|
|
|
407
404
|
};
|
|
408
405
|
const program = parsed.program;
|
|
409
406
|
const bindingDiagnostics: Diagnostic[] = [];
|
|
407
|
+
const adapterTarget = (binding: string): AdapterBindingTarget | undefined => {
|
|
408
|
+
const registry = options.adapterRegistry;
|
|
409
|
+
if (!registry || !Object.hasOwn(registry, binding)) return;
|
|
410
|
+
const target = registry[binding];
|
|
411
|
+
return target &&
|
|
412
|
+
Object.hasOwn(target.adapter.operationMap, target.operation)
|
|
413
|
+
? target
|
|
414
|
+
: undefined;
|
|
415
|
+
};
|
|
410
416
|
try {
|
|
411
417
|
if (program.header)
|
|
412
418
|
fail(
|
|
@@ -422,7 +428,6 @@ export function compile(
|
|
|
422
428
|
);
|
|
423
429
|
const templates = new Map<string, InstrumentDecl>();
|
|
424
430
|
const declarationSources = new Map<InstrumentDecl, string>();
|
|
425
|
-
const declarationExportPaths = new Map<InstrumentDecl, string>();
|
|
426
431
|
const requirementOrigins = new Map<
|
|
427
432
|
UdlSubjectRequirement,
|
|
428
433
|
{ source: string; span: Span; message: string }
|
|
@@ -442,25 +447,34 @@ export function compile(
|
|
|
442
447
|
if (!content)
|
|
443
448
|
fail(use, `unknown header ${use.name}`, "choose a published header");
|
|
444
449
|
sources.set(use.name, content);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
header.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
450
|
+
let header;
|
|
451
|
+
try {
|
|
452
|
+
header = parseHeader(content, use.name);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (!(error instanceof CompileFailure)) throw error;
|
|
455
|
+
throw new CompileFailure({
|
|
456
|
+
...error.diagnostic,
|
|
457
|
+
related: [
|
|
458
|
+
{
|
|
459
|
+
source: "program",
|
|
460
|
+
span: use.span,
|
|
461
|
+
message: `Imported header ${use.name}`,
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
});
|
|
465
|
+
}
|
|
456
466
|
const registerTemplates = (
|
|
457
467
|
parentDecl: InstrumentDecl,
|
|
458
468
|
prefix: string,
|
|
459
|
-
exportPath: string,
|
|
460
469
|
) => {
|
|
470
|
+
if (templates.has(prefix))
|
|
471
|
+
fail(
|
|
472
|
+
parentDecl,
|
|
473
|
+
`duplicate instrument ${prefix}`,
|
|
474
|
+
"give each instrument a distinct name",
|
|
475
|
+
);
|
|
461
476
|
templates.set(prefix, parentDecl);
|
|
462
477
|
declarationSources.set(parentDecl, use.name);
|
|
463
|
-
declarationExportPaths.set(parentDecl, exportPath);
|
|
464
478
|
const recs = entries(asBlock(entries(parentDecl.body).get("records")));
|
|
465
479
|
for (const [recName, recBlock] of recs) {
|
|
466
480
|
const recDecl: InstrumentDecl = {
|
|
@@ -470,16 +484,12 @@ export function compile(
|
|
|
470
484
|
body: asBlock(recBlock),
|
|
471
485
|
span: parentDecl.span,
|
|
472
486
|
};
|
|
473
|
-
registerTemplates(
|
|
474
|
-
recDecl,
|
|
475
|
-
`${prefix}.${recName}`,
|
|
476
|
-
`${exportPath}.${recName}`,
|
|
477
|
-
);
|
|
487
|
+
registerTemplates(recDecl, `${prefix}.${recName}`);
|
|
478
488
|
}
|
|
479
489
|
};
|
|
480
|
-
for (const decl of header.
|
|
490
|
+
for (const decl of header.decls)
|
|
481
491
|
if (decl.kind === "instrument") {
|
|
482
|
-
registerTemplates(decl, `${use.name}.${decl.name}
|
|
492
|
+
registerTemplates(decl, `${use.name}.${decl.name}`);
|
|
483
493
|
}
|
|
484
494
|
}
|
|
485
495
|
for (const decl of program.decls)
|
|
@@ -498,12 +508,12 @@ export function compile(
|
|
|
498
508
|
product: program.name,
|
|
499
509
|
title: program.title,
|
|
500
510
|
currency: "SAR",
|
|
501
|
-
parties: {
|
|
511
|
+
parties: Object.assign(Object.create(null) as UdlDocument["parties"], {
|
|
502
512
|
programOperator: { kind: "business", role: "program_operator" },
|
|
503
513
|
programTax: { kind: "business", role: "tax_payable" },
|
|
504
514
|
programFines: { kind: "business", role: "fine_payable" },
|
|
505
515
|
programCosts: { kind: "business", role: "cost_recovery" },
|
|
506
|
-
},
|
|
516
|
+
}),
|
|
507
517
|
objects: [],
|
|
508
518
|
instruments: [],
|
|
509
519
|
};
|
|
@@ -559,6 +569,8 @@ export function compile(
|
|
|
559
569
|
}
|
|
560
570
|
}
|
|
561
571
|
const origins: CompileOriginMapEntry[] = [];
|
|
572
|
+
const exposures: { instrument: string; action: string; entry: Entry }[] =
|
|
573
|
+
[];
|
|
562
574
|
const refundBindings = new Map<
|
|
563
575
|
string,
|
|
564
576
|
{ parameter: string; span: Span; defaultState: string; action: string }[]
|
|
@@ -760,13 +772,11 @@ export function compile(
|
|
|
760
772
|
);
|
|
761
773
|
if (sub) {
|
|
762
774
|
const fullExport = `${familyDeclaration.exportPath}.${sub}`;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
);
|
|
769
|
-
} catch {}
|
|
775
|
+
return resolveFamily(
|
|
776
|
+
`${familyDeclaration.module}.${fullExport}`,
|
|
777
|
+
{ span: origin },
|
|
778
|
+
false,
|
|
779
|
+
);
|
|
770
780
|
}
|
|
771
781
|
}
|
|
772
782
|
|
|
@@ -777,9 +787,7 @@ export function compile(
|
|
|
777
787
|
const mod = declarationSources.get(tmpl);
|
|
778
788
|
if (!mod || mod === "program") continue;
|
|
779
789
|
if (targetId === asgnName) {
|
|
780
|
-
|
|
781
|
-
return resolveFamily(asgn.target, asgn, false);
|
|
782
|
-
} catch {}
|
|
790
|
+
return resolveFamily(asgn.target, asgn, false);
|
|
783
791
|
} else {
|
|
784
792
|
const sub = resolveChildExportPath(
|
|
785
793
|
tmpl,
|
|
@@ -787,9 +795,7 @@ export function compile(
|
|
|
787
795
|
);
|
|
788
796
|
if (sub) {
|
|
789
797
|
const fullTarget = `${asgn.target}.${sub}`;
|
|
790
|
-
|
|
791
|
-
return resolveFamily(fullTarget, asgn, false);
|
|
792
|
-
} catch {}
|
|
798
|
+
return resolveFamily(fullTarget, asgn, false);
|
|
793
799
|
}
|
|
794
800
|
}
|
|
795
801
|
}
|
|
@@ -798,12 +804,18 @@ export function compile(
|
|
|
798
804
|
};
|
|
799
805
|
|
|
800
806
|
const checkTargetFamily = (
|
|
801
|
-
targetIds:
|
|
807
|
+
targetIds: unknown,
|
|
802
808
|
expectedFamily: UdlFamily,
|
|
803
809
|
expr: { span: Span; source?: string },
|
|
804
810
|
): void => {
|
|
805
811
|
const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
|
|
806
812
|
for (const tid of ids) {
|
|
813
|
+
if (typeof tid !== "string")
|
|
814
|
+
fail(
|
|
815
|
+
expr,
|
|
816
|
+
"instrument target needs a name",
|
|
817
|
+
"name a declared instrument or a list of instruments",
|
|
818
|
+
);
|
|
807
819
|
const fam = getInstrumentFamily(tid);
|
|
808
820
|
if (
|
|
809
821
|
!fam ||
|
|
@@ -964,11 +976,11 @@ export function compile(
|
|
|
964
976
|
entry.value.name === "money",
|
|
965
977
|
);
|
|
966
978
|
const target =
|
|
967
|
-
candidates.length === 1 ? candidates[0]!.key : "
|
|
979
|
+
candidates.length === 1 ? candidates[0]!.key : "yourMoneyField";
|
|
968
980
|
fail(
|
|
969
981
|
suppliedValue,
|
|
970
|
-
`\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its
|
|
971
|
-
`Remove \`${key}\`. To use your
|
|
982
|
+
`\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its actions require the object's \`${field}\` field.`,
|
|
983
|
+
`Remove \`${key}\`. To use your object's money field, add \`rename { ${field}: ${target} }\`.`,
|
|
972
984
|
);
|
|
973
985
|
}
|
|
974
986
|
fail(
|
|
@@ -1087,11 +1099,16 @@ export function compile(
|
|
|
1087
1099
|
(binding.kind === "name" && binding.value === expr.value))
|
|
1088
1100
|
)
|
|
1089
1101
|
return expr;
|
|
1090
|
-
// A resolved party parameter
|
|
1102
|
+
// A resolved party parameter or an own parameter bound to a value takes
|
|
1103
|
+
// precedence over a same-named sibling attachment; `limits: limits`
|
|
1104
|
+
// binds by identity and still names the sibling.
|
|
1091
1105
|
if (attachmentInfo && !resolvedParties.has(expr.value)) {
|
|
1092
1106
|
const [local, ...tail] = expr.value.split(".");
|
|
1107
|
+
const own = environment.get(local!);
|
|
1108
|
+
const ownValue =
|
|
1109
|
+
own !== undefined && !(own.kind === "name" && own.value === local);
|
|
1093
1110
|
const target = `${attachmentInfo.subjectKindId}_${local}`;
|
|
1094
|
-
if (attachmentSubjects.has(target))
|
|
1111
|
+
if (!ownValue && attachmentSubjects.has(target))
|
|
1095
1112
|
return { ...expr, value: [target, ...tail].join("_") };
|
|
1096
1113
|
}
|
|
1097
1114
|
if (expr.value.startsWith("party.")) {
|
|
@@ -1141,7 +1158,7 @@ export function compile(
|
|
|
1141
1158
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
1142
1159
|
const type =
|
|
1143
1160
|
t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
1144
|
-
|
|
1161
|
+
let v =
|
|
1145
1162
|
(supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
1146
1163
|
? actual
|
|
1147
1164
|
: resolve(
|
|
@@ -1149,6 +1166,12 @@ export function compile(
|
|
|
1149
1166
|
new Set(),
|
|
1150
1167
|
!!attachmentInfo && type === "party",
|
|
1151
1168
|
);
|
|
1169
|
+
if (
|
|
1170
|
+
type === "duration" &&
|
|
1171
|
+
(v.kind === "text" || v.kind === "name") &&
|
|
1172
|
+
/^P/.test(v.value)
|
|
1173
|
+
)
|
|
1174
|
+
v = { ...v, kind: "duration" };
|
|
1152
1175
|
environment.set(param.key, v);
|
|
1153
1176
|
if (type === "enum" && t.kind === "call") {
|
|
1154
1177
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -1214,19 +1237,36 @@ export function compile(
|
|
|
1214
1237
|
"reference needs an object name",
|
|
1215
1238
|
"name a declared object",
|
|
1216
1239
|
);
|
|
1217
|
-
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1240
|
+
let targetType = objects.get(value.value)?.name;
|
|
1241
|
+
const candidates = [
|
|
1242
|
+
...[...assignments.values()].map(
|
|
1243
|
+
(assignment) => [assignment.name, assignment.target] as const,
|
|
1244
|
+
),
|
|
1245
|
+
...program.decls
|
|
1246
|
+
.filter((decl) => decl.kind === "instrument")
|
|
1247
|
+
.map((decl) => [decl.name, decl.name] as const),
|
|
1248
|
+
];
|
|
1249
|
+
for (const [instance, target] of candidates) {
|
|
1250
|
+
if (value.value === instance) {
|
|
1251
|
+
targetType = target;
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
if (
|
|
1255
|
+
!value.value.startsWith(`${instance}.`) &&
|
|
1256
|
+
!value.value.startsWith(`${instance}_`)
|
|
1257
|
+
)
|
|
1258
|
+
continue;
|
|
1259
|
+
const template = templates.get(target);
|
|
1260
|
+
if (!template) continue;
|
|
1261
|
+
const suffix = value.value
|
|
1262
|
+
.slice(instance.length + 1)
|
|
1263
|
+
.replaceAll(".", "_");
|
|
1264
|
+
const child = resolveChildExportPath(template, suffix);
|
|
1265
|
+
if (child) targetType = `${target}.${child}`;
|
|
1266
|
+
}
|
|
1221
1267
|
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
!document.instruments.some(
|
|
1225
|
-
(inst) => inst.id === value.value,
|
|
1226
|
-
)) ||
|
|
1227
|
-
(t.kind === "type" &&
|
|
1228
|
-
t.target &&
|
|
1229
|
-
[targetType, ...tail].join(".") !== t.target)
|
|
1268
|
+
!targetType ||
|
|
1269
|
+
(t.kind === "type" && t.target && targetType !== t.target)
|
|
1230
1270
|
)
|
|
1231
1271
|
fail(
|
|
1232
1272
|
value,
|
|
@@ -1258,17 +1298,22 @@ export function compile(
|
|
|
1258
1298
|
type === "date" ||
|
|
1259
1299
|
type === "duration" ||
|
|
1260
1300
|
type === "integer" ||
|
|
1261
|
-
type === "text"
|
|
1301
|
+
type === "text" ||
|
|
1302
|
+
type === "boolean"
|
|
1262
1303
|
) {
|
|
1304
|
+
const bounds = tunableBounds(t);
|
|
1263
1305
|
if (v.kind === "name" && v.value === "runtime") continue;
|
|
1264
|
-
const expected =
|
|
1306
|
+
const expected =
|
|
1307
|
+
type === "integer"
|
|
1308
|
+
? "number"
|
|
1309
|
+
: type === "boolean"
|
|
1310
|
+
? "name"
|
|
1311
|
+
: type;
|
|
1265
1312
|
if (
|
|
1266
|
-
v.kind !== expected
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
/^P/.test(v.value)
|
|
1271
|
-
)
|
|
1313
|
+
v.kind !== expected ||
|
|
1314
|
+
(type === "boolean" &&
|
|
1315
|
+
v.kind === "name" &&
|
|
1316
|
+
!["true", "false"].includes(v.value))
|
|
1272
1317
|
) {
|
|
1273
1318
|
if (type === "money" && v.kind === "name" && attachmentInfo) {
|
|
1274
1319
|
const target = v.value.replace(/^subject\./, "");
|
|
@@ -1301,7 +1346,6 @@ export function compile(
|
|
|
1301
1346
|
}
|
|
1302
1347
|
try {
|
|
1303
1348
|
const value = literal(v);
|
|
1304
|
-
const bounds = tunableBounds(t);
|
|
1305
1349
|
if (
|
|
1306
1350
|
bounds &&
|
|
1307
1351
|
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
@@ -1321,6 +1365,12 @@ export function compile(
|
|
|
1321
1365
|
);
|
|
1322
1366
|
throw error;
|
|
1323
1367
|
}
|
|
1368
|
+
} else {
|
|
1369
|
+
fail(
|
|
1370
|
+
t,
|
|
1371
|
+
`unknown tunable type ${type}`,
|
|
1372
|
+
"use a declared HSX tunable type",
|
|
1373
|
+
);
|
|
1324
1374
|
}
|
|
1325
1375
|
} catch (error) {
|
|
1326
1376
|
if (!(error instanceof CompileFailure)) throw error;
|
|
@@ -1373,6 +1423,21 @@ export function compile(
|
|
|
1373
1423
|
"constraint needs two declared tunables",
|
|
1374
1424
|
"name the compared tunables",
|
|
1375
1425
|
);
|
|
1426
|
+
const numericKind = (expr: Expr) =>
|
|
1427
|
+
expr.kind === "name" && /^P/.test(expr.value)
|
|
1428
|
+
? "duration"
|
|
1429
|
+
: expr.kind;
|
|
1430
|
+
if (
|
|
1431
|
+
!["money", "number", "percent", "duration"].includes(
|
|
1432
|
+
numericKind(left),
|
|
1433
|
+
) ||
|
|
1434
|
+
numericKind(left) !== numericKind(right)
|
|
1435
|
+
)
|
|
1436
|
+
fail(
|
|
1437
|
+
rule,
|
|
1438
|
+
"constraint needs numeric tunables of the same type",
|
|
1439
|
+
"compare two amounts, integers, percentages or durations",
|
|
1440
|
+
);
|
|
1376
1441
|
const a = BigInt(String(literal(left))),
|
|
1377
1442
|
b = BigInt(String(literal(right)));
|
|
1378
1443
|
if (
|
|
@@ -1473,7 +1538,7 @@ export function compile(
|
|
|
1473
1538
|
? se.value.items.map(text)
|
|
1474
1539
|
: [text(se.value)];
|
|
1475
1540
|
return names.some((n) => {
|
|
1476
|
-
const reg =
|
|
1541
|
+
const reg = adapterTarget(n);
|
|
1477
1542
|
if (!reg) return false;
|
|
1478
1543
|
const op = reg.adapter.operationMap[reg.operation];
|
|
1479
1544
|
return op?.subjectRequirements?.some(
|
|
@@ -1534,7 +1599,7 @@ export function compile(
|
|
|
1534
1599
|
instrumentVal = data(rawEntries.get("instrument")!);
|
|
1535
1600
|
if (famTuple) {
|
|
1536
1601
|
checkTargetFamily(
|
|
1537
|
-
instrumentVal
|
|
1602
|
+
instrumentVal,
|
|
1538
1603
|
famTuple,
|
|
1539
1604
|
rawEntries.get("instrument")!,
|
|
1540
1605
|
);
|
|
@@ -1612,7 +1677,7 @@ export function compile(
|
|
|
1612
1677
|
const lowerFields = (block: BlockExpr): UdlField[] => {
|
|
1613
1678
|
const result: UdlField[] = [];
|
|
1614
1679
|
for (const row of block.entries) {
|
|
1615
|
-
const
|
|
1680
|
+
const { type: t } = fieldType(row);
|
|
1616
1681
|
const constant =
|
|
1617
1682
|
row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
1618
1683
|
const f = lowerFieldShape(row, resolve);
|
|
@@ -1842,7 +1907,7 @@ export function compile(
|
|
|
1842
1907
|
op: "sum",
|
|
1843
1908
|
values: [val(constant)],
|
|
1844
1909
|
});
|
|
1845
|
-
else f.value = literal(constant);
|
|
1910
|
+
else f.value = literal(constant, String(type));
|
|
1846
1911
|
}
|
|
1847
1912
|
result.push(f as UdlField);
|
|
1848
1913
|
}
|
|
@@ -1850,9 +1915,11 @@ export function compile(
|
|
|
1850
1915
|
};
|
|
1851
1916
|
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
1852
1917
|
const lifecycle = data(
|
|
1853
|
-
body.get("lifecycle")
|
|
1918
|
+
asBlock(body.get("lifecycle")),
|
|
1854
1919
|
) as UdlInstrument["lifecycle"];
|
|
1855
|
-
lifecycle.transitions =
|
|
1920
|
+
lifecycle.transitions = Object.create(
|
|
1921
|
+
null,
|
|
1922
|
+
) as UdlInstrument["lifecycle"]["transitions"];
|
|
1856
1923
|
const inst: UdlInstrument = {
|
|
1857
1924
|
id,
|
|
1858
1925
|
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
@@ -1863,7 +1930,7 @@ export function compile(
|
|
|
1863
1930
|
fields,
|
|
1864
1931
|
calculate: calculations,
|
|
1865
1932
|
lifecycle,
|
|
1866
|
-
actions:
|
|
1933
|
+
actions: Object.create(null) as UdlInstrument["actions"],
|
|
1867
1934
|
actionOrder: [],
|
|
1868
1935
|
};
|
|
1869
1936
|
for (const row of decl.body.entries.filter((e) =>
|
|
@@ -1970,15 +2037,8 @@ export function compile(
|
|
|
1970
2037
|
"name a bound ADL adapter",
|
|
1971
2038
|
);
|
|
1972
2039
|
const binding = text(resolve(adapterExpr!));
|
|
1973
|
-
const target =
|
|
1974
|
-
|
|
1975
|
-
Object.hasOwn(options.adapterRegistry, binding)
|
|
1976
|
-
? options.adapterRegistry[binding]
|
|
1977
|
-
: undefined;
|
|
1978
|
-
if (
|
|
1979
|
-
!target ||
|
|
1980
|
-
!Object.hasOwn(target.adapter.operationMap, target.operation)
|
|
1981
|
-
)
|
|
2040
|
+
const target = adapterTarget(binding);
|
|
2041
|
+
if (!target)
|
|
1982
2042
|
fail(
|
|
1983
2043
|
boundary,
|
|
1984
2044
|
`unknown boundary adapter ${binding}`,
|
|
@@ -2034,7 +2094,7 @@ export function compile(
|
|
|
2034
2094
|
? entry.value.items.map(adapterName)
|
|
2035
2095
|
: [adapterName(entry.value)];
|
|
2036
2096
|
for (const bindingName of bindingNames) {
|
|
2037
|
-
const target =
|
|
2097
|
+
const target = adapterTarget(bindingName);
|
|
2038
2098
|
if (target) {
|
|
2039
2099
|
const { adapter, operation } = target;
|
|
2040
2100
|
const opBinding = adapter.operationMap[operation];
|
|
@@ -2562,22 +2622,39 @@ export function compile(
|
|
|
2562
2622
|
}
|
|
2563
2623
|
if (attachmentInfo && !attachmentInfo.child) {
|
|
2564
2624
|
if (attachmentInfo.exposed.has(name)) {
|
|
2625
|
+
if (automatic(a.actor))
|
|
2626
|
+
fail(
|
|
2627
|
+
row,
|
|
2628
|
+
`${name} runs on the clock or its parent, not a caller`,
|
|
2629
|
+
"expose only caller actions",
|
|
2630
|
+
);
|
|
2565
2631
|
a.publicAction = attachmentInfo.exposed.get(name)!;
|
|
2566
2632
|
} else {
|
|
2567
2633
|
delete a.publicAction;
|
|
2568
2634
|
}
|
|
2569
2635
|
}
|
|
2570
2636
|
if (automatic(a.actor)) delete a.publicAction;
|
|
2571
|
-
const checkSubjectPaths = (obj: unknown,
|
|
2637
|
+
const checkSubjectPaths = (obj: unknown, key = "") => {
|
|
2572
2638
|
if (typeof obj === "string") {
|
|
2573
|
-
if (
|
|
2639
|
+
if (
|
|
2640
|
+
[
|
|
2641
|
+
"field",
|
|
2642
|
+
"fields",
|
|
2643
|
+
"reference",
|
|
2644
|
+
"anchor",
|
|
2645
|
+
"subject",
|
|
2646
|
+
"at",
|
|
2647
|
+
"instruction",
|
|
2648
|
+
].includes(key) &&
|
|
2649
|
+
obj.startsWith("subject.")
|
|
2650
|
+
) {
|
|
2574
2651
|
const subField = obj.split(".")[1]!;
|
|
2575
2652
|
const req = a.subject?.requirements.find(
|
|
2576
2653
|
(r) => r.field.name === subField,
|
|
2577
2654
|
);
|
|
2578
2655
|
if (!req) {
|
|
2579
2656
|
failWithCode(
|
|
2580
|
-
|
|
2657
|
+
row,
|
|
2581
2658
|
"subject_field_unknown",
|
|
2582
2659
|
`subject.${subField} names no declared subject requirement in action ${name}`,
|
|
2583
2660
|
`declare ${subField} in subject { ... }`,
|
|
@@ -2585,14 +2662,15 @@ export function compile(
|
|
|
2585
2662
|
}
|
|
2586
2663
|
}
|
|
2587
2664
|
} else if (Array.isArray(obj)) {
|
|
2588
|
-
for (const item of obj) checkSubjectPaths(item,
|
|
2665
|
+
for (const item of obj) checkSubjectPaths(item, key);
|
|
2589
2666
|
} else if (obj !== null && typeof obj === "object") {
|
|
2590
|
-
for (const val of Object.
|
|
2667
|
+
for (const [key, val] of Object.entries(obj))
|
|
2668
|
+
checkSubjectPaths(val, key);
|
|
2591
2669
|
}
|
|
2592
2670
|
};
|
|
2593
|
-
checkSubjectPaths(a.requires
|
|
2594
|
-
checkSubjectPaths(a.set
|
|
2595
|
-
checkSubjectPaths(a.invoke
|
|
2671
|
+
checkSubjectPaths(a.requires);
|
|
2672
|
+
checkSubjectPaths(a.set);
|
|
2673
|
+
checkSubjectPaths(a.invoke);
|
|
2596
2674
|
currentAction = undefined;
|
|
2597
2675
|
currentActionName = undefined;
|
|
2598
2676
|
for (const requirement of a.subject?.requirements ?? []) {
|
|
@@ -2617,6 +2695,27 @@ export function compile(
|
|
|
2617
2695
|
(inst as unknown as Record<string, unknown>)[key] = data(
|
|
2618
2696
|
body.get(key)!,
|
|
2619
2697
|
);
|
|
2698
|
+
const shape = udlInstrumentSchema.safeParse(inst);
|
|
2699
|
+
if (!shape.success) {
|
|
2700
|
+
const issue = shape.error.issues[0]!;
|
|
2701
|
+
const actionName =
|
|
2702
|
+
issue.path[0] === "actions" ? String(issue.path[1]) : undefined;
|
|
2703
|
+
const node =
|
|
2704
|
+
decl.body.entries.find(
|
|
2705
|
+
(entry) =>
|
|
2706
|
+
entry.key ===
|
|
2707
|
+
(actionName ? `action ${actionName}` : String(issue.path[0])),
|
|
2708
|
+
) ?? decl;
|
|
2709
|
+
failWithCode(
|
|
2710
|
+
{
|
|
2711
|
+
span: node.span,
|
|
2712
|
+
source: declarationSources.get(decl) ?? "program",
|
|
2713
|
+
},
|
|
2714
|
+
"UDL1003",
|
|
2715
|
+
`${id}.${issue.path.join(".")}: ${issue.message}`,
|
|
2716
|
+
"use the UDL typed clause shape",
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2620
2719
|
origins.push({
|
|
2621
2720
|
path: `$.instruments[${document.instruments.length}]`,
|
|
2622
2721
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
@@ -2696,7 +2795,6 @@ export function compile(
|
|
|
2696
2795
|
}
|
|
2697
2796
|
}
|
|
2698
2797
|
|
|
2699
|
-
// 1. Lower authored fields
|
|
2700
2798
|
const authoredFields: UdlObjectField[] = [];
|
|
2701
2799
|
const authoredNames: string[] = [];
|
|
2702
2800
|
const fieldsBlock = asBlock(body.get("fields"));
|
|
@@ -2713,7 +2811,6 @@ export function compile(
|
|
|
2713
2811
|
authoredNames.push(row.key);
|
|
2714
2812
|
}
|
|
2715
2813
|
|
|
2716
|
-
// 2. Process attachments
|
|
2717
2814
|
const attachments: UdlObjectAttachment[] = [];
|
|
2718
2815
|
for (const entry of decl.body.entries) {
|
|
2719
2816
|
if (!entry.key.startsWith("attach ")) continue;
|
|
@@ -2746,6 +2843,12 @@ export function compile(
|
|
|
2746
2843
|
for (const row of attachmentBlock.entries) {
|
|
2747
2844
|
if (row.key === "rename") {
|
|
2748
2845
|
for (const r of asBlock(row.value).entries) {
|
|
2846
|
+
if (renames.has(r.key))
|
|
2847
|
+
fail(
|
|
2848
|
+
r,
|
|
2849
|
+
`duplicate rename ${r.key}`,
|
|
2850
|
+
"rename each subject field once",
|
|
2851
|
+
);
|
|
2749
2852
|
renames.set(r.key, text(r.value));
|
|
2750
2853
|
renameEntries.set(r.key, r);
|
|
2751
2854
|
}
|
|
@@ -2753,7 +2856,23 @@ export function compile(
|
|
|
2753
2856
|
if (row.value.kind === "call") {
|
|
2754
2857
|
const actionName = row.value.name;
|
|
2755
2858
|
const publicName = text(row.value.args[0]!);
|
|
2859
|
+
if (
|
|
2860
|
+
exposed.has(actionName) ||
|
|
2861
|
+
!template.body.entries.some(
|
|
2862
|
+
(entry) => entry.key === `action ${actionName}`,
|
|
2863
|
+
)
|
|
2864
|
+
)
|
|
2865
|
+
fail(
|
|
2866
|
+
row,
|
|
2867
|
+
`unknown or repeated action ${actionName}`,
|
|
2868
|
+
"expose one declared action once",
|
|
2869
|
+
);
|
|
2756
2870
|
exposed.set(actionName, publicName);
|
|
2871
|
+
exposures.push({
|
|
2872
|
+
instrument: instId,
|
|
2873
|
+
action: actionName,
|
|
2874
|
+
entry: row,
|
|
2875
|
+
});
|
|
2757
2876
|
}
|
|
2758
2877
|
} else {
|
|
2759
2878
|
tunableEntries.push(row);
|
|
@@ -2882,7 +3001,6 @@ export function compile(
|
|
|
2882
3001
|
}
|
|
2883
3002
|
}
|
|
2884
3003
|
|
|
2885
|
-
// 4. Validate columns
|
|
2886
3004
|
const columnsExpr = body.get("columns");
|
|
2887
3005
|
let columns: string[] = [];
|
|
2888
3006
|
if (columnsExpr) {
|
|
@@ -2950,7 +3068,11 @@ export function compile(
|
|
|
2950
3068
|
// Propagate invoked requirements with the conditions on each invocation path.
|
|
2951
3069
|
let changedInvocations = true;
|
|
2952
3070
|
let invocationIterations = 0;
|
|
2953
|
-
|
|
3071
|
+
const invocationDepth = document.instruments.reduce(
|
|
3072
|
+
(total, inst) => total + inst.actionOrder.length,
|
|
3073
|
+
0,
|
|
3074
|
+
);
|
|
3075
|
+
while (changedInvocations && invocationIterations < invocationDepth) {
|
|
2954
3076
|
changedInvocations = false;
|
|
2955
3077
|
invocationIterations++;
|
|
2956
3078
|
for (const inst of document.instruments) {
|
|
@@ -3167,6 +3289,17 @@ export function compile(
|
|
|
3167
3289
|
const eliminatedStates = new Map<string, Set<string>>();
|
|
3168
3290
|
// Remove branches excluded by immutable tunables before checking reference states.
|
|
3169
3291
|
for (const inst of document.instruments) {
|
|
3292
|
+
if (
|
|
3293
|
+
!inst.lifecycle.states.includes(inst.lifecycle.initial) ||
|
|
3294
|
+
Object.values(inst.lifecycle.transitions).some(
|
|
3295
|
+
(edge) =>
|
|
3296
|
+
edge.from.some((state) => !inst.lifecycle.states.includes(state)) ||
|
|
3297
|
+
(edge.to !== "preserve" &&
|
|
3298
|
+
!inst.lifecycle.states.includes(edge.to)),
|
|
3299
|
+
)
|
|
3300
|
+
)
|
|
3301
|
+
continue;
|
|
3302
|
+
let specialized = false;
|
|
3170
3303
|
for (const [key, action] of Object.entries(inst.actions)) {
|
|
3171
3304
|
const constant = (value: import("@hyperscale0/udl").UdlValue) => {
|
|
3172
3305
|
if ("literal" in value) return value.literal;
|
|
@@ -3188,8 +3321,10 @@ export function compile(
|
|
|
3188
3321
|
) {
|
|
3189
3322
|
delete inst.actions[key];
|
|
3190
3323
|
delete inst.lifecycle.transitions[key];
|
|
3324
|
+
specialized = true;
|
|
3191
3325
|
}
|
|
3192
3326
|
}
|
|
3327
|
+
if (!specialized) continue;
|
|
3193
3328
|
const reachable = new Set([inst.lifecycle.initial]);
|
|
3194
3329
|
for (let n = 0; n < inst.lifecycle.states.length; n++)
|
|
3195
3330
|
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
@@ -3229,6 +3364,16 @@ export function compile(
|
|
|
3229
3364
|
);
|
|
3230
3365
|
}
|
|
3231
3366
|
}
|
|
3367
|
+
for (const exposure of exposures)
|
|
3368
|
+
if (
|
|
3369
|
+
!document.instruments.find((inst) => inst.id === exposure.instrument)
|
|
3370
|
+
?.actions[exposure.action]
|
|
3371
|
+
)
|
|
3372
|
+
fail(
|
|
3373
|
+
exposure.entry,
|
|
3374
|
+
`action ${exposure.action} is excluded by these bindings`,
|
|
3375
|
+
"expose an action available with these tunables",
|
|
3376
|
+
);
|
|
3232
3377
|
const changed = new Set<string>();
|
|
3233
3378
|
for (const decl of program.decls)
|
|
3234
3379
|
if (decl.kind === "hide" || decl.kind === "expose") {
|
|
@@ -3254,6 +3399,24 @@ export function compile(
|
|
|
3254
3399
|
);
|
|
3255
3400
|
else action!.publicAction = decl.name!;
|
|
3256
3401
|
}
|
|
3402
|
+
for (const decl of program.decls) {
|
|
3403
|
+
if (decl.kind !== "expose") continue;
|
|
3404
|
+
const parts = decl.target.split(".");
|
|
3405
|
+
parts.pop();
|
|
3406
|
+
const instrument = document.instruments.find(
|
|
3407
|
+
(item) => item.id === parts.join("_"),
|
|
3408
|
+
)!;
|
|
3409
|
+
if (
|
|
3410
|
+
Object.values(instrument.actions).filter(
|
|
3411
|
+
(action) => action.publicAction === decl.name,
|
|
3412
|
+
).length > 1
|
|
3413
|
+
)
|
|
3414
|
+
fail(
|
|
3415
|
+
decl,
|
|
3416
|
+
`public action ${decl.name} is used more than once on ${parts.join(".")}`,
|
|
3417
|
+
"give each exposed action a distinct public name",
|
|
3418
|
+
);
|
|
3419
|
+
}
|
|
3257
3420
|
const usedParties = new Set<string>();
|
|
3258
3421
|
const collectParties = (value: unknown): void => {
|
|
3259
3422
|
if (typeof value === "string") {
|