@hyperscale0/hsx 5.1.0 → 5.3.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 +28 -0
- 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.js +1 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +193 -102
- 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/examples-bundle.js +2 -2
- package/dist/src/examples-bundle.js.map +1 -1
- 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 +36 -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 +2 -2
- 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 -1
- package/dist/src/version.js +1 -1
- package/docs/README.md +18 -10
- package/examples/insurance.hsx +2 -2
- package/examples/travel.hsx +2 -2
- package/package.json +3 -3
- package/src/ast.ts +0 -10
- package/src/cli.ts +1 -1
- package/src/compile.ts +277 -128
- package/src/diagnostics.ts +28 -0
- package/src/examples-bundle.ts +2 -2
- package/src/header-source.ts +44 -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 +2 -2
- package/src/tunables.ts +6 -1
- package/src/version.ts +1 -1
- package/std/financing.hsx +2 -2
- package/std/insurance.hsx +9 -2
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(
|
|
@@ -1141,7 +1153,7 @@ export function compile(
|
|
|
1141
1153
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
1142
1154
|
const type =
|
|
1143
1155
|
t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
1144
|
-
|
|
1156
|
+
let v =
|
|
1145
1157
|
(supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
1146
1158
|
? actual
|
|
1147
1159
|
: resolve(
|
|
@@ -1149,6 +1161,12 @@ export function compile(
|
|
|
1149
1161
|
new Set(),
|
|
1150
1162
|
!!attachmentInfo && type === "party",
|
|
1151
1163
|
);
|
|
1164
|
+
if (
|
|
1165
|
+
type === "duration" &&
|
|
1166
|
+
(v.kind === "text" || v.kind === "name") &&
|
|
1167
|
+
/^P/.test(v.value)
|
|
1168
|
+
)
|
|
1169
|
+
v = { ...v, kind: "duration" };
|
|
1152
1170
|
environment.set(param.key, v);
|
|
1153
1171
|
if (type === "enum" && t.kind === "call") {
|
|
1154
1172
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -1214,19 +1232,36 @@ export function compile(
|
|
|
1214
1232
|
"reference needs an object name",
|
|
1215
1233
|
"name a declared object",
|
|
1216
1234
|
);
|
|
1217
|
-
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1235
|
+
let targetType = objects.get(value.value)?.name;
|
|
1236
|
+
const candidates = [
|
|
1237
|
+
...[...assignments.values()].map(
|
|
1238
|
+
(assignment) => [assignment.name, assignment.target] as const,
|
|
1239
|
+
),
|
|
1240
|
+
...program.decls
|
|
1241
|
+
.filter((decl) => decl.kind === "instrument")
|
|
1242
|
+
.map((decl) => [decl.name, decl.name] as const),
|
|
1243
|
+
];
|
|
1244
|
+
for (const [instance, target] of candidates) {
|
|
1245
|
+
if (value.value === instance) {
|
|
1246
|
+
targetType = target;
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
if (
|
|
1250
|
+
!value.value.startsWith(`${instance}.`) &&
|
|
1251
|
+
!value.value.startsWith(`${instance}_`)
|
|
1252
|
+
)
|
|
1253
|
+
continue;
|
|
1254
|
+
const template = templates.get(target);
|
|
1255
|
+
if (!template) continue;
|
|
1256
|
+
const suffix = value.value
|
|
1257
|
+
.slice(instance.length + 1)
|
|
1258
|
+
.replaceAll(".", "_");
|
|
1259
|
+
const child = resolveChildExportPath(template, suffix);
|
|
1260
|
+
if (child) targetType = `${target}.${child}`;
|
|
1261
|
+
}
|
|
1221
1262
|
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)
|
|
1263
|
+
!targetType ||
|
|
1264
|
+
(t.kind === "type" && t.target && targetType !== t.target)
|
|
1230
1265
|
)
|
|
1231
1266
|
fail(
|
|
1232
1267
|
value,
|
|
@@ -1258,17 +1293,22 @@ export function compile(
|
|
|
1258
1293
|
type === "date" ||
|
|
1259
1294
|
type === "duration" ||
|
|
1260
1295
|
type === "integer" ||
|
|
1261
|
-
type === "text"
|
|
1296
|
+
type === "text" ||
|
|
1297
|
+
type === "boolean"
|
|
1262
1298
|
) {
|
|
1299
|
+
const bounds = tunableBounds(t);
|
|
1263
1300
|
if (v.kind === "name" && v.value === "runtime") continue;
|
|
1264
|
-
const expected =
|
|
1301
|
+
const expected =
|
|
1302
|
+
type === "integer"
|
|
1303
|
+
? "number"
|
|
1304
|
+
: type === "boolean"
|
|
1305
|
+
? "name"
|
|
1306
|
+
: type;
|
|
1265
1307
|
if (
|
|
1266
|
-
v.kind !== expected
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
/^P/.test(v.value)
|
|
1271
|
-
)
|
|
1308
|
+
v.kind !== expected ||
|
|
1309
|
+
(type === "boolean" &&
|
|
1310
|
+
v.kind === "name" &&
|
|
1311
|
+
!["true", "false"].includes(v.value))
|
|
1272
1312
|
) {
|
|
1273
1313
|
if (type === "money" && v.kind === "name" && attachmentInfo) {
|
|
1274
1314
|
const target = v.value.replace(/^subject\./, "");
|
|
@@ -1301,7 +1341,6 @@ export function compile(
|
|
|
1301
1341
|
}
|
|
1302
1342
|
try {
|
|
1303
1343
|
const value = literal(v);
|
|
1304
|
-
const bounds = tunableBounds(t);
|
|
1305
1344
|
if (
|
|
1306
1345
|
bounds &&
|
|
1307
1346
|
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
@@ -1321,6 +1360,12 @@ export function compile(
|
|
|
1321
1360
|
);
|
|
1322
1361
|
throw error;
|
|
1323
1362
|
}
|
|
1363
|
+
} else {
|
|
1364
|
+
fail(
|
|
1365
|
+
t,
|
|
1366
|
+
`unknown tunable type ${type}`,
|
|
1367
|
+
"use a declared HSX tunable type",
|
|
1368
|
+
);
|
|
1324
1369
|
}
|
|
1325
1370
|
} catch (error) {
|
|
1326
1371
|
if (!(error instanceof CompileFailure)) throw error;
|
|
@@ -1373,6 +1418,21 @@ export function compile(
|
|
|
1373
1418
|
"constraint needs two declared tunables",
|
|
1374
1419
|
"name the compared tunables",
|
|
1375
1420
|
);
|
|
1421
|
+
const numericKind = (expr: Expr) =>
|
|
1422
|
+
expr.kind === "name" && /^P/.test(expr.value)
|
|
1423
|
+
? "duration"
|
|
1424
|
+
: expr.kind;
|
|
1425
|
+
if (
|
|
1426
|
+
!["money", "number", "percent", "duration"].includes(
|
|
1427
|
+
numericKind(left),
|
|
1428
|
+
) ||
|
|
1429
|
+
numericKind(left) !== numericKind(right)
|
|
1430
|
+
)
|
|
1431
|
+
fail(
|
|
1432
|
+
rule,
|
|
1433
|
+
"constraint needs numeric tunables of the same type",
|
|
1434
|
+
"compare two amounts, integers, percentages or durations",
|
|
1435
|
+
);
|
|
1376
1436
|
const a = BigInt(String(literal(left))),
|
|
1377
1437
|
b = BigInt(String(literal(right)));
|
|
1378
1438
|
if (
|
|
@@ -1473,7 +1533,7 @@ export function compile(
|
|
|
1473
1533
|
? se.value.items.map(text)
|
|
1474
1534
|
: [text(se.value)];
|
|
1475
1535
|
return names.some((n) => {
|
|
1476
|
-
const reg =
|
|
1536
|
+
const reg = adapterTarget(n);
|
|
1477
1537
|
if (!reg) return false;
|
|
1478
1538
|
const op = reg.adapter.operationMap[reg.operation];
|
|
1479
1539
|
return op?.subjectRequirements?.some(
|
|
@@ -1534,7 +1594,7 @@ export function compile(
|
|
|
1534
1594
|
instrumentVal = data(rawEntries.get("instrument")!);
|
|
1535
1595
|
if (famTuple) {
|
|
1536
1596
|
checkTargetFamily(
|
|
1537
|
-
instrumentVal
|
|
1597
|
+
instrumentVal,
|
|
1538
1598
|
famTuple,
|
|
1539
1599
|
rawEntries.get("instrument")!,
|
|
1540
1600
|
);
|
|
@@ -1612,7 +1672,7 @@ export function compile(
|
|
|
1612
1672
|
const lowerFields = (block: BlockExpr): UdlField[] => {
|
|
1613
1673
|
const result: UdlField[] = [];
|
|
1614
1674
|
for (const row of block.entries) {
|
|
1615
|
-
const
|
|
1675
|
+
const { type: t } = fieldType(row);
|
|
1616
1676
|
const constant =
|
|
1617
1677
|
row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
1618
1678
|
const f = lowerFieldShape(row, resolve);
|
|
@@ -1711,7 +1771,16 @@ export function compile(
|
|
|
1711
1771
|
"account needs an owner, optional book, mode and key",
|
|
1712
1772
|
"write account(buyer, claim, contra)",
|
|
1713
1773
|
);
|
|
1714
|
-
|
|
1774
|
+
const owner = t.args[0]!;
|
|
1775
|
+
if (owner.kind === "call" && owner.name === "adapter") {
|
|
1776
|
+
if (owner.args.length !== 1)
|
|
1777
|
+
fail(
|
|
1778
|
+
owner,
|
|
1779
|
+
"adapter needs one binding",
|
|
1780
|
+
'write account(adapter(binding), cash, "premium")',
|
|
1781
|
+
);
|
|
1782
|
+
f.owner = { adapter: text(resolve(owner.args[0]!)) };
|
|
1783
|
+
} else f.owner = text(resolve(owner));
|
|
1715
1784
|
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
1716
1785
|
if (t.args[2]) {
|
|
1717
1786
|
const mode = text(t.args[2]);
|
|
@@ -1833,7 +1902,7 @@ export function compile(
|
|
|
1833
1902
|
op: "sum",
|
|
1834
1903
|
values: [val(constant)],
|
|
1835
1904
|
});
|
|
1836
|
-
else f.value = literal(constant);
|
|
1905
|
+
else f.value = literal(constant, String(type));
|
|
1837
1906
|
}
|
|
1838
1907
|
result.push(f as UdlField);
|
|
1839
1908
|
}
|
|
@@ -1841,9 +1910,11 @@ export function compile(
|
|
|
1841
1910
|
};
|
|
1842
1911
|
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
1843
1912
|
const lifecycle = data(
|
|
1844
|
-
body.get("lifecycle")
|
|
1913
|
+
asBlock(body.get("lifecycle")),
|
|
1845
1914
|
) as UdlInstrument["lifecycle"];
|
|
1846
|
-
lifecycle.transitions =
|
|
1915
|
+
lifecycle.transitions = Object.create(
|
|
1916
|
+
null,
|
|
1917
|
+
) as UdlInstrument["lifecycle"]["transitions"];
|
|
1847
1918
|
const inst: UdlInstrument = {
|
|
1848
1919
|
id,
|
|
1849
1920
|
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
@@ -1854,7 +1925,7 @@ export function compile(
|
|
|
1854
1925
|
fields,
|
|
1855
1926
|
calculate: calculations,
|
|
1856
1927
|
lifecycle,
|
|
1857
|
-
actions:
|
|
1928
|
+
actions: Object.create(null) as UdlInstrument["actions"],
|
|
1858
1929
|
actionOrder: [],
|
|
1859
1930
|
};
|
|
1860
1931
|
for (const row of decl.body.entries.filter((e) =>
|
|
@@ -1961,15 +2032,8 @@ export function compile(
|
|
|
1961
2032
|
"name a bound ADL adapter",
|
|
1962
2033
|
);
|
|
1963
2034
|
const binding = text(resolve(adapterExpr!));
|
|
1964
|
-
const target =
|
|
1965
|
-
|
|
1966
|
-
Object.hasOwn(options.adapterRegistry, binding)
|
|
1967
|
-
? options.adapterRegistry[binding]
|
|
1968
|
-
: undefined;
|
|
1969
|
-
if (
|
|
1970
|
-
!target ||
|
|
1971
|
-
!Object.hasOwn(target.adapter.operationMap, target.operation)
|
|
1972
|
-
)
|
|
2035
|
+
const target = adapterTarget(binding);
|
|
2036
|
+
if (!target)
|
|
1973
2037
|
fail(
|
|
1974
2038
|
boundary,
|
|
1975
2039
|
`unknown boundary adapter ${binding}`,
|
|
@@ -2025,7 +2089,7 @@ export function compile(
|
|
|
2025
2089
|
? entry.value.items.map(adapterName)
|
|
2026
2090
|
: [adapterName(entry.value)];
|
|
2027
2091
|
for (const bindingName of bindingNames) {
|
|
2028
|
-
const target =
|
|
2092
|
+
const target = adapterTarget(bindingName);
|
|
2029
2093
|
if (target) {
|
|
2030
2094
|
const { adapter, operation } = target;
|
|
2031
2095
|
const opBinding = adapter.operationMap[operation];
|
|
@@ -2553,15 +2617,32 @@ export function compile(
|
|
|
2553
2617
|
}
|
|
2554
2618
|
if (attachmentInfo && !attachmentInfo.child) {
|
|
2555
2619
|
if (attachmentInfo.exposed.has(name)) {
|
|
2620
|
+
if (automatic(a.actor))
|
|
2621
|
+
fail(
|
|
2622
|
+
row,
|
|
2623
|
+
`${name} runs on the clock or its parent, not a caller`,
|
|
2624
|
+
"expose only caller actions",
|
|
2625
|
+
);
|
|
2556
2626
|
a.publicAction = attachmentInfo.exposed.get(name)!;
|
|
2557
2627
|
} else {
|
|
2558
2628
|
delete a.publicAction;
|
|
2559
2629
|
}
|
|
2560
2630
|
}
|
|
2561
2631
|
if (automatic(a.actor)) delete a.publicAction;
|
|
2562
|
-
const checkSubjectPaths = (obj: unknown, span: Span) => {
|
|
2632
|
+
const checkSubjectPaths = (obj: unknown, span: Span, key = "") => {
|
|
2563
2633
|
if (typeof obj === "string") {
|
|
2564
|
-
if (
|
|
2634
|
+
if (
|
|
2635
|
+
[
|
|
2636
|
+
"field",
|
|
2637
|
+
"fields",
|
|
2638
|
+
"reference",
|
|
2639
|
+
"anchor",
|
|
2640
|
+
"subject",
|
|
2641
|
+
"at",
|
|
2642
|
+
"instruction",
|
|
2643
|
+
].includes(key) &&
|
|
2644
|
+
obj.startsWith("subject.")
|
|
2645
|
+
) {
|
|
2565
2646
|
const subField = obj.split(".")[1]!;
|
|
2566
2647
|
const req = a.subject?.requirements.find(
|
|
2567
2648
|
(r) => r.field.name === subField,
|
|
@@ -2576,9 +2657,10 @@ export function compile(
|
|
|
2576
2657
|
}
|
|
2577
2658
|
}
|
|
2578
2659
|
} else if (Array.isArray(obj)) {
|
|
2579
|
-
for (const item of obj) checkSubjectPaths(item, span);
|
|
2660
|
+
for (const item of obj) checkSubjectPaths(item, span, key);
|
|
2580
2661
|
} else if (obj !== null && typeof obj === "object") {
|
|
2581
|
-
for (const val of Object.
|
|
2662
|
+
for (const [key, val] of Object.entries(obj))
|
|
2663
|
+
checkSubjectPaths(val, span, key);
|
|
2582
2664
|
}
|
|
2583
2665
|
};
|
|
2584
2666
|
checkSubjectPaths(a.requires, row.span);
|
|
@@ -2608,6 +2690,27 @@ export function compile(
|
|
|
2608
2690
|
(inst as unknown as Record<string, unknown>)[key] = data(
|
|
2609
2691
|
body.get(key)!,
|
|
2610
2692
|
);
|
|
2693
|
+
const shape = udlInstrumentSchema.safeParse(inst);
|
|
2694
|
+
if (!shape.success) {
|
|
2695
|
+
const issue = shape.error.issues[0]!;
|
|
2696
|
+
const actionName =
|
|
2697
|
+
issue.path[0] === "actions" ? String(issue.path[1]) : undefined;
|
|
2698
|
+
const node =
|
|
2699
|
+
decl.body.entries.find(
|
|
2700
|
+
(entry) =>
|
|
2701
|
+
entry.key ===
|
|
2702
|
+
(actionName ? `action ${actionName}` : String(issue.path[0])),
|
|
2703
|
+
) ?? decl;
|
|
2704
|
+
failWithCode(
|
|
2705
|
+
{
|
|
2706
|
+
span: node.span,
|
|
2707
|
+
source: declarationSources.get(decl) ?? "program",
|
|
2708
|
+
},
|
|
2709
|
+
"UDL1003",
|
|
2710
|
+
`${id}.${issue.path.join(".")}: ${issue.message}`,
|
|
2711
|
+
"use the UDL typed clause shape",
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2611
2714
|
origins.push({
|
|
2612
2715
|
path: `$.instruments[${document.instruments.length}]`,
|
|
2613
2716
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
@@ -2687,7 +2790,6 @@ export function compile(
|
|
|
2687
2790
|
}
|
|
2688
2791
|
}
|
|
2689
2792
|
|
|
2690
|
-
// 1. Lower authored fields
|
|
2691
2793
|
const authoredFields: UdlObjectField[] = [];
|
|
2692
2794
|
const authoredNames: string[] = [];
|
|
2693
2795
|
const fieldsBlock = asBlock(body.get("fields"));
|
|
@@ -2704,7 +2806,6 @@ export function compile(
|
|
|
2704
2806
|
authoredNames.push(row.key);
|
|
2705
2807
|
}
|
|
2706
2808
|
|
|
2707
|
-
// 2. Process attachments
|
|
2708
2809
|
const attachments: UdlObjectAttachment[] = [];
|
|
2709
2810
|
for (const entry of decl.body.entries) {
|
|
2710
2811
|
if (!entry.key.startsWith("attach ")) continue;
|
|
@@ -2737,6 +2838,12 @@ export function compile(
|
|
|
2737
2838
|
for (const row of attachmentBlock.entries) {
|
|
2738
2839
|
if (row.key === "rename") {
|
|
2739
2840
|
for (const r of asBlock(row.value).entries) {
|
|
2841
|
+
if (renames.has(r.key))
|
|
2842
|
+
fail(
|
|
2843
|
+
r,
|
|
2844
|
+
`duplicate rename ${r.key}`,
|
|
2845
|
+
"rename each subject field once",
|
|
2846
|
+
);
|
|
2740
2847
|
renames.set(r.key, text(r.value));
|
|
2741
2848
|
renameEntries.set(r.key, r);
|
|
2742
2849
|
}
|
|
@@ -2744,7 +2851,23 @@ export function compile(
|
|
|
2744
2851
|
if (row.value.kind === "call") {
|
|
2745
2852
|
const actionName = row.value.name;
|
|
2746
2853
|
const publicName = text(row.value.args[0]!);
|
|
2854
|
+
if (
|
|
2855
|
+
exposed.has(actionName) ||
|
|
2856
|
+
!template.body.entries.some(
|
|
2857
|
+
(entry) => entry.key === `action ${actionName}`,
|
|
2858
|
+
)
|
|
2859
|
+
)
|
|
2860
|
+
fail(
|
|
2861
|
+
row,
|
|
2862
|
+
`unknown or repeated action ${actionName}`,
|
|
2863
|
+
"expose one declared action once",
|
|
2864
|
+
);
|
|
2747
2865
|
exposed.set(actionName, publicName);
|
|
2866
|
+
exposures.push({
|
|
2867
|
+
instrument: instId,
|
|
2868
|
+
action: actionName,
|
|
2869
|
+
entry: row,
|
|
2870
|
+
});
|
|
2748
2871
|
}
|
|
2749
2872
|
} else {
|
|
2750
2873
|
tunableEntries.push(row);
|
|
@@ -2873,7 +2996,6 @@ export function compile(
|
|
|
2873
2996
|
}
|
|
2874
2997
|
}
|
|
2875
2998
|
|
|
2876
|
-
// 4. Validate columns
|
|
2877
2999
|
const columnsExpr = body.get("columns");
|
|
2878
3000
|
let columns: string[] = [];
|
|
2879
3001
|
if (columnsExpr) {
|
|
@@ -2941,7 +3063,11 @@ export function compile(
|
|
|
2941
3063
|
// Propagate invoked requirements with the conditions on each invocation path.
|
|
2942
3064
|
let changedInvocations = true;
|
|
2943
3065
|
let invocationIterations = 0;
|
|
2944
|
-
|
|
3066
|
+
const invocationDepth = document.instruments.reduce(
|
|
3067
|
+
(total, inst) => total + inst.actionOrder.length,
|
|
3068
|
+
0,
|
|
3069
|
+
);
|
|
3070
|
+
while (changedInvocations && invocationIterations < invocationDepth) {
|
|
2945
3071
|
changedInvocations = false;
|
|
2946
3072
|
invocationIterations++;
|
|
2947
3073
|
for (const inst of document.instruments) {
|
|
@@ -3158,6 +3284,17 @@ export function compile(
|
|
|
3158
3284
|
const eliminatedStates = new Map<string, Set<string>>();
|
|
3159
3285
|
// Remove branches excluded by immutable tunables before checking reference states.
|
|
3160
3286
|
for (const inst of document.instruments) {
|
|
3287
|
+
if (
|
|
3288
|
+
!inst.lifecycle.states.includes(inst.lifecycle.initial) ||
|
|
3289
|
+
Object.values(inst.lifecycle.transitions).some(
|
|
3290
|
+
(edge) =>
|
|
3291
|
+
edge.from.some((state) => !inst.lifecycle.states.includes(state)) ||
|
|
3292
|
+
(edge.to !== "preserve" &&
|
|
3293
|
+
!inst.lifecycle.states.includes(edge.to)),
|
|
3294
|
+
)
|
|
3295
|
+
)
|
|
3296
|
+
continue;
|
|
3297
|
+
let specialized = false;
|
|
3161
3298
|
for (const [key, action] of Object.entries(inst.actions)) {
|
|
3162
3299
|
const constant = (value: import("@hyperscale0/udl").UdlValue) => {
|
|
3163
3300
|
if ("literal" in value) return value.literal;
|
|
@@ -3179,8 +3316,10 @@ export function compile(
|
|
|
3179
3316
|
) {
|
|
3180
3317
|
delete inst.actions[key];
|
|
3181
3318
|
delete inst.lifecycle.transitions[key];
|
|
3319
|
+
specialized = true;
|
|
3182
3320
|
}
|
|
3183
3321
|
}
|
|
3322
|
+
if (!specialized) continue;
|
|
3184
3323
|
const reachable = new Set([inst.lifecycle.initial]);
|
|
3185
3324
|
for (let n = 0; n < inst.lifecycle.states.length; n++)
|
|
3186
3325
|
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
@@ -3220,6 +3359,16 @@ export function compile(
|
|
|
3220
3359
|
);
|
|
3221
3360
|
}
|
|
3222
3361
|
}
|
|
3362
|
+
for (const exposure of exposures)
|
|
3363
|
+
if (
|
|
3364
|
+
!document.instruments.find((inst) => inst.id === exposure.instrument)
|
|
3365
|
+
?.actions[exposure.action]
|
|
3366
|
+
)
|
|
3367
|
+
fail(
|
|
3368
|
+
exposure.entry,
|
|
3369
|
+
`action ${exposure.action} is excluded by these bindings`,
|
|
3370
|
+
"expose an action available with these tunables",
|
|
3371
|
+
);
|
|
3223
3372
|
const changed = new Set<string>();
|
|
3224
3373
|
for (const decl of program.decls)
|
|
3225
3374
|
if (decl.kind === "hide" || decl.kind === "expose") {
|