@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -2
  3. package/bin/hsx.ts +0 -2
  4. package/dist/bin/hsx.js +0 -2
  5. package/dist/bin/hsx.js.map +1 -1
  6. package/dist/src/ast.d.ts +0 -1
  7. package/dist/src/ast.d.ts.map +1 -1
  8. package/dist/src/ast.js +0 -11
  9. package/dist/src/ast.js.map +1 -1
  10. package/dist/src/cli.d.ts +0 -2
  11. package/dist/src/cli.d.ts.map +1 -1
  12. package/dist/src/cli.js +10 -5
  13. package/dist/src/cli.js.map +1 -1
  14. package/dist/src/compile.d.ts.map +1 -1
  15. package/dist/src/compile.js +204 -107
  16. package/dist/src/compile.js.map +1 -1
  17. package/dist/src/diagnostics.d.ts +14 -0
  18. package/dist/src/diagnostics.d.ts.map +1 -0
  19. package/dist/src/diagnostics.js +20 -0
  20. package/dist/src/diagnostics.js.map +1 -0
  21. package/dist/src/header-source.d.ts +3 -0
  22. package/dist/src/header-source.d.ts.map +1 -0
  23. package/dist/src/header-source.js +41 -0
  24. package/dist/src/header-source.js.map +1 -0
  25. package/dist/src/headers.d.ts.map +1 -1
  26. package/dist/src/headers.js +4 -6
  27. package/dist/src/headers.js.map +1 -1
  28. package/dist/src/keywords.d.ts +8 -2
  29. package/dist/src/keywords.d.ts.map +1 -1
  30. package/dist/src/keywords.js +13 -5
  31. package/dist/src/keywords.js.map +1 -1
  32. package/dist/src/lex.d.ts +1 -1
  33. package/dist/src/lex.d.ts.map +1 -1
  34. package/dist/src/lex.js +11 -3
  35. package/dist/src/lex.js.map +1 -1
  36. package/dist/src/parse.d.ts.map +1 -1
  37. package/dist/src/parse.js +22 -5
  38. package/dist/src/parse.js.map +1 -1
  39. package/dist/src/std-bundle.js +12 -12
  40. package/dist/src/std-bundle.js.map +1 -1
  41. package/dist/src/tunables.d.ts.map +1 -1
  42. package/dist/src/tunables.js +2 -1
  43. package/dist/src/tunables.js.map +1 -1
  44. package/dist/src/version.d.ts +1 -2
  45. package/dist/src/version.d.ts.map +1 -1
  46. package/dist/src/version.js +1 -2
  47. package/dist/src/version.js.map +1 -1
  48. package/docs/README.md +54 -7
  49. package/docs/headers.md +1 -1
  50. package/package.json +3 -3
  51. package/src/ast.ts +0 -10
  52. package/src/cli.ts +13 -7
  53. package/src/compile.ts +296 -133
  54. package/src/diagnostics.ts +28 -0
  55. package/src/header-source.ts +54 -0
  56. package/src/headers.ts +4 -6
  57. package/src/keywords.ts +13 -5
  58. package/src/lex.ts +13 -5
  59. package/src/parse.ts +29 -5
  60. package/src/std-bundle.ts +12 -12
  61. package/src/tunables.ts +6 -1
  62. package/src/version.ts +1 -2
  63. package/std/cards.hsx +12 -0
  64. package/std/collections.hsx +11 -0
  65. package/std/escrow.hsx +16 -2
  66. package/std/financing.hsx +36 -5
  67. package/std/insurance.hsx +20 -5
  68. package/std/lending.hsx +14 -0
  69. package/std/marketplace.hsx +14 -0
  70. package/std/money.hsx +13 -0
  71. package/std/reporting.hsx +10 -0
  72. package/std/savings.hsx +28 -11
  73. package/std/travel.hsx +22 -4
  74. package/std/wallet.hsx +9 -0
@@ -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
- class CompileFailure extends Error {
10
- diagnostic;
11
- constructor(diagnostic) {
12
- super(diagnostic.message);
13
- this.diagnostic = diagnostic;
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
- let rawValue = row.value.kind === "default" ? row.value.type : row.value;
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" || (typeof actor === "object" && "parent" in actor);
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
- new Date(raw.slice(0, 10) + "T00:00:00Z").toISOString().slice(0, 10) !==
244
- raw.slice(0, 10))
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
- const header = parseProgram(content);
289
- if (header.diagnostics.length ||
290
- !header.program.header ||
291
- header.program.name !== use.name)
292
- fail(use, `header ${use.name} is malformed`, "repair the header source before compiling");
293
- const registerTemplates = (parentDecl, prefix, exportPath) => {
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}`, `${exportPath}.${recName}`);
322
+ registerTemplates(recDecl, `${prefix}.${recName}`);
307
323
  }
308
324
  };
309
- for (const decl of header.program.decls)
325
+ for (const decl of header.decls)
310
326
  if (decl.kind === "instrument") {
311
- registerTemplates(decl, `${use.name}.${decl.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
- try {
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
- try {
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
- try {
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 : "yourDepositField";
672
- fail(suppliedValue, `\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its funding action requires the object's \`${field}\` field.`, `Remove \`${key}\`. To use your deposit field, add \`rename { ${field}: ${target} }\`.`);
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 takes precedence over a same-named attachment.
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
- const v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
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
- const [root, ...tail] = value.value.split(".");
825
- const obj = objects.get(root);
826
- const assignment = assignments.get(root);
827
- const targetType = obj ? obj.name : assignment?.target;
828
- if ((!obj &&
829
- !assignment &&
830
- !document.instruments.some((inst) => inst.id === value.value)) ||
831
- (t.kind === "type" &&
832
- t.target &&
833
- [targetType, ...tail].join(".") !== t.target))
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" ? "number" : type;
861
- if (v.kind !== expected &&
862
- !(type === "duration" &&
863
- (v.kind === "text" || v.kind === "name") &&
864
- /^P/.test(v.value))) {
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 = options.adapterRegistry?.[n];
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 t = row.value.kind === "default" ? row.value.type : row.value;
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") ?? emptyBlock);
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 = options.adapterRegistry &&
1413
- Object.hasOwn(options.adapterRegistry, binding)
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 = options.adapterRegistry?.[bindingName];
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, span) => {
1898
+ const checkSubjectPaths = (obj, key = "") => {
1852
1899
  if (typeof obj === "string") {
1853
- if (obj.startsWith("subject.")) {
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({ span }, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${name}`, `declare ${subField} in subject { ... }`);
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, span);
1919
+ checkSubjectPaths(item, key);
1864
1920
  }
1865
1921
  else if (obj !== null && typeof obj === "object") {
1866
- for (const val of Object.values(obj))
1867
- checkSubjectPaths(val, span);
1922
+ for (const [key, val] of Object.entries(obj))
1923
+ checkSubjectPaths(val, key);
1868
1924
  }
1869
1925
  };
1870
- checkSubjectPaths(a.requires, row.span);
1871
- checkSubjectPaths(a.set, row.span);
1872
- checkSubjectPaths(a.invoke, row.span);
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
- while (changedInvocations && invocationIterations < 32) {
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") {