@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +28 -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 +193 -102
  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/examples-bundle.js +2 -2
  16. package/dist/src/examples-bundle.js.map +1 -1
  17. package/dist/src/header-source.d.ts +3 -0
  18. package/dist/src/header-source.d.ts.map +1 -0
  19. package/dist/src/header-source.js +36 -0
  20. package/dist/src/header-source.js.map +1 -0
  21. package/dist/src/headers.d.ts.map +1 -1
  22. package/dist/src/headers.js +4 -6
  23. package/dist/src/headers.js.map +1 -1
  24. package/dist/src/keywords.d.ts +8 -2
  25. package/dist/src/keywords.d.ts.map +1 -1
  26. package/dist/src/keywords.js +13 -5
  27. package/dist/src/keywords.js.map +1 -1
  28. package/dist/src/lex.d.ts +1 -1
  29. package/dist/src/lex.d.ts.map +1 -1
  30. package/dist/src/lex.js +11 -3
  31. package/dist/src/lex.js.map +1 -1
  32. package/dist/src/parse.d.ts.map +1 -1
  33. package/dist/src/parse.js +22 -5
  34. package/dist/src/parse.js.map +1 -1
  35. package/dist/src/std-bundle.js +2 -2
  36. package/dist/src/std-bundle.js.map +1 -1
  37. package/dist/src/tunables.d.ts.map +1 -1
  38. package/dist/src/tunables.js +2 -1
  39. package/dist/src/tunables.js.map +1 -1
  40. package/dist/src/version.d.ts +1 -1
  41. package/dist/src/version.js +1 -1
  42. package/docs/README.md +18 -10
  43. package/examples/insurance.hsx +2 -2
  44. package/examples/travel.hsx +2 -2
  45. package/package.json +3 -3
  46. package/src/ast.ts +0 -10
  47. package/src/cli.ts +1 -1
  48. package/src/compile.ts +277 -128
  49. package/src/diagnostics.ts +28 -0
  50. package/src/examples-bundle.ts +2 -2
  51. package/src/header-source.ts +44 -0
  52. package/src/headers.ts +4 -6
  53. package/src/keywords.ts +13 -5
  54. package/src/lex.ts +13 -5
  55. package/src/parse.ts +29 -5
  56. package/src/std-bundle.ts +2 -2
  57. package/src/tunables.ts +6 -1
  58. package/src/version.ts +1 -1
  59. package/std/financing.hsx +2 -2
  60. package/std/insurance.hsx +9 -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;
@@ -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
- f.owner = text(resolve(t.args[0]));
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") ?? emptyBlock);
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 = options.adapterRegistry &&
1406
- Object.hasOwn(options.adapterRegistry, binding)
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 = options.adapterRegistry?.[bindingName];
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 (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.")) {
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.values(obj))
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
- while (changedInvocations && invocationIterations < 32) {
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") {