@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/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
|
}
|
|
@@ -780,9 +790,13 @@ export function compile(source, options = {}) {
|
|
|
780
790
|
continue;
|
|
781
791
|
const t = param.value.kind === "default" ? param.value.type : param.value;
|
|
782
792
|
const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
783
|
-
|
|
793
|
+
let v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
784
794
|
? actual
|
|
785
795
|
: resolve(actual, new Set(), !!attachmentInfo && type === "party");
|
|
796
|
+
if (type === "duration" &&
|
|
797
|
+
(v.kind === "text" || v.kind === "name") &&
|
|
798
|
+
/^P/.test(v.value))
|
|
799
|
+
v = { ...v, kind: "duration" };
|
|
786
800
|
environment.set(param.key, v);
|
|
787
801
|
if (type === "enum" && t.kind === "call") {
|
|
788
802
|
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
@@ -821,16 +835,33 @@ export function compile(source, options = {}) {
|
|
|
821
835
|
for (const value of values) {
|
|
822
836
|
if (value.kind !== "name")
|
|
823
837
|
fail(value, "reference needs an object name", "name a declared object");
|
|
824
|
-
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
838
|
+
let targetType = objects.get(value.value)?.name;
|
|
839
|
+
const candidates = [
|
|
840
|
+
...[...assignments.values()].map((assignment) => [assignment.name, assignment.target]),
|
|
841
|
+
...program.decls
|
|
842
|
+
.filter((decl) => decl.kind === "instrument")
|
|
843
|
+
.map((decl) => [decl.name, decl.name]),
|
|
844
|
+
];
|
|
845
|
+
for (const [instance, target] of candidates) {
|
|
846
|
+
if (value.value === instance) {
|
|
847
|
+
targetType = target;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
if (!value.value.startsWith(`${instance}.`) &&
|
|
851
|
+
!value.value.startsWith(`${instance}_`))
|
|
852
|
+
continue;
|
|
853
|
+
const template = templates.get(target);
|
|
854
|
+
if (!template)
|
|
855
|
+
continue;
|
|
856
|
+
const suffix = value.value
|
|
857
|
+
.slice(instance.length + 1)
|
|
858
|
+
.replaceAll(".", "_");
|
|
859
|
+
const child = resolveChildExportPath(template, suffix);
|
|
860
|
+
if (child)
|
|
861
|
+
targetType = `${target}.${child}`;
|
|
862
|
+
}
|
|
863
|
+
if (!targetType ||
|
|
864
|
+
(t.kind === "type" && t.target && targetType !== t.target))
|
|
834
865
|
fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
|
|
835
866
|
if (seen.has(value.value))
|
|
836
867
|
fail(value, "duplicate reference", "list each object once");
|
|
@@ -854,14 +885,20 @@ export function compile(source, options = {}) {
|
|
|
854
885
|
type === "date" ||
|
|
855
886
|
type === "duration" ||
|
|
856
887
|
type === "integer" ||
|
|
857
|
-
type === "text"
|
|
888
|
+
type === "text" ||
|
|
889
|
+
type === "boolean") {
|
|
890
|
+
const bounds = tunableBounds(t);
|
|
858
891
|
if (v.kind === "name" && v.value === "runtime")
|
|
859
892
|
continue;
|
|
860
|
-
const expected = type === "integer"
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
893
|
+
const expected = type === "integer"
|
|
894
|
+
? "number"
|
|
895
|
+
: type === "boolean"
|
|
896
|
+
? "name"
|
|
897
|
+
: type;
|
|
898
|
+
if (v.kind !== expected ||
|
|
899
|
+
(type === "boolean" &&
|
|
900
|
+
v.kind === "name" &&
|
|
901
|
+
!["true", "false"].includes(v.value))) {
|
|
865
902
|
if (type === "money" && v.kind === "name" && attachmentInfo) {
|
|
866
903
|
const target = v.value.replace(/^subject\./, "");
|
|
867
904
|
const fallback = param.value.kind === "default"
|
|
@@ -881,7 +918,6 @@ export function compile(source, options = {}) {
|
|
|
881
918
|
}
|
|
882
919
|
try {
|
|
883
920
|
const value = literal(v);
|
|
884
|
-
const bounds = tunableBounds(t);
|
|
885
921
|
if (bounds &&
|
|
886
922
|
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
887
923
|
BigInt(String(value)) > BigInt(bounds.maximum)))
|
|
@@ -893,6 +929,9 @@ export function compile(source, options = {}) {
|
|
|
893
929
|
throw error;
|
|
894
930
|
}
|
|
895
931
|
}
|
|
932
|
+
else {
|
|
933
|
+
fail(t, `unknown tunable type ${type}`, "use a declared HSX tunable type");
|
|
934
|
+
}
|
|
896
935
|
}
|
|
897
936
|
catch (error) {
|
|
898
937
|
if (!(error instanceof CompileFailure))
|
|
@@ -931,6 +970,12 @@ export function compile(source, options = {}) {
|
|
|
931
970
|
const left = environment.get(constraint.key), right = environment.get(text(rule.args[0]));
|
|
932
971
|
if (!left || !right)
|
|
933
972
|
fail(rule, "constraint needs two declared tunables", "name the compared tunables");
|
|
973
|
+
const numericKind = (expr) => expr.kind === "name" && /^P/.test(expr.value)
|
|
974
|
+
? "duration"
|
|
975
|
+
: expr.kind;
|
|
976
|
+
if (!["money", "number", "percent", "duration"].includes(numericKind(left)) ||
|
|
977
|
+
numericKind(left) !== numericKind(right))
|
|
978
|
+
fail(rule, "constraint needs numeric tunables of the same type", "compare two amounts, integers, percentages or durations");
|
|
934
979
|
const a = BigInt(String(literal(left))), b = BigInt(String(literal(right)));
|
|
935
980
|
if (!(rule.name === "less_than"
|
|
936
981
|
? a < b
|
|
@@ -1009,7 +1054,7 @@ export function compile(source, options = {}) {
|
|
|
1009
1054
|
? se.value.items.map(text)
|
|
1010
1055
|
: [text(se.value)];
|
|
1011
1056
|
return names.some((n) => {
|
|
1012
|
-
const reg =
|
|
1057
|
+
const reg = adapterTarget(n);
|
|
1013
1058
|
if (!reg)
|
|
1014
1059
|
return false;
|
|
1015
1060
|
const op = reg.adapter.operationMap[reg.operation];
|
|
@@ -1119,7 +1164,7 @@ export function compile(source, options = {}) {
|
|
|
1119
1164
|
const lowerFields = (block) => {
|
|
1120
1165
|
const result = [];
|
|
1121
1166
|
for (const row of block.entries) {
|
|
1122
|
-
const
|
|
1167
|
+
const { type: t } = fieldType(row);
|
|
1123
1168
|
const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
|
|
1124
1169
|
const f = lowerFieldShape(row, resolve);
|
|
1125
1170
|
const type = f.type;
|
|
@@ -1203,7 +1248,14 @@ export function compile(source, options = {}) {
|
|
|
1203
1248
|
if (t.kind === "call") {
|
|
1204
1249
|
if (t.args.length < 1 || t.args.length > 4)
|
|
1205
1250
|
fail(t, "account needs an owner, optional book, mode and key", "write account(buyer, claim, contra)");
|
|
1206
|
-
|
|
1251
|
+
const owner = t.args[0];
|
|
1252
|
+
if (owner.kind === "call" && owner.name === "adapter") {
|
|
1253
|
+
if (owner.args.length !== 1)
|
|
1254
|
+
fail(owner, "adapter needs one binding", 'write account(adapter(binding), cash, "premium")');
|
|
1255
|
+
f.owner = { adapter: text(resolve(owner.args[0])) };
|
|
1256
|
+
}
|
|
1257
|
+
else
|
|
1258
|
+
f.owner = text(resolve(owner));
|
|
1207
1259
|
f.book = t.args[1] ? text(t.args[1]) : "cash";
|
|
1208
1260
|
if (t.args[2]) {
|
|
1209
1261
|
const mode = text(t.args[2]);
|
|
@@ -1310,15 +1362,15 @@ export function compile(source, options = {}) {
|
|
|
1310
1362
|
values: [val(constant)],
|
|
1311
1363
|
});
|
|
1312
1364
|
else
|
|
1313
|
-
f.value = literal(constant);
|
|
1365
|
+
f.value = literal(constant, String(type));
|
|
1314
1366
|
}
|
|
1315
1367
|
result.push(f);
|
|
1316
1368
|
}
|
|
1317
1369
|
return result;
|
|
1318
1370
|
};
|
|
1319
1371
|
fields.push(...lowerFields(asBlock(body.get("fields"))));
|
|
1320
|
-
const lifecycle = data(body.get("lifecycle")
|
|
1321
|
-
lifecycle.transitions =
|
|
1372
|
+
const lifecycle = data(asBlock(body.get("lifecycle")));
|
|
1373
|
+
lifecycle.transitions = Object.create(null);
|
|
1322
1374
|
const inst = {
|
|
1323
1375
|
id,
|
|
1324
1376
|
...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
|
|
@@ -1329,7 +1381,7 @@ export function compile(source, options = {}) {
|
|
|
1329
1381
|
fields,
|
|
1330
1382
|
calculate: calculations,
|
|
1331
1383
|
lifecycle,
|
|
1332
|
-
actions:
|
|
1384
|
+
actions: Object.create(null),
|
|
1333
1385
|
actionOrder: [],
|
|
1334
1386
|
};
|
|
1335
1387
|
for (const row of decl.body.entries.filter((e) => e.key.startsWith("action "))) {
|
|
@@ -1402,12 +1454,8 @@ export function compile(source, options = {}) {
|
|
|
1402
1454
|
if (!adapterExpr)
|
|
1403
1455
|
fail(boundary, "boundary needs an adapter", "name a bound ADL adapter");
|
|
1404
1456
|
const binding = text(resolve(adapterExpr));
|
|
1405
|
-
const target =
|
|
1406
|
-
|
|
1407
|
-
? options.adapterRegistry[binding]
|
|
1408
|
-
: undefined;
|
|
1409
|
-
if (!target ||
|
|
1410
|
-
!Object.hasOwn(target.adapter.operationMap, target.operation))
|
|
1457
|
+
const target = adapterTarget(binding);
|
|
1458
|
+
if (!target)
|
|
1411
1459
|
fail(boundary, `unknown boundary adapter ${binding}`, "bind the named ADL adapter before compilation");
|
|
1412
1460
|
boundaryBindings.add(binding);
|
|
1413
1461
|
}
|
|
@@ -1451,7 +1499,7 @@ export function compile(source, options = {}) {
|
|
|
1451
1499
|
? entry.value.items.map(adapterName)
|
|
1452
1500
|
: [adapterName(entry.value)];
|
|
1453
1501
|
for (const bindingName of bindingNames) {
|
|
1454
|
-
const target =
|
|
1502
|
+
const target = adapterTarget(bindingName);
|
|
1455
1503
|
if (target) {
|
|
1456
1504
|
const { adapter, operation } = target;
|
|
1457
1505
|
const opBinding = adapter.operationMap[operation];
|
|
@@ -1833,6 +1881,8 @@ export function compile(source, options = {}) {
|
|
|
1833
1881
|
}
|
|
1834
1882
|
if (attachmentInfo && !attachmentInfo.child) {
|
|
1835
1883
|
if (attachmentInfo.exposed.has(name)) {
|
|
1884
|
+
if (automatic(a.actor))
|
|
1885
|
+
fail(row, `${name} runs on the clock or its parent, not a caller`, "expose only caller actions");
|
|
1836
1886
|
a.publicAction = attachmentInfo.exposed.get(name);
|
|
1837
1887
|
}
|
|
1838
1888
|
else {
|
|
@@ -1841,9 +1891,18 @@ export function compile(source, options = {}) {
|
|
|
1841
1891
|
}
|
|
1842
1892
|
if (automatic(a.actor))
|
|
1843
1893
|
delete a.publicAction;
|
|
1844
|
-
const checkSubjectPaths = (obj, span) => {
|
|
1894
|
+
const checkSubjectPaths = (obj, span, key = "") => {
|
|
1845
1895
|
if (typeof obj === "string") {
|
|
1846
|
-
if (
|
|
1896
|
+
if ([
|
|
1897
|
+
"field",
|
|
1898
|
+
"fields",
|
|
1899
|
+
"reference",
|
|
1900
|
+
"anchor",
|
|
1901
|
+
"subject",
|
|
1902
|
+
"at",
|
|
1903
|
+
"instruction",
|
|
1904
|
+
].includes(key) &&
|
|
1905
|
+
obj.startsWith("subject.")) {
|
|
1847
1906
|
const subField = obj.split(".")[1];
|
|
1848
1907
|
const req = a.subject?.requirements.find((r) => r.field.name === subField);
|
|
1849
1908
|
if (!req) {
|
|
@@ -1853,11 +1912,11 @@ export function compile(source, options = {}) {
|
|
|
1853
1912
|
}
|
|
1854
1913
|
else if (Array.isArray(obj)) {
|
|
1855
1914
|
for (const item of obj)
|
|
1856
|
-
checkSubjectPaths(item, span);
|
|
1915
|
+
checkSubjectPaths(item, span, key);
|
|
1857
1916
|
}
|
|
1858
1917
|
else if (obj !== null && typeof obj === "object") {
|
|
1859
|
-
for (const val of Object.
|
|
1860
|
-
checkSubjectPaths(val, span);
|
|
1918
|
+
for (const [key, val] of Object.entries(obj))
|
|
1919
|
+
checkSubjectPaths(val, span, key);
|
|
1861
1920
|
}
|
|
1862
1921
|
};
|
|
1863
1922
|
checkSubjectPaths(a.requires, row.span);
|
|
@@ -1880,6 +1939,17 @@ export function compile(source, options = {}) {
|
|
|
1880
1939
|
for (const key of ["invariants", "reports", "revisioned"])
|
|
1881
1940
|
if (body.has(key))
|
|
1882
1941
|
inst[key] = data(body.get(key));
|
|
1942
|
+
const shape = udlInstrumentSchema.safeParse(inst);
|
|
1943
|
+
if (!shape.success) {
|
|
1944
|
+
const issue = shape.error.issues[0];
|
|
1945
|
+
const actionName = issue.path[0] === "actions" ? String(issue.path[1]) : undefined;
|
|
1946
|
+
const node = decl.body.entries.find((entry) => entry.key ===
|
|
1947
|
+
(actionName ? `action ${actionName}` : String(issue.path[0]))) ?? decl;
|
|
1948
|
+
failWithCode({
|
|
1949
|
+
span: node.span,
|
|
1950
|
+
source: declarationSources.get(decl) ?? "program",
|
|
1951
|
+
}, "UDL1003", `${id}.${issue.path.join(".")}: ${issue.message}`, "use the UDL typed clause shape");
|
|
1952
|
+
}
|
|
1883
1953
|
origins.push({
|
|
1884
1954
|
path: `$.instruments[${document.instruments.length}]`,
|
|
1885
1955
|
span: { ...origin, ...lineColAt(source, origin.start) },
|
|
@@ -1940,7 +2010,6 @@ export function compile(source, options = {}) {
|
|
|
1940
2010
|
fail(decl, `unknown object clause ${key}`, "use fields, columns, or attach");
|
|
1941
2011
|
}
|
|
1942
2012
|
}
|
|
1943
|
-
// 1. Lower authored fields
|
|
1944
2013
|
const authoredFields = [];
|
|
1945
2014
|
const authoredNames = [];
|
|
1946
2015
|
const fieldsBlock = asBlock(body.get("fields"));
|
|
@@ -1952,7 +2021,6 @@ export function compile(source, options = {}) {
|
|
|
1952
2021
|
authoredFields.push(fieldDef);
|
|
1953
2022
|
authoredNames.push(row.key);
|
|
1954
2023
|
}
|
|
1955
|
-
// 2. Process attachments
|
|
1956
2024
|
const attachments = [];
|
|
1957
2025
|
for (const entry of decl.body.entries) {
|
|
1958
2026
|
if (!entry.key.startsWith("attach "))
|
|
@@ -1976,6 +2044,8 @@ export function compile(source, options = {}) {
|
|
|
1976
2044
|
for (const row of attachmentBlock.entries) {
|
|
1977
2045
|
if (row.key === "rename") {
|
|
1978
2046
|
for (const r of asBlock(row.value).entries) {
|
|
2047
|
+
if (renames.has(r.key))
|
|
2048
|
+
fail(r, `duplicate rename ${r.key}`, "rename each subject field once");
|
|
1979
2049
|
renames.set(r.key, text(r.value));
|
|
1980
2050
|
renameEntries.set(r.key, r);
|
|
1981
2051
|
}
|
|
@@ -1984,7 +2054,15 @@ export function compile(source, options = {}) {
|
|
|
1984
2054
|
if (row.value.kind === "call") {
|
|
1985
2055
|
const actionName = row.value.name;
|
|
1986
2056
|
const publicName = text(row.value.args[0]);
|
|
2057
|
+
if (exposed.has(actionName) ||
|
|
2058
|
+
!template.body.entries.some((entry) => entry.key === `action ${actionName}`))
|
|
2059
|
+
fail(row, `unknown or repeated action ${actionName}`, "expose one declared action once");
|
|
1987
2060
|
exposed.set(actionName, publicName);
|
|
2061
|
+
exposures.push({
|
|
2062
|
+
instrument: instId,
|
|
2063
|
+
action: actionName,
|
|
2064
|
+
entry: row,
|
|
2065
|
+
});
|
|
1988
2066
|
}
|
|
1989
2067
|
}
|
|
1990
2068
|
else {
|
|
@@ -2077,7 +2155,6 @@ export function compile(source, options = {}) {
|
|
|
2077
2155
|
}
|
|
2078
2156
|
}
|
|
2079
2157
|
}
|
|
2080
|
-
// 4. Validate columns
|
|
2081
2158
|
const columnsExpr = body.get("columns");
|
|
2082
2159
|
let columns = [];
|
|
2083
2160
|
if (columnsExpr) {
|
|
@@ -2123,7 +2200,8 @@ export function compile(source, options = {}) {
|
|
|
2123
2200
|
// Propagate invoked requirements with the conditions on each invocation path.
|
|
2124
2201
|
let changedInvocations = true;
|
|
2125
2202
|
let invocationIterations = 0;
|
|
2126
|
-
|
|
2203
|
+
const invocationDepth = document.instruments.reduce((total, inst) => total + inst.actionOrder.length, 0);
|
|
2204
|
+
while (changedInvocations && invocationIterations < invocationDepth) {
|
|
2127
2205
|
changedInvocations = false;
|
|
2128
2206
|
invocationIterations++;
|
|
2129
2207
|
for (const inst of document.instruments) {
|
|
@@ -2283,6 +2361,12 @@ export function compile(source, options = {}) {
|
|
|
2283
2361
|
const eliminatedStates = new Map();
|
|
2284
2362
|
// Remove branches excluded by immutable tunables before checking reference states.
|
|
2285
2363
|
for (const inst of document.instruments) {
|
|
2364
|
+
if (!inst.lifecycle.states.includes(inst.lifecycle.initial) ||
|
|
2365
|
+
Object.values(inst.lifecycle.transitions).some((edge) => edge.from.some((state) => !inst.lifecycle.states.includes(state)) ||
|
|
2366
|
+
(edge.to !== "preserve" &&
|
|
2367
|
+
!inst.lifecycle.states.includes(edge.to))))
|
|
2368
|
+
continue;
|
|
2369
|
+
let specialized = false;
|
|
2286
2370
|
for (const [key, action] of Object.entries(inst.actions)) {
|
|
2287
2371
|
const constant = (value) => {
|
|
2288
2372
|
if ("literal" in value)
|
|
@@ -2304,8 +2388,11 @@ export function compile(source, options = {}) {
|
|
|
2304
2388
|
})) {
|
|
2305
2389
|
delete inst.actions[key];
|
|
2306
2390
|
delete inst.lifecycle.transitions[key];
|
|
2391
|
+
specialized = true;
|
|
2307
2392
|
}
|
|
2308
2393
|
}
|
|
2394
|
+
if (!specialized)
|
|
2395
|
+
continue;
|
|
2309
2396
|
const reachable = new Set([inst.lifecycle.initial]);
|
|
2310
2397
|
for (let n = 0; n < inst.lifecycle.states.length; n++)
|
|
2311
2398
|
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
@@ -2335,6 +2422,10 @@ export function compile(source, options = {}) {
|
|
|
2335
2422
|
: field.target).some((id) => eliminatedStates.get(id)?.has(state)));
|
|
2336
2423
|
}
|
|
2337
2424
|
}
|
|
2425
|
+
for (const exposure of exposures)
|
|
2426
|
+
if (!document.instruments.find((inst) => inst.id === exposure.instrument)
|
|
2427
|
+
?.actions[exposure.action])
|
|
2428
|
+
fail(exposure.entry, `action ${exposure.action} is excluded by these bindings`, "expose an action available with these tunables");
|
|
2338
2429
|
const changed = new Set();
|
|
2339
2430
|
for (const decl of program.decls)
|
|
2340
2431
|
if (decl.kind === "hide" || decl.kind === "expose") {
|