@hyperscale0/hsx 5.2.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/src/ast.d.ts +0 -1
  3. package/dist/src/ast.d.ts.map +1 -1
  4. package/dist/src/ast.js +0 -11
  5. package/dist/src/ast.js.map +1 -1
  6. package/dist/src/cli.js +1 -1
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/compile.d.ts.map +1 -1
  9. package/dist/src/compile.js +185 -101
  10. package/dist/src/compile.js.map +1 -1
  11. package/dist/src/diagnostics.d.ts +14 -0
  12. package/dist/src/diagnostics.d.ts.map +1 -0
  13. package/dist/src/diagnostics.js +20 -0
  14. package/dist/src/diagnostics.js.map +1 -0
  15. package/dist/src/header-source.d.ts +3 -0
  16. package/dist/src/header-source.d.ts.map +1 -0
  17. package/dist/src/header-source.js +36 -0
  18. package/dist/src/header-source.js.map +1 -0
  19. package/dist/src/headers.d.ts.map +1 -1
  20. package/dist/src/headers.js +4 -6
  21. package/dist/src/headers.js.map +1 -1
  22. package/dist/src/keywords.d.ts +8 -2
  23. package/dist/src/keywords.d.ts.map +1 -1
  24. package/dist/src/keywords.js +13 -5
  25. package/dist/src/keywords.js.map +1 -1
  26. package/dist/src/lex.d.ts +1 -1
  27. package/dist/src/lex.d.ts.map +1 -1
  28. package/dist/src/lex.js +11 -3
  29. package/dist/src/lex.js.map +1 -1
  30. package/dist/src/parse.d.ts.map +1 -1
  31. package/dist/src/parse.js +22 -5
  32. package/dist/src/parse.js.map +1 -1
  33. package/dist/src/std-bundle.js +1 -1
  34. package/dist/src/std-bundle.js.map +1 -1
  35. package/dist/src/tunables.d.ts.map +1 -1
  36. package/dist/src/tunables.js +2 -1
  37. package/dist/src/tunables.js.map +1 -1
  38. package/dist/src/version.d.ts +1 -1
  39. package/dist/src/version.js +1 -1
  40. package/docs/README.md +5 -3
  41. package/package.json +3 -3
  42. package/src/ast.ts +0 -10
  43. package/src/cli.ts +1 -1
  44. package/src/compile.ts +267 -127
  45. package/src/diagnostics.ts +28 -0
  46. package/src/header-source.ts +44 -0
  47. package/src/headers.ts +4 -6
  48. package/src/keywords.ts +13 -5
  49. package/src/lex.ts +13 -5
  50. package/src/parse.ts +29 -5
  51. package/src/std-bundle.ts +1 -1
  52. package/src/tunables.ts +6 -1
  53. package/src/version.ts +1 -1
  54. package/std/financing.hsx +2 -2
@@ -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
  }
@@ -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
- const v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
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
- 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))
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" ? "number" : type;
861
- if (v.kind !== expected &&
862
- !(type === "duration" &&
863
- (v.kind === "text" || v.kind === "name") &&
864
- /^P/.test(v.value))) {
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 = options.adapterRegistry?.[n];
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 t = row.value.kind === "default" ? row.value.type : row.value;
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;
@@ -1317,15 +1362,15 @@ export function compile(source, options = {}) {
1317
1362
  values: [val(constant)],
1318
1363
  });
1319
1364
  else
1320
- f.value = literal(constant);
1365
+ f.value = literal(constant, String(type));
1321
1366
  }
1322
1367
  result.push(f);
1323
1368
  }
1324
1369
  return result;
1325
1370
  };
1326
1371
  fields.push(...lowerFields(asBlock(body.get("fields"))));
1327
- const lifecycle = data(body.get("lifecycle") ?? emptyBlock);
1328
- lifecycle.transitions = {};
1372
+ const lifecycle = data(asBlock(body.get("lifecycle")));
1373
+ lifecycle.transitions = Object.create(null);
1329
1374
  const inst = {
1330
1375
  id,
1331
1376
  ...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
@@ -1336,7 +1381,7 @@ export function compile(source, options = {}) {
1336
1381
  fields,
1337
1382
  calculate: calculations,
1338
1383
  lifecycle,
1339
- actions: {},
1384
+ actions: Object.create(null),
1340
1385
  actionOrder: [],
1341
1386
  };
1342
1387
  for (const row of decl.body.entries.filter((e) => e.key.startsWith("action "))) {
@@ -1409,12 +1454,8 @@ export function compile(source, options = {}) {
1409
1454
  if (!adapterExpr)
1410
1455
  fail(boundary, "boundary needs an adapter", "name a bound ADL adapter");
1411
1456
  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))
1457
+ const target = adapterTarget(binding);
1458
+ if (!target)
1418
1459
  fail(boundary, `unknown boundary adapter ${binding}`, "bind the named ADL adapter before compilation");
1419
1460
  boundaryBindings.add(binding);
1420
1461
  }
@@ -1458,7 +1499,7 @@ export function compile(source, options = {}) {
1458
1499
  ? entry.value.items.map(adapterName)
1459
1500
  : [adapterName(entry.value)];
1460
1501
  for (const bindingName of bindingNames) {
1461
- const target = options.adapterRegistry?.[bindingName];
1502
+ const target = adapterTarget(bindingName);
1462
1503
  if (target) {
1463
1504
  const { adapter, operation } = target;
1464
1505
  const opBinding = adapter.operationMap[operation];
@@ -1840,6 +1881,8 @@ export function compile(source, options = {}) {
1840
1881
  }
1841
1882
  if (attachmentInfo && !attachmentInfo.child) {
1842
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");
1843
1886
  a.publicAction = attachmentInfo.exposed.get(name);
1844
1887
  }
1845
1888
  else {
@@ -1848,9 +1891,18 @@ export function compile(source, options = {}) {
1848
1891
  }
1849
1892
  if (automatic(a.actor))
1850
1893
  delete a.publicAction;
1851
- const checkSubjectPaths = (obj, span) => {
1894
+ const checkSubjectPaths = (obj, span, key = "") => {
1852
1895
  if (typeof obj === "string") {
1853
- if (obj.startsWith("subject.")) {
1896
+ if ([
1897
+ "field",
1898
+ "fields",
1899
+ "reference",
1900
+ "anchor",
1901
+ "subject",
1902
+ "at",
1903
+ "instruction",
1904
+ ].includes(key) &&
1905
+ obj.startsWith("subject.")) {
1854
1906
  const subField = obj.split(".")[1];
1855
1907
  const req = a.subject?.requirements.find((r) => r.field.name === subField);
1856
1908
  if (!req) {
@@ -1860,11 +1912,11 @@ export function compile(source, options = {}) {
1860
1912
  }
1861
1913
  else if (Array.isArray(obj)) {
1862
1914
  for (const item of obj)
1863
- checkSubjectPaths(item, span);
1915
+ checkSubjectPaths(item, span, key);
1864
1916
  }
1865
1917
  else if (obj !== null && typeof obj === "object") {
1866
- for (const val of Object.values(obj))
1867
- checkSubjectPaths(val, span);
1918
+ for (const [key, val] of Object.entries(obj))
1919
+ checkSubjectPaths(val, span, key);
1868
1920
  }
1869
1921
  };
1870
1922
  checkSubjectPaths(a.requires, row.span);
@@ -1887,6 +1939,17 @@ export function compile(source, options = {}) {
1887
1939
  for (const key of ["invariants", "reports", "revisioned"])
1888
1940
  if (body.has(key))
1889
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
+ }
1890
1953
  origins.push({
1891
1954
  path: `$.instruments[${document.instruments.length}]`,
1892
1955
  span: { ...origin, ...lineColAt(source, origin.start) },
@@ -1947,7 +2010,6 @@ export function compile(source, options = {}) {
1947
2010
  fail(decl, `unknown object clause ${key}`, "use fields, columns, or attach");
1948
2011
  }
1949
2012
  }
1950
- // 1. Lower authored fields
1951
2013
  const authoredFields = [];
1952
2014
  const authoredNames = [];
1953
2015
  const fieldsBlock = asBlock(body.get("fields"));
@@ -1959,7 +2021,6 @@ export function compile(source, options = {}) {
1959
2021
  authoredFields.push(fieldDef);
1960
2022
  authoredNames.push(row.key);
1961
2023
  }
1962
- // 2. Process attachments
1963
2024
  const attachments = [];
1964
2025
  for (const entry of decl.body.entries) {
1965
2026
  if (!entry.key.startsWith("attach "))
@@ -1983,6 +2044,8 @@ export function compile(source, options = {}) {
1983
2044
  for (const row of attachmentBlock.entries) {
1984
2045
  if (row.key === "rename") {
1985
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");
1986
2049
  renames.set(r.key, text(r.value));
1987
2050
  renameEntries.set(r.key, r);
1988
2051
  }
@@ -1991,7 +2054,15 @@ export function compile(source, options = {}) {
1991
2054
  if (row.value.kind === "call") {
1992
2055
  const actionName = row.value.name;
1993
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");
1994
2060
  exposed.set(actionName, publicName);
2061
+ exposures.push({
2062
+ instrument: instId,
2063
+ action: actionName,
2064
+ entry: row,
2065
+ });
1995
2066
  }
1996
2067
  }
1997
2068
  else {
@@ -2084,7 +2155,6 @@ export function compile(source, options = {}) {
2084
2155
  }
2085
2156
  }
2086
2157
  }
2087
- // 4. Validate columns
2088
2158
  const columnsExpr = body.get("columns");
2089
2159
  let columns = [];
2090
2160
  if (columnsExpr) {
@@ -2130,7 +2200,8 @@ export function compile(source, options = {}) {
2130
2200
  // Propagate invoked requirements with the conditions on each invocation path.
2131
2201
  let changedInvocations = true;
2132
2202
  let invocationIterations = 0;
2133
- while (changedInvocations && invocationIterations < 32) {
2203
+ const invocationDepth = document.instruments.reduce((total, inst) => total + inst.actionOrder.length, 0);
2204
+ while (changedInvocations && invocationIterations < invocationDepth) {
2134
2205
  changedInvocations = false;
2135
2206
  invocationIterations++;
2136
2207
  for (const inst of document.instruments) {
@@ -2290,6 +2361,12 @@ export function compile(source, options = {}) {
2290
2361
  const eliminatedStates = new Map();
2291
2362
  // Remove branches excluded by immutable tunables before checking reference states.
2292
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;
2293
2370
  for (const [key, action] of Object.entries(inst.actions)) {
2294
2371
  const constant = (value) => {
2295
2372
  if ("literal" in value)
@@ -2311,8 +2388,11 @@ export function compile(source, options = {}) {
2311
2388
  })) {
2312
2389
  delete inst.actions[key];
2313
2390
  delete inst.lifecycle.transitions[key];
2391
+ specialized = true;
2314
2392
  }
2315
2393
  }
2394
+ if (!specialized)
2395
+ continue;
2316
2396
  const reachable = new Set([inst.lifecycle.initial]);
2317
2397
  for (let n = 0; n < inst.lifecycle.states.length; n++)
2318
2398
  for (const edge of Object.values(inst.lifecycle.transitions))
@@ -2342,6 +2422,10 @@ export function compile(source, options = {}) {
2342
2422
  : field.target).some((id) => eliminatedStates.get(id)?.has(state)));
2343
2423
  }
2344
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");
2345
2429
  const changed = new Set();
2346
2430
  for (const decl of program.decls)
2347
2431
  if (decl.kind === "hide" || decl.kind === "expose") {