@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/dist/src/compile.js
CHANGED
|
@@ -1,38 +1,23 @@
|
|
|
1
|
+
import { CompileFailure, fail, failWithCode } from "./diagnostics.js";
|
|
1
2
|
import { hash as sha256 } from "fast-sha256";
|
|
2
3
|
import { buildUdlCostManifest } from "./cost.js";
|
|
3
|
-
import { validateUdl, resolveField, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, udlObjectFieldSchema, } from "@hyperscale0/udl";
|
|
4
|
+
import { validateUdl, resolveField, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, udlObjectFieldSchema, udlInstrumentSchema, } from "@hyperscale0/udl";
|
|
4
5
|
import { bindingDependencies, parameterDiagnostics, BindingContractError, } from "./binding-contract.js";
|
|
5
6
|
import { tunableBounds } from "./tunables.js";
|
|
6
7
|
import { parseProgram } from "./parse.js";
|
|
8
|
+
import { parseHeader } from "./header-source.js";
|
|
7
9
|
import { lineColAt, } from "./ast.js";
|
|
8
10
|
import { bundledStandardLibrary } from "./std-library.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
function fail(expr, message, fix) {
|
|
17
|
-
return failWithCode(expr, "HSX1001", message, fix);
|
|
18
|
-
}
|
|
19
|
-
function failWithCode(expr, code, message, fix) {
|
|
20
|
-
throw new CompileFailure({
|
|
21
|
-
code,
|
|
22
|
-
message,
|
|
23
|
-
fix,
|
|
24
|
-
span: expr.span,
|
|
25
|
-
...(expr.source ? { source: expr.source } : {}),
|
|
26
|
-
});
|
|
11
|
+
function fieldType(row) {
|
|
12
|
+
const type = row.value.kind === "default" ? row.value.type : row.value;
|
|
13
|
+
if (type.kind !== "call" || type.name !== "sensitive")
|
|
14
|
+
return { type, sensitive: false };
|
|
15
|
+
if (type.args.length !== 1)
|
|
16
|
+
fail(type, "sensitive needs one field type", "write sensitive(text)");
|
|
17
|
+
return { type: type.args[0], sensitive: true };
|
|
27
18
|
}
|
|
28
19
|
function lowerFieldShape(row, resolveExpr = (e) => e) {
|
|
29
|
-
|
|
30
|
-
let isSensitive = false;
|
|
31
|
-
if (rawValue.kind === "call" && rawValue.name === "sensitive") {
|
|
32
|
-
isSensitive = true;
|
|
33
|
-
rawValue = rawValue.args[0];
|
|
34
|
-
}
|
|
35
|
-
const t = rawValue;
|
|
20
|
+
const { type: t, sensitive: isSensitive } = fieldType(row);
|
|
36
21
|
if (t.kind === "block") {
|
|
37
22
|
const b = entries(t);
|
|
38
23
|
if (b.has("family") || b.has("target") || b.has("instrument")) {
|
|
@@ -83,7 +68,12 @@ function lowerFieldShape(row, resolveExpr = (e) => e) {
|
|
|
83
68
|
f.minimum = literal(resolveExpr(t.args[0]));
|
|
84
69
|
f.maximum = literal(resolveExpr(t.args[1]));
|
|
85
70
|
}
|
|
71
|
+
if (t.kind === "call" &&
|
|
72
|
+
!["enum", "text", "integer", "money", "list", "account"].includes(type))
|
|
73
|
+
fail(t, `unknown field constructor ${type}`, "use a field type without arguments");
|
|
86
74
|
if (type === "list" && t.kind === "call") {
|
|
75
|
+
if (t.args.length < 1 || t.args.length > 2)
|
|
76
|
+
fail(t, "list needs an item type and optional bound", "write list(text, 12)");
|
|
87
77
|
const item = t.args[0];
|
|
88
78
|
f.item = item.kind === "type" ? item.name : text(item);
|
|
89
79
|
if (item.kind === "type" && item.name === "ref") {
|
|
@@ -114,7 +104,7 @@ function lowerObjectField(row, resolveExpr = (e) => e) {
|
|
|
114
104
|
f.value =
|
|
115
105
|
f.type === "enum" && constant.kind === "name"
|
|
116
106
|
? constant.value
|
|
117
|
-
: literal(constant);
|
|
107
|
+
: literal(constant, String(f.type));
|
|
118
108
|
}
|
|
119
109
|
const result = udlObjectFieldSchema.safeParse(f);
|
|
120
110
|
if (!result.success)
|
|
@@ -141,7 +131,8 @@ const emptyBlock = {
|
|
|
141
131
|
};
|
|
142
132
|
const camel = (name) => name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
143
133
|
/** Clock and parent actors run without a caller, so they have no public name. */
|
|
144
|
-
const automatic = (actor) => actor === "clock" ||
|
|
134
|
+
const automatic = (actor) => actor === "clock" ||
|
|
135
|
+
(actor !== null && typeof actor === "object" && "parent" in actor);
|
|
145
136
|
const title = (name) => name[0].toUpperCase() + name.slice(1).replaceAll("_", " ");
|
|
146
137
|
function entries(block) {
|
|
147
138
|
const map = new Map();
|
|
@@ -188,7 +179,9 @@ function decimal(raw, scale, expr) {
|
|
|
188
179
|
return (BigInt(whole) * 10n ** BigInt(scale) +
|
|
189
180
|
BigInt(fraction.padEnd(scale, "0") || "0")).toString();
|
|
190
181
|
}
|
|
191
|
-
function literal(expr) {
|
|
182
|
+
function literal(expr, expectedType) {
|
|
183
|
+
if (expr.kind === "text" && expectedType === "duration")
|
|
184
|
+
return literal({ ...expr, kind: "duration" });
|
|
192
185
|
if (expr.kind === "money") {
|
|
193
186
|
const [raw, currency] = expr.value.split(" ");
|
|
194
187
|
if (currency !== "SAR")
|
|
@@ -211,8 +204,7 @@ function literal(expr) {
|
|
|
211
204
|
return n;
|
|
212
205
|
}
|
|
213
206
|
if (expr.kind === "duration" ||
|
|
214
|
-
(expr.kind === "name" && /^P(?:\d|T)/.test(expr.value))
|
|
215
|
-
(expr.kind === "text" && /^P(?:\d|T)/.test(expr.value))) {
|
|
207
|
+
(expr.kind === "name" && /^P(?:\d|T)/.test(expr.value))) {
|
|
216
208
|
const short = /^(\d+)(ms|s|m|h|d|w)$/.exec(expr.value);
|
|
217
209
|
const units = {
|
|
218
210
|
ms: 1,
|
|
@@ -222,7 +214,7 @@ function literal(expr) {
|
|
|
222
214
|
d: 86400000,
|
|
223
215
|
w: 604800000,
|
|
224
216
|
};
|
|
225
|
-
const iso = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(expr.value);
|
|
217
|
+
const iso = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(expr.value);
|
|
226
218
|
const n = short
|
|
227
219
|
? Number(short[1]) * units[short[2]]
|
|
228
220
|
: iso
|
|
@@ -239,9 +231,10 @@ function literal(expr) {
|
|
|
239
231
|
if (expr.kind === "date") {
|
|
240
232
|
const raw = expr.value.length === 10 ? expr.value + "T00:00:00Z" : expr.value;
|
|
241
233
|
const n = Date.parse(raw);
|
|
234
|
+
const day = Date.parse(raw.slice(0, 10) + "T00:00:00Z");
|
|
242
235
|
if (!Number.isFinite(n) ||
|
|
243
|
-
|
|
244
|
-
|
|
236
|
+
!Number.isFinite(day) ||
|
|
237
|
+
new Date(day).toISOString().slice(0, 10) !== raw.slice(0, 10))
|
|
245
238
|
fail(expr, "invalid date", "write a calendar date or a timestamp with an explicit offset");
|
|
246
239
|
return new Date(n).toISOString();
|
|
247
240
|
}
|
|
@@ -267,6 +260,16 @@ export function compile(source, options = {}) {
|
|
|
267
260
|
};
|
|
268
261
|
const program = parsed.program;
|
|
269
262
|
const bindingDiagnostics = [];
|
|
263
|
+
const adapterTarget = (binding) => {
|
|
264
|
+
const registry = options.adapterRegistry;
|
|
265
|
+
if (!registry || !Object.hasOwn(registry, binding))
|
|
266
|
+
return;
|
|
267
|
+
const target = registry[binding];
|
|
268
|
+
return target &&
|
|
269
|
+
Object.hasOwn(target.adapter.operationMap, target.operation)
|
|
270
|
+
? target
|
|
271
|
+
: undefined;
|
|
272
|
+
};
|
|
270
273
|
try {
|
|
271
274
|
if (program.header)
|
|
272
275
|
fail(program, "compile a program, not a header", 'start with program product_name "Title"');
|
|
@@ -274,7 +277,6 @@ export function compile(source, options = {}) {
|
|
|
274
277
|
fail(program, "this release supports SAR", "write currency SAR or omit currency");
|
|
275
278
|
const templates = new Map();
|
|
276
279
|
const declarationSources = new Map();
|
|
277
|
-
const declarationExportPaths = new Map();
|
|
278
280
|
const requirementOrigins = new Map();
|
|
279
281
|
const used = new Set();
|
|
280
282
|
for (const use of program.decls.filter((d) => d.kind === "use")) {
|
|
@@ -285,15 +287,29 @@ export function compile(source, options = {}) {
|
|
|
285
287
|
if (!content)
|
|
286
288
|
fail(use, `unknown header ${use.name}`, "choose a published header");
|
|
287
289
|
sources.set(use.name, content);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
let header;
|
|
291
|
+
try {
|
|
292
|
+
header = parseHeader(content, use.name);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (!(error instanceof CompileFailure))
|
|
296
|
+
throw error;
|
|
297
|
+
throw new CompileFailure({
|
|
298
|
+
...error.diagnostic,
|
|
299
|
+
related: [
|
|
300
|
+
{
|
|
301
|
+
source: "program",
|
|
302
|
+
span: use.span,
|
|
303
|
+
message: `Imported header ${use.name}`,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const registerTemplates = (parentDecl, prefix) => {
|
|
309
|
+
if (templates.has(prefix))
|
|
310
|
+
fail(parentDecl, `duplicate instrument ${prefix}`, "give each instrument a distinct name");
|
|
294
311
|
templates.set(prefix, parentDecl);
|
|
295
312
|
declarationSources.set(parentDecl, use.name);
|
|
296
|
-
declarationExportPaths.set(parentDecl, exportPath);
|
|
297
313
|
const recs = entries(asBlock(entries(parentDecl.body).get("records")));
|
|
298
314
|
for (const [recName, recBlock] of recs) {
|
|
299
315
|
const recDecl = {
|
|
@@ -303,12 +319,12 @@ export function compile(source, options = {}) {
|
|
|
303
319
|
body: asBlock(recBlock),
|
|
304
320
|
span: parentDecl.span,
|
|
305
321
|
};
|
|
306
|
-
registerTemplates(recDecl, `${prefix}.${recName}
|
|
322
|
+
registerTemplates(recDecl, `${prefix}.${recName}`);
|
|
307
323
|
}
|
|
308
324
|
};
|
|
309
|
-
for (const decl of header.
|
|
325
|
+
for (const decl of header.decls)
|
|
310
326
|
if (decl.kind === "instrument") {
|
|
311
|
-
registerTemplates(decl, `${use.name}.${decl.name}
|
|
327
|
+
registerTemplates(decl, `${use.name}.${decl.name}`);
|
|
312
328
|
}
|
|
313
329
|
}
|
|
314
330
|
for (const decl of program.decls)
|
|
@@ -324,12 +340,12 @@ export function compile(source, options = {}) {
|
|
|
324
340
|
product: program.name,
|
|
325
341
|
title: program.title,
|
|
326
342
|
currency: "SAR",
|
|
327
|
-
parties: {
|
|
343
|
+
parties: Object.assign(Object.create(null), {
|
|
328
344
|
programOperator: { kind: "business", role: "program_operator" },
|
|
329
345
|
programTax: { kind: "business", role: "tax_payable" },
|
|
330
346
|
programFines: { kind: "business", role: "fine_payable" },
|
|
331
347
|
programCosts: { kind: "business", role: "cost_recovery" },
|
|
332
|
-
},
|
|
348
|
+
}),
|
|
333
349
|
objects: [],
|
|
334
350
|
instruments: [],
|
|
335
351
|
};
|
|
@@ -374,6 +390,7 @@ export function compile(source, options = {}) {
|
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
const origins = [];
|
|
393
|
+
const exposures = [];
|
|
377
394
|
const refundBindings = new Map();
|
|
378
395
|
const resolveFamily = (rawPath, expr, required = true) => {
|
|
379
396
|
const parts = rawPath.split(".");
|
|
@@ -502,10 +519,7 @@ export function compile(source, options = {}) {
|
|
|
502
519
|
const sub = resolveChildExportPath(decl, targetId.slice(id.length + 1));
|
|
503
520
|
if (sub) {
|
|
504
521
|
const fullExport = `${familyDeclaration.exportPath}.${sub}`;
|
|
505
|
-
|
|
506
|
-
return resolveFamily(`${familyDeclaration.module}.${fullExport}`, { span: origin }, false);
|
|
507
|
-
}
|
|
508
|
-
catch { }
|
|
522
|
+
return resolveFamily(`${familyDeclaration.module}.${fullExport}`, { span: origin }, false);
|
|
509
523
|
}
|
|
510
524
|
}
|
|
511
525
|
for (const [asgnName, asgn] of assignments) {
|
|
@@ -517,19 +531,13 @@ export function compile(source, options = {}) {
|
|
|
517
531
|
if (!mod || mod === "program")
|
|
518
532
|
continue;
|
|
519
533
|
if (targetId === asgnName) {
|
|
520
|
-
|
|
521
|
-
return resolveFamily(asgn.target, asgn, false);
|
|
522
|
-
}
|
|
523
|
-
catch { }
|
|
534
|
+
return resolveFamily(asgn.target, asgn, false);
|
|
524
535
|
}
|
|
525
536
|
else {
|
|
526
537
|
const sub = resolveChildExportPath(tmpl, targetId.slice(asgnName.length + 1));
|
|
527
538
|
if (sub) {
|
|
528
539
|
const fullTarget = `${asgn.target}.${sub}`;
|
|
529
|
-
|
|
530
|
-
return resolveFamily(fullTarget, asgn, false);
|
|
531
|
-
}
|
|
532
|
-
catch { }
|
|
540
|
+
return resolveFamily(fullTarget, asgn, false);
|
|
533
541
|
}
|
|
534
542
|
}
|
|
535
543
|
}
|
|
@@ -539,6 +547,8 @@ export function compile(source, options = {}) {
|
|
|
539
547
|
const checkTargetFamily = (targetIds, expectedFamily, expr) => {
|
|
540
548
|
const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
|
|
541
549
|
for (const tid of ids) {
|
|
550
|
+
if (typeof tid !== "string")
|
|
551
|
+
fail(expr, "instrument target needs a name", "name a declared instrument or a list of instruments");
|
|
542
552
|
const fam = getInstrumentFamily(tid);
|
|
543
553
|
if (!fam ||
|
|
544
554
|
fam.module !== expectedFamily.module ||
|
|
@@ -668,8 +678,8 @@ export function compile(source, options = {}) {
|
|
|
668
678
|
? entry.value.value === "money"
|
|
669
679
|
: (entry.value.kind === "type" || entry.value.kind === "call") &&
|
|
670
680
|
entry.value.name === "money");
|
|
671
|
-
const target = candidates.length === 1 ? candidates[0].key : "
|
|
672
|
-
fail(suppliedValue, `\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its
|
|
681
|
+
const target = candidates.length === 1 ? candidates[0].key : "yourMoneyField";
|
|
682
|
+
fail(suppliedValue, `\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its actions require the object's \`${field}\` field.`, `Remove \`${key}\`. To use your object's money field, add \`rename { ${field}: ${target} }\`.`);
|
|
673
683
|
}
|
|
674
684
|
fail(suppliedValue, `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
|
|
675
685
|
}
|
|
@@ -736,11 +746,15 @@ export function compile(source, options = {}) {
|
|
|
736
746
|
(!binding ||
|
|
737
747
|
(binding.kind === "name" && binding.value === expr.value)))
|
|
738
748
|
return expr;
|
|
739
|
-
// A resolved party parameter
|
|
749
|
+
// A resolved party parameter or an own parameter bound to a value takes
|
|
750
|
+
// precedence over a same-named sibling attachment; `limits: limits`
|
|
751
|
+
// binds by identity and still names the sibling.
|
|
740
752
|
if (attachmentInfo && !resolvedParties.has(expr.value)) {
|
|
741
753
|
const [local, ...tail] = expr.value.split(".");
|
|
754
|
+
const own = environment.get(local);
|
|
755
|
+
const ownValue = own !== undefined && !(own.kind === "name" && own.value === local);
|
|
742
756
|
const target = `${attachmentInfo.subjectKindId}_${local}`;
|
|
743
|
-
if (attachmentSubjects.has(target))
|
|
757
|
+
if (!ownValue && attachmentSubjects.has(target))
|
|
744
758
|
return { ...expr, value: [target, ...tail].join("_") };
|
|
745
759
|
}
|
|
746
760
|
if (expr.value.startsWith("party.")) {
|
|
@@ -780,9 +794,13 @@ export function compile(source, options = {}) {
|
|
|
780
794
|
continue;
|
|
781
795
|
const t = param.value.kind === "default" ? param.value.type : param.value;
|
|
782
796
|
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
783
|
-
|
|
797
|
+
let v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
784
798
|
? actual
|
|
785
799
|
: resolve(actual, new Set(), !!attachmentInfo && type === "party");
|
|
800
|
+
if (type === "duration" &&
|
|
801
|
+
(v.kind === "text" || v.kind === "name") &&
|
|
802
|
+
/^P/.test(v.value))
|
|
803
|
+
v = { ...v, kind: "duration" };
|
|
786
804
|
environment.set(param.key, v);
|
|
787
805
|
if (type === "enum" && t.kind === "call") {
|
|
788
806
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -821,16 +839,33 @@ export function compile(source, options = {}) {
|
|
|
821
839
|
for (const value of values) {
|
|
822
840
|
if (value.kind !== "name")
|
|
823
841
|
fail(value, "reference needs an object name", "name a declared object");
|
|
824
|
-
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
842
|
+
let targetType = objects.get(value.value)?.name;
|
|
843
|
+
const candidates = [
|
|
844
|
+
...[...assignments.values()].map((assignment) => [assignment.name, assignment.target]),
|
|
845
|
+
...program.decls
|
|
846
|
+
.filter((decl) => decl.kind === "instrument")
|
|
847
|
+
.map((decl) => [decl.name, decl.name]),
|
|
848
|
+
];
|
|
849
|
+
for (const [instance, target] of candidates) {
|
|
850
|
+
if (value.value === instance) {
|
|
851
|
+
targetType = target;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
if (!value.value.startsWith(`${instance}.`) &&
|
|
855
|
+
!value.value.startsWith(`${instance}_`))
|
|
856
|
+
continue;
|
|
857
|
+
const template = templates.get(target);
|
|
858
|
+
if (!template)
|
|
859
|
+
continue;
|
|
860
|
+
const suffix = value.value
|
|
861
|
+
.slice(instance.length + 1)
|
|
862
|
+
.replaceAll(".", "_");
|
|
863
|
+
const child = resolveChildExportPath(template, suffix);
|
|
864
|
+
if (child)
|
|
865
|
+
targetType = `${target}.${child}`;
|
|
866
|
+
}
|
|
867
|
+
if (!targetType ||
|
|
868
|
+
(t.kind === "type" && t.target && targetType !== t.target))
|
|
834
869
|
fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
|
|
835
870
|
if (seen.has(value.value))
|
|
836
871
|
fail(value, "duplicate reference", "list each object once");
|
|
@@ -854,14 +889,20 @@ export function compile(source, options = {}) {
|
|
|
854
889
|
type === "date" ||
|
|
855
890
|
type === "duration" ||
|
|
856
891
|
type === "integer" ||
|
|
857
|
-
type === "text"
|
|
892
|
+
type === "text" ||
|
|
893
|
+
type === "boolean") {
|
|
894
|
+
const bounds = tunableBounds(t);
|
|
858
895
|
if (v.kind === "name" && v.value === "runtime")
|
|
859
896
|
continue;
|
|
860
|
-
const expected = type === "integer"
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
897
|
+
const expected = type === "integer"
|
|
898
|
+
? "number"
|
|
899
|
+
: type === "boolean"
|
|
900
|
+
? "name"
|
|
901
|
+
: type;
|
|
902
|
+
if (v.kind !== expected ||
|
|
903
|
+
(type === "boolean" &&
|
|
904
|
+
v.kind === "name" &&
|
|
905
|
+
!["true", "false"].includes(v.value))) {
|
|
865
906
|
if (type === "money" && v.kind === "name" && attachmentInfo) {
|
|
866
907
|
const target = v.value.replace(/^subject\./, "");
|
|
867
908
|
const fallback = param.value.kind === "default"
|
|
@@ -881,7 +922,6 @@ export function compile(source, options = {}) {
|
|
|
881
922
|
}
|
|
882
923
|
try {
|
|
883
924
|
const value = literal(v);
|
|
884
|
-
const bounds = tunableBounds(t);
|
|
885
925
|
if (bounds &&
|
|
886
926
|
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
887
927
|
BigInt(String(value)) > BigInt(bounds.maximum)))
|
|
@@ -893,6 +933,9 @@ export function compile(source, options = {}) {
|
|
|
893
933
|
throw error;
|
|
894
934
|
}
|
|
895
935
|
}
|
|
936
|
+
else {
|
|
937
|
+
fail(t, `unknown tunable type ${type}`, "use a declared HSX tunable type");
|
|
938
|
+
}
|
|
896
939
|
}
|
|
897
940
|
catch (error) {
|
|
898
941
|
if (!(error instanceof CompileFailure))
|
|
@@ -931,6 +974,12 @@ export function compile(source, options = {}) {
|
|
|
931
974
|
const left = environment.get(constraint.key), right = environment.get(text(rule.args[0]));
|
|
932
975
|
if (!left || !right)
|
|
933
976
|
fail(rule, "constraint needs two declared tunables", "name the compared tunables");
|
|
977
|
+
const numericKind = (expr) => expr.kind === "name" && /^P/.test(expr.value)
|
|
978
|
+
? "duration"
|
|
979
|
+
: expr.kind;
|
|
980
|
+
if (!["money", "number", "percent", "duration"].includes(numericKind(left)) ||
|
|
981
|
+
numericKind(left) !== numericKind(right))
|
|
982
|
+
fail(rule, "constraint needs numeric tunables of the same type", "compare two amounts, integers, percentages or durations");
|
|
934
983
|
const a = BigInt(String(literal(left))), b = BigInt(String(literal(right)));
|
|
935
984
|
if (!(rule.name === "less_than"
|
|
936
985
|
? a < b
|
|
@@ -1009,7 +1058,7 @@ export function compile(source, options = {}) {
|
|
|
1009
1058
|
? se.value.items.map(text)
|
|
1010
1059
|
: [text(se.value)];
|
|
1011
1060
|
return names.some((n) => {
|
|
1012
|
-
const reg =
|
|
1061
|
+
const reg = adapterTarget(n);
|
|
1013
1062
|
if (!reg)
|
|
1014
1063
|
return false;
|
|
1015
1064
|
const op = reg.adapter.operationMap[reg.operation];
|
|
@@ -1119,7 +1168,7 @@ export function compile(source, options = {}) {
|
|
|
1119
1168
|
const lowerFields = (block) => {
|
|
1120
1169
|
const result = [];
|
|
1121
1170
|
for (const row of block.entries) {
|
|
1122
|
-
const
|
|
1171
|
+
const { type: t } = fieldType(row);
|
|
1123
1172
|
const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
1124
1173
|
const f = lowerFieldShape(row, resolve);
|
|
1125
1174
|
const type = f.type;
|
|
@@ -1317,15 +1366,15 @@ export function compile(source, options = {}) {
|
|
|
1317
1366
|
values: [val(constant)],
|
|
1318
1367
|
});
|
|
1319
1368
|
else
|
|
1320
|
-
f.value = literal(constant);
|
|
1369
|
+
f.value = literal(constant, String(type));
|
|
1321
1370
|
}
|
|
1322
1371
|
result.push(f);
|
|
1323
1372
|
}
|
|
1324
1373
|
return result;
|
|
1325
1374
|
};
|
|
1326
1375
|
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
1327
|
-
const lifecycle = data(body.get("lifecycle")
|
|
1328
|
-
lifecycle.transitions =
|
|
1376
|
+
const lifecycle = data(asBlock(body.get("lifecycle")));
|
|
1377
|
+
lifecycle.transitions = Object.create(null);
|
|
1329
1378
|
const inst = {
|
|
1330
1379
|
id,
|
|
1331
1380
|
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
@@ -1336,7 +1385,7 @@ export function compile(source, options = {}) {
|
|
|
1336
1385
|
fields,
|
|
1337
1386
|
calculate: calculations,
|
|
1338
1387
|
lifecycle,
|
|
1339
|
-
actions:
|
|
1388
|
+
actions: Object.create(null),
|
|
1340
1389
|
actionOrder: [],
|
|
1341
1390
|
};
|
|
1342
1391
|
for (const row of decl.body.entries.filter((e) => e.key.startsWith("action "))) {
|
|
@@ -1409,12 +1458,8 @@ export function compile(source, options = {}) {
|
|
|
1409
1458
|
if (!adapterExpr)
|
|
1410
1459
|
fail(boundary, "boundary needs an adapter", "name a bound ADL adapter");
|
|
1411
1460
|
const binding = text(resolve(adapterExpr));
|
|
1412
|
-
const target =
|
|
1413
|
-
|
|
1414
|
-
? options.adapterRegistry[binding]
|
|
1415
|
-
: undefined;
|
|
1416
|
-
if (!target ||
|
|
1417
|
-
!Object.hasOwn(target.adapter.operationMap, target.operation))
|
|
1461
|
+
const target = adapterTarget(binding);
|
|
1462
|
+
if (!target)
|
|
1418
1463
|
fail(boundary, `unknown boundary adapter ${binding}`, "bind the named ADL adapter before compilation");
|
|
1419
1464
|
boundaryBindings.add(binding);
|
|
1420
1465
|
}
|
|
@@ -1458,7 +1503,7 @@ export function compile(source, options = {}) {
|
|
|
1458
1503
|
? entry.value.items.map(adapterName)
|
|
1459
1504
|
: [adapterName(entry.value)];
|
|
1460
1505
|
for (const bindingName of bindingNames) {
|
|
1461
|
-
const target =
|
|
1506
|
+
const target = adapterTarget(bindingName);
|
|
1462
1507
|
if (target) {
|
|
1463
1508
|
const { adapter, operation } = target;
|
|
1464
1509
|
const opBinding = adapter.operationMap[operation];
|
|
@@ -1840,6 +1885,8 @@ export function compile(source, options = {}) {
|
|
|
1840
1885
|
}
|
|
1841
1886
|
if (attachmentInfo && !attachmentInfo.child) {
|
|
1842
1887
|
if (attachmentInfo.exposed.has(name)) {
|
|
1888
|
+
if (automatic(a.actor))
|
|
1889
|
+
fail(row, `${name} runs on the clock or its parent, not a caller`, "expose only caller actions");
|
|
1843
1890
|
a.publicAction = attachmentInfo.exposed.get(name);
|
|
1844
1891
|
}
|
|
1845
1892
|
else {
|
|
@@ -1848,28 +1895,37 @@ export function compile(source, options = {}) {
|
|
|
1848
1895
|
}
|
|
1849
1896
|
if (automatic(a.actor))
|
|
1850
1897
|
delete a.publicAction;
|
|
1851
|
-
const checkSubjectPaths = (obj,
|
|
1898
|
+
const checkSubjectPaths = (obj, key = "") => {
|
|
1852
1899
|
if (typeof obj === "string") {
|
|
1853
|
-
if (
|
|
1900
|
+
if ([
|
|
1901
|
+
"field",
|
|
1902
|
+
"fields",
|
|
1903
|
+
"reference",
|
|
1904
|
+
"anchor",
|
|
1905
|
+
"subject",
|
|
1906
|
+
"at",
|
|
1907
|
+
"instruction",
|
|
1908
|
+
].includes(key) &&
|
|
1909
|
+
obj.startsWith("subject.")) {
|
|
1854
1910
|
const subField = obj.split(".")[1];
|
|
1855
1911
|
const req = a.subject?.requirements.find((r) => r.field.name === subField);
|
|
1856
1912
|
if (!req) {
|
|
1857
|
-
failWithCode(
|
|
1913
|
+
failWithCode(row, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${name}`, `declare ${subField} in subject { ... }`);
|
|
1858
1914
|
}
|
|
1859
1915
|
}
|
|
1860
1916
|
}
|
|
1861
1917
|
else if (Array.isArray(obj)) {
|
|
1862
1918
|
for (const item of obj)
|
|
1863
|
-
checkSubjectPaths(item,
|
|
1919
|
+
checkSubjectPaths(item, key);
|
|
1864
1920
|
}
|
|
1865
1921
|
else if (obj !== null && typeof obj === "object") {
|
|
1866
|
-
for (const val of Object.
|
|
1867
|
-
checkSubjectPaths(val,
|
|
1922
|
+
for (const [key, val] of Object.entries(obj))
|
|
1923
|
+
checkSubjectPaths(val, key);
|
|
1868
1924
|
}
|
|
1869
1925
|
};
|
|
1870
|
-
checkSubjectPaths(a.requires
|
|
1871
|
-
checkSubjectPaths(a.set
|
|
1872
|
-
checkSubjectPaths(a.invoke
|
|
1926
|
+
checkSubjectPaths(a.requires);
|
|
1927
|
+
checkSubjectPaths(a.set);
|
|
1928
|
+
checkSubjectPaths(a.invoke);
|
|
1873
1929
|
currentAction = undefined;
|
|
1874
1930
|
currentActionName = undefined;
|
|
1875
1931
|
for (const requirement of a.subject?.requirements ?? []) {
|
|
@@ -1887,6 +1943,17 @@ export function compile(source, options = {}) {
|
|
|
1887
1943
|
for (const key of ["invariants", "reports", "revisioned"])
|
|
1888
1944
|
if (body.has(key))
|
|
1889
1945
|
inst[key] = data(body.get(key));
|
|
1946
|
+
const shape = udlInstrumentSchema.safeParse(inst);
|
|
1947
|
+
if (!shape.success) {
|
|
1948
|
+
const issue = shape.error.issues[0];
|
|
1949
|
+
const actionName = issue.path[0] === "actions" ? String(issue.path[1]) : undefined;
|
|
1950
|
+
const node = decl.body.entries.find((entry) => entry.key ===
|
|
1951
|
+
(actionName ? `action ${actionName}` : String(issue.path[0]))) ?? decl;
|
|
1952
|
+
failWithCode({
|
|
1953
|
+
span: node.span,
|
|
1954
|
+
source: declarationSources.get(decl) ?? "program",
|
|
1955
|
+
}, "UDL1003", `${id}.${issue.path.join(".")}: ${issue.message}`, "use the UDL typed clause shape");
|
|
1956
|
+
}
|
|
1890
1957
|
origins.push({
|
|
1891
1958
|
path: `$.instruments[${document.instruments.length}]`,
|
|
1892
1959
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
@@ -1947,7 +2014,6 @@ export function compile(source, options = {}) {
|
|
|
1947
2014
|
fail(decl, `unknown object clause ${key}`, "use fields, columns, or attach");
|
|
1948
2015
|
}
|
|
1949
2016
|
}
|
|
1950
|
-
// 1. Lower authored fields
|
|
1951
2017
|
const authoredFields = [];
|
|
1952
2018
|
const authoredNames = [];
|
|
1953
2019
|
const fieldsBlock = asBlock(body.get("fields"));
|
|
@@ -1959,7 +2025,6 @@ export function compile(source, options = {}) {
|
|
|
1959
2025
|
authoredFields.push(fieldDef);
|
|
1960
2026
|
authoredNames.push(row.key);
|
|
1961
2027
|
}
|
|
1962
|
-
// 2. Process attachments
|
|
1963
2028
|
const attachments = [];
|
|
1964
2029
|
for (const entry of decl.body.entries) {
|
|
1965
2030
|
if (!entry.key.startsWith("attach "))
|
|
@@ -1983,6 +2048,8 @@ export function compile(source, options = {}) {
|
|
|
1983
2048
|
for (const row of attachmentBlock.entries) {
|
|
1984
2049
|
if (row.key === "rename") {
|
|
1985
2050
|
for (const r of asBlock(row.value).entries) {
|
|
2051
|
+
if (renames.has(r.key))
|
|
2052
|
+
fail(r, `duplicate rename ${r.key}`, "rename each subject field once");
|
|
1986
2053
|
renames.set(r.key, text(r.value));
|
|
1987
2054
|
renameEntries.set(r.key, r);
|
|
1988
2055
|
}
|
|
@@ -1991,7 +2058,15 @@ export function compile(source, options = {}) {
|
|
|
1991
2058
|
if (row.value.kind === "call") {
|
|
1992
2059
|
const actionName = row.value.name;
|
|
1993
2060
|
const publicName = text(row.value.args[0]);
|
|
2061
|
+
if (exposed.has(actionName) ||
|
|
2062
|
+
!template.body.entries.some((entry) => entry.key === `action ${actionName}`))
|
|
2063
|
+
fail(row, `unknown or repeated action ${actionName}`, "expose one declared action once");
|
|
1994
2064
|
exposed.set(actionName, publicName);
|
|
2065
|
+
exposures.push({
|
|
2066
|
+
instrument: instId,
|
|
2067
|
+
action: actionName,
|
|
2068
|
+
entry: row,
|
|
2069
|
+
});
|
|
1995
2070
|
}
|
|
1996
2071
|
}
|
|
1997
2072
|
else {
|
|
@@ -2084,7 +2159,6 @@ export function compile(source, options = {}) {
|
|
|
2084
2159
|
}
|
|
2085
2160
|
}
|
|
2086
2161
|
}
|
|
2087
|
-
// 4. Validate columns
|
|
2088
2162
|
const columnsExpr = body.get("columns");
|
|
2089
2163
|
let columns = [];
|
|
2090
2164
|
if (columnsExpr) {
|
|
@@ -2130,7 +2204,8 @@ export function compile(source, options = {}) {
|
|
|
2130
2204
|
// Propagate invoked requirements with the conditions on each invocation path.
|
|
2131
2205
|
let changedInvocations = true;
|
|
2132
2206
|
let invocationIterations = 0;
|
|
2133
|
-
|
|
2207
|
+
const invocationDepth = document.instruments.reduce((total, inst) => total + inst.actionOrder.length, 0);
|
|
2208
|
+
while (changedInvocations && invocationIterations < invocationDepth) {
|
|
2134
2209
|
changedInvocations = false;
|
|
2135
2210
|
invocationIterations++;
|
|
2136
2211
|
for (const inst of document.instruments) {
|
|
@@ -2290,6 +2365,12 @@ export function compile(source, options = {}) {
|
|
|
2290
2365
|
const eliminatedStates = new Map();
|
|
2291
2366
|
// Remove branches excluded by immutable tunables before checking reference states.
|
|
2292
2367
|
for (const inst of document.instruments) {
|
|
2368
|
+
if (!inst.lifecycle.states.includes(inst.lifecycle.initial) ||
|
|
2369
|
+
Object.values(inst.lifecycle.transitions).some((edge) => edge.from.some((state) => !inst.lifecycle.states.includes(state)) ||
|
|
2370
|
+
(edge.to !== "preserve" &&
|
|
2371
|
+
!inst.lifecycle.states.includes(edge.to))))
|
|
2372
|
+
continue;
|
|
2373
|
+
let specialized = false;
|
|
2293
2374
|
for (const [key, action] of Object.entries(inst.actions)) {
|
|
2294
2375
|
const constant = (value) => {
|
|
2295
2376
|
if ("literal" in value)
|
|
@@ -2311,8 +2392,11 @@ export function compile(source, options = {}) {
|
|
|
2311
2392
|
})) {
|
|
2312
2393
|
delete inst.actions[key];
|
|
2313
2394
|
delete inst.lifecycle.transitions[key];
|
|
2395
|
+
specialized = true;
|
|
2314
2396
|
}
|
|
2315
2397
|
}
|
|
2398
|
+
if (!specialized)
|
|
2399
|
+
continue;
|
|
2316
2400
|
const reachable = new Set([inst.lifecycle.initial]);
|
|
2317
2401
|
for (let n = 0; n < inst.lifecycle.states.length; n++)
|
|
2318
2402
|
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
@@ -2342,6 +2426,10 @@ export function compile(source, options = {}) {
|
|
|
2342
2426
|
: field.target).some((id) => eliminatedStates.get(id)?.has(state)));
|
|
2343
2427
|
}
|
|
2344
2428
|
}
|
|
2429
|
+
for (const exposure of exposures)
|
|
2430
|
+
if (!document.instruments.find((inst) => inst.id === exposure.instrument)
|
|
2431
|
+
?.actions[exposure.action])
|
|
2432
|
+
fail(exposure.entry, `action ${exposure.action} is excluded by these bindings`, "expose an action available with these tunables");
|
|
2345
2433
|
const changed = new Set();
|
|
2346
2434
|
for (const decl of program.decls)
|
|
2347
2435
|
if (decl.kind === "hide" || decl.kind === "expose") {
|
|
@@ -2359,6 +2447,15 @@ export function compile(source, options = {}) {
|
|
|
2359
2447
|
else
|
|
2360
2448
|
action.publicAction = decl.name;
|
|
2361
2449
|
}
|
|
2450
|
+
for (const decl of program.decls) {
|
|
2451
|
+
if (decl.kind !== "expose")
|
|
2452
|
+
continue;
|
|
2453
|
+
const parts = decl.target.split(".");
|
|
2454
|
+
parts.pop();
|
|
2455
|
+
const instrument = document.instruments.find((item) => item.id === parts.join("_"));
|
|
2456
|
+
if (Object.values(instrument.actions).filter((action) => action.publicAction === decl.name).length > 1)
|
|
2457
|
+
fail(decl, `public action ${decl.name} is used more than once on ${parts.join(".")}`, "give each exposed action a distinct public name");
|
|
2458
|
+
}
|
|
2362
2459
|
const usedParties = new Set();
|
|
2363
2460
|
const collectParties = (value) => {
|
|
2364
2461
|
if (typeof value === "string") {
|