@hyperscale0/hsx 1.0.342 → 1.0.350

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 (144) hide show
  1. package/README.md +1 -1
  2. package/bases/cards.hsx +0 -1
  3. package/bases/collections.hsx +0 -2
  4. package/bases/escrow.hsx +0 -1
  5. package/bases/financing.hsx +0 -1
  6. package/bases/insurance.hsx +0 -1
  7. package/bases/lending.hsx +0 -3
  8. package/bases/marketplace.hsx +0 -2
  9. package/bases/money.hsx +0 -1
  10. package/bases/reporting.hsx +0 -2
  11. package/bases/savings.hsx +0 -1
  12. package/bases/wallet.hsx +0 -1
  13. package/dist/bin/hsx.d.ts.map +1 -1
  14. package/dist/bin/hsx.js.map +1 -1
  15. package/dist/src/ast.d.ts +57 -7
  16. package/dist/src/ast.d.ts.map +1 -1
  17. package/dist/src/ast.js.map +1 -1
  18. package/dist/src/attachment-economics.d.ts.map +1 -1
  19. package/dist/src/attachment-economics.js.map +1 -1
  20. package/dist/src/bases-bundle.d.ts.map +1 -1
  21. package/dist/src/bases-bundle.js +11 -11
  22. package/dist/src/bases-bundle.js.map +1 -1
  23. package/dist/src/bases.d.ts.map +1 -1
  24. package/dist/src/bases.js.map +1 -1
  25. package/dist/src/catalog-program.d.ts +1 -1
  26. package/dist/src/catalog-program.d.ts.map +1 -1
  27. package/dist/src/catalog-program.js +0 -13
  28. package/dist/src/catalog-program.js.map +1 -1
  29. package/dist/src/checks.d.ts +60 -0
  30. package/dist/src/checks.d.ts.map +1 -0
  31. package/dist/src/checks.js +240 -0
  32. package/dist/src/checks.js.map +1 -0
  33. package/dist/src/cli.d.ts.map +1 -1
  34. package/dist/src/cli.js +6 -25
  35. package/dist/src/cli.js.map +1 -1
  36. package/dist/src/compile.d.ts +29 -12
  37. package/dist/src/compile.d.ts.map +1 -1
  38. package/dist/src/compile.js +414 -316
  39. package/dist/src/compile.js.map +1 -1
  40. package/dist/src/cost.d.ts.map +1 -1
  41. package/dist/src/cost.js.map +1 -1
  42. package/dist/src/diagnostics.d.ts +14 -1
  43. package/dist/src/diagnostics.d.ts.map +1 -1
  44. package/dist/src/diagnostics.js +31 -0
  45. package/dist/src/diagnostics.js.map +1 -1
  46. package/dist/src/examples-bundle.d.ts.map +1 -1
  47. package/dist/src/examples-bundle.js +15 -15
  48. package/dist/src/examples-bundle.js.map +1 -1
  49. package/dist/src/examples.d.ts.map +1 -1
  50. package/dist/src/examples.js.map +1 -1
  51. package/dist/src/format.d.ts.map +1 -1
  52. package/dist/src/format.js.map +1 -1
  53. package/dist/src/header-source.d.ts +30 -0
  54. package/dist/src/header-source.d.ts.map +1 -1
  55. package/dist/src/header-source.js +120 -9
  56. package/dist/src/header-source.js.map +1 -1
  57. package/dist/src/headers.d.ts +9 -18
  58. package/dist/src/headers.d.ts.map +1 -1
  59. package/dist/src/headers.js +25 -23
  60. package/dist/src/headers.js.map +1 -1
  61. package/dist/src/index.d.ts +4 -3
  62. package/dist/src/index.d.ts.map +1 -1
  63. package/dist/src/index.js +2 -2
  64. package/dist/src/index.js.map +1 -1
  65. package/dist/src/keywords.d.ts +2 -4
  66. package/dist/src/keywords.d.ts.map +1 -1
  67. package/dist/src/keywords.js +6 -5
  68. package/dist/src/keywords.js.map +1 -1
  69. package/dist/src/language.d.ts +3 -4
  70. package/dist/src/language.d.ts.map +1 -1
  71. package/dist/src/language.js +123 -60
  72. package/dist/src/language.js.map +1 -1
  73. package/dist/src/lex.d.ts +1 -1
  74. package/dist/src/lex.d.ts.map +1 -1
  75. package/dist/src/lex.js +3 -5
  76. package/dist/src/lex.js.map +1 -1
  77. package/dist/src/parse.d.ts +3 -1
  78. package/dist/src/parse.d.ts.map +1 -1
  79. package/dist/src/parse.js +234 -22
  80. package/dist/src/parse.js.map +1 -1
  81. package/dist/src/std-bundle.d.ts +2 -1
  82. package/dist/src/std-bundle.d.ts.map +1 -1
  83. package/dist/src/std-bundle.js +16 -15
  84. package/dist/src/std-bundle.js.map +1 -1
  85. package/dist/src/template-guide.d.ts +3 -9
  86. package/dist/src/template-guide.d.ts.map +1 -1
  87. package/dist/src/template-guide.js +16 -23
  88. package/dist/src/template-guide.js.map +1 -1
  89. package/dist/src/toolchain.d.ts +23 -0
  90. package/dist/src/toolchain.d.ts.map +1 -0
  91. package/dist/src/toolchain.js +29 -0
  92. package/dist/src/toolchain.js.map +1 -0
  93. package/dist/src/tunables.d.ts.map +1 -1
  94. package/dist/src/tunables.js.map +1 -1
  95. package/docs/README.md +69 -28
  96. package/examples/camera-rental.hsx +0 -1
  97. package/examples/cards.hsx +0 -1
  98. package/examples/collections.hsx +0 -2
  99. package/examples/escrow.hsx +0 -1
  100. package/examples/financing.hsx +0 -1
  101. package/examples/insurance.hsx +0 -2
  102. package/examples/lending.hsx +0 -3
  103. package/examples/library.hsx +0 -3
  104. package/examples/marketplace.hsx +0 -2
  105. package/examples/money.hsx +0 -1
  106. package/examples/rental-hire.hsx +0 -1
  107. package/examples/reporting.hsx +0 -2
  108. package/examples/savings.hsx +0 -1
  109. package/examples/serviced.hsx +0 -4
  110. package/examples/wallet.hsx +0 -1
  111. package/package.json +8 -8
  112. package/src/ast.ts +56 -3
  113. package/src/bases-bundle.ts +11 -11
  114. package/src/catalog-program.ts +0 -13
  115. package/src/checks.ts +386 -0
  116. package/src/cli.ts +6 -25
  117. package/src/compile.ts +583 -438
  118. package/src/diagnostics.ts +49 -1
  119. package/src/examples-bundle.ts +15 -15
  120. package/src/header-source.ts +187 -11
  121. package/src/headers.ts +31 -32
  122. package/src/index.ts +10 -4
  123. package/src/keywords.ts +6 -5
  124. package/src/language.ts +153 -68
  125. package/src/lex.ts +3 -5
  126. package/src/parse.ts +216 -24
  127. package/src/std-bundle.ts +17 -15
  128. package/src/template-guide.ts +17 -34
  129. package/src/toolchain.ts +51 -0
  130. package/std/booking.hsx +14 -1
  131. package/std/collections.hsx +12 -2
  132. package/std/financing.hsx +10 -16
  133. package/std/money.hsx +2 -3
  134. package/std/reporting.hsx +10 -1
  135. package/dist/src/binding-contract.d.ts +0 -22
  136. package/dist/src/binding-contract.d.ts.map +0 -1
  137. package/dist/src/binding-contract.js +0 -80
  138. package/dist/src/binding-contract.js.map +0 -1
  139. package/dist/src/std-library.d.ts +0 -5
  140. package/dist/src/std-library.d.ts.map +0 -1
  141. package/dist/src/std-library.js +0 -5
  142. package/dist/src/std-library.js.map +0 -1
  143. package/src/binding-contract.ts +0 -111
  144. package/src/std-library.ts +0 -7
package/src/compile.ts CHANGED
@@ -1,15 +1,19 @@
1
1
  import {
2
2
  CompileFailure,
3
+ conflict,
3
4
  didYouMean,
4
5
  fail,
5
6
  failWithCode,
7
+ type Site,
6
8
  } from "./diagnostics.ts";
7
- import { hash as sha256 } from "fast-sha256";
8
9
  import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
9
10
  import {
11
+ canonicalJson,
12
+ digest,
10
13
  validateUdl,
11
14
  resolveField,
12
15
  sameAccount,
16
+ sameFamily,
13
17
  sameObjectField,
14
18
  subjectPartyRoles,
15
19
  RESERVED_OBJECT_NAMES,
@@ -33,20 +37,16 @@ import {
33
37
  type UdlSubjectRequirement,
34
38
  type UdlValue,
35
39
  } from "@hyperscale0/udl";
36
- import {
37
- bindingDependencies,
38
- parameterDiagnostics,
39
- BindingContractError,
40
- } from "./binding-contract.ts";
41
40
  import { tunableBounds } from "./tunables.ts";
42
- import { parseProgram } from "./parse.ts";
41
+ import { lowerChecks, type CompiledCheck } from "./checks.ts";
42
+ import { parseExpression, parseProgram } from "./parse.ts";
43
43
  import {
44
44
  applyAttachmentEconomics,
45
45
  purposeWarnings,
46
46
  samePartyMoves,
47
47
  unboundEconomicSources,
48
48
  } from "./attachment-economics.ts";
49
- import { parseHeader } from "./header-source.ts";
49
+ import { instrumentChecks, parseHeader } from "./header-source.ts";
50
50
  import {
51
51
  lineColAt,
52
52
  type AssignmentDecl,
@@ -58,9 +58,11 @@ import {
58
58
  type ObjectDecl,
59
59
  type Span,
60
60
  } from "./ast.ts";
61
- import { bundledStandardLibrary, type StandardLibrary } from "./std-library.ts";
62
-
63
- import type { ProviderAdapter } from "@hyperscale0/adl";
61
+ import {
62
+ defaultToolchain,
63
+ type AdapterBindingTarget,
64
+ type Toolchain,
65
+ } from "./toolchain.ts";
64
66
 
65
67
  export interface CompileDiagnostic extends Diagnostic {
66
68
  line: number;
@@ -71,18 +73,42 @@ export interface CompileDiagnostic extends Diagnostic {
71
73
  severity: "error";
72
74
  stage: "parse" | "check" | "lower";
73
75
  }
76
+ /** A std header parameter's value in force on one instrument, and who set it. */
77
+ export interface CompileTerm {
78
+ name: string;
79
+ /** program: the author bound it. party: a party of the same name bound it. std: the header's default. */
80
+ from: "program" | "party" | "std";
81
+ /** The value as written, or "unset" for an optional parameter nothing set. */
82
+ text: string;
83
+ /** "program", or the header that holds the default. */
84
+ source: string;
85
+ line: number;
86
+ column: number;
87
+ }
88
+ /** One std term on one instrument, set as another compile had it. */
89
+ export type CompileTermOverride = Pick<
90
+ CompileTerm,
91
+ "name" | "from" | "text"
92
+ > & {
93
+ instrument: string;
94
+ };
74
95
  export interface CompileOriginMapEntry {
75
96
  path: string;
76
97
  span: Span & { line: number; column: number };
77
- standardBlock?: { key: string; recordPath: string; headerDigest: string };
78
- }
79
- export interface AdapterBindingTarget {
80
- adapter: ProviderAdapter;
81
- operation: string;
98
+ /** The std declaration an instrument lowers from, by its declaration digest. */
99
+ standardBlock?: { key: string; recordPath: string; digest: string };
100
+ /** One row per header parameter of a std instrument. */
101
+ terms?: CompileTerm[];
82
102
  }
83
- export interface CompileOptions {
84
- standardLibrary?: StandardLibrary;
85
- adapterRegistry?: Readonly<Record<string, AdapterBindingTarget>>;
103
+ /**
104
+ * A parameter's value in force where it was written. An optional parameter
105
+ * nothing set has no value; a std term is unset for `when x is set`.
106
+ */
107
+ interface Term {
108
+ value?: Expr;
109
+ from: CompileTerm["from"];
110
+ span: Span;
111
+ source: string;
86
112
  }
87
113
  export interface CompileWarning extends Omit<CompileDiagnostic, "severity"> {
88
114
  severity: "warning";
@@ -96,6 +122,8 @@ export interface CompileResult {
96
122
  document: UdlDocument;
97
123
  costManifest: UdlCostManifest;
98
124
  originMap: CompileOriginMapEntry[];
125
+ /** `check` blocks, beside the document and never inside it. */
126
+ checks: CompiledCheck[];
99
127
  };
100
128
  }
101
129
  function fieldType(row: Entry): { type: Expr; sensitive: boolean } {
@@ -228,27 +256,11 @@ function lowerObjectField(
228
256
  );
229
257
  return result.data;
230
258
  }
231
- function canonicalJson(value: unknown): string {
232
- if (value === null || typeof value !== "object") {
233
- return JSON.stringify(value);
234
- }
235
- if (Array.isArray(value)) {
236
- return `[${value.map((item) => (item === undefined ? "null" : canonicalJson(item))).join(",")}]`;
237
- }
238
- const record = value as Record<string, unknown>;
239
- const keys = Object.keys(record)
240
- .filter((k) => record[k] !== undefined)
241
- .sort();
242
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
243
- }
244
-
245
259
  const emptyBlock: BlockExpr = {
246
260
  kind: "block",
247
261
  entries: [],
248
262
  span: { start: 0, end: 0 },
249
263
  };
250
- const camel = (name: string) =>
251
- name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
252
264
  /** Clock and parent actors run without a caller, so they have no public name. */
253
265
  const automatic = (actor: UdlAction["actor"]) =>
254
266
  actor === "clock" ||
@@ -273,33 +285,74 @@ function namedParties(instrument: UdlInstrument): string[] {
273
285
  }
274
286
  return names;
275
287
  }
276
- function entries(block: BlockExpr): Map<string, Expr> {
277
- const map = new Map<string, Expr>();
288
+ /**
289
+ * How a block merges a key given twice: `concat` joins the values, `equal`
290
+ * keeps one value given twice alike, and `unique` refuses a second. A key not
291
+ * listed is unique; tunable arguments are equal.
292
+ */
293
+ type Merge = "unique" | "concat" | "equal";
294
+ const MERGE: Readonly<Record<string, Merge>> = {
295
+ requires: "concat",
296
+ moves: "concat",
297
+ invariants: "concat",
298
+ invoke: "concat",
299
+ calculate: "concat",
300
+ expose: "concat",
301
+ };
302
+ // The sources of the compile in progress, so a conflict prints line:column.
303
+ let texts: ReadonlyMap<string, string> = new Map();
304
+ /** An expression with spans, sources and comments dropped. */
305
+ const normal = (value: unknown): unknown =>
306
+ Array.isArray(value)
307
+ ? value.map(normal)
308
+ : value && typeof value === "object"
309
+ ? Object.fromEntries(
310
+ Object.entries(value).flatMap(([key, item]) =>
311
+ key === "span" || key === "source" || item === undefined
312
+ ? []
313
+ : [[key, normal(item)]],
314
+ ),
315
+ )
316
+ : value;
317
+ function given(
318
+ block: BlockExpr,
319
+ merge = (key: string): Merge => MERGE[key] ?? "unique",
320
+ ): Map<string, Entry> {
321
+ const map = new Map<string, Entry>();
278
322
  for (const row of block.entries) {
279
323
  const previous = map.get(row.key);
280
- if (previous) {
281
- if (
282
- ![
283
- "requires",
284
- "moves",
285
- "invariants",
286
- "invoke",
287
- "calculate",
288
- "expose",
289
- ].includes(row.key)
290
- )
291
- fail(row, `duplicate ${row.key}`, "keep one value for this name");
324
+ const rule = merge(row.key);
325
+ if (!previous) map.set(row.key, row);
326
+ else if (rule === "concat") {
292
327
  const items = (expr: Expr) =>
293
328
  expr.kind === "list" ? expr.items : [expr];
294
329
  map.set(row.key, {
295
- kind: "list",
296
- items: [...items(previous), ...items(row.value)],
297
- span: row.span,
330
+ ...row,
331
+ value: {
332
+ kind: "list",
333
+ items: [...items(previous.value), ...items(row.value)],
334
+ span: row.span,
335
+ },
298
336
  });
299
- } else map.set(row.key, row.value);
337
+ } else if (
338
+ rule === "unique" ||
339
+ JSON.stringify(normal(previous.value)) !==
340
+ JSON.stringify(normal(row.value))
341
+ )
342
+ conflict(
343
+ texts,
344
+ `${row.key} is ${rule === "equal" ? "set" : "given"} twice`,
345
+ previous,
346
+ row,
347
+ "keep one",
348
+ rule === "equal" ? { values: [previous.value, row.value] } : {},
349
+ );
300
350
  }
301
351
  return map;
302
352
  }
353
+ function entries(block: BlockExpr): Map<string, Expr> {
354
+ return new Map([...given(block)].map(([key, row]) => [key, row.value]));
355
+ }
303
356
  function asBlock(expr: Expr | undefined): BlockExpr {
304
357
  if (!expr) return emptyBlock;
305
358
  if (expr.kind !== "block")
@@ -456,12 +509,26 @@ function literal(expr: Expr, expectedType?: string): string | number | boolean {
456
509
  return fail(expr, "expected a literal", "write a typed constant");
457
510
  }
458
511
 
512
+ /** A std declaration's identity: its AST with spans, sources and comments dropped. */
513
+ function declarationDigest(decl: InstrumentDecl): string {
514
+ return digest(normal(decl));
515
+ }
516
+
517
+ /**
518
+ * An override compiles the program as if one std term held other text, so a
519
+ * plan can see what that term alone moves.
520
+ */
459
521
  export function compile(
460
522
  source: string,
461
- options: CompileOptions = {},
523
+ toolchain: Toolchain = defaultToolchain,
524
+ override?: CompileTermOverride,
462
525
  ): CompileResult {
463
526
  const parsed = parseProgram(source);
464
- const sources = new Map([["program", source]]);
527
+ const sources = new Map([
528
+ ["program", source],
529
+ ...(override ? [["override", override.text] as const] : []),
530
+ ]);
531
+ texts = sources;
465
532
  const diagnostic = (
466
533
  d: Diagnostic,
467
534
  stage: CompileDiagnostic["stage"],
@@ -498,8 +565,8 @@ export function compile(
498
565
  const program = parsed.program;
499
566
  const bindingDiagnostics: Diagnostic[] = [];
500
567
  const adapterTarget = (binding: string): AdapterBindingTarget | undefined => {
501
- const registry = options.adapterRegistry;
502
- if (!registry || !Object.hasOwn(registry, binding)) return;
568
+ const registry = toolchain.adapters;
569
+ if (!Object.hasOwn(registry, binding)) return;
503
570
  const target = registry[binding];
504
571
  return target &&
505
572
  Object.hasOwn(target.adapter.operationMap, target.operation)
@@ -530,6 +597,8 @@ export function compile(
530
597
  { entry: Entry; name: string; template: InstrumentDecl }
531
598
  >();
532
599
  const declarationSources = new Map<InstrumentDecl, string>();
600
+ // Declaration digest by std path, such as financing.installments.payment.
601
+ const familyDigests = new Map<string, string>();
533
602
  const standardOrigins = new Map<
534
603
  InstrumentDecl,
535
604
  NonNullable<CompileOriginMapEntry["standardBlock"]>
@@ -541,40 +610,27 @@ export function compile(
541
610
  // Instruments that lost a std action to an empty all() parent. Their
542
611
  // lifecycles drop the states only that action reached.
543
612
  const trimmed = new Set<string>();
544
- const used = new Set<string>();
545
- for (const use of program.decls.filter((d) => d.kind === "use")) {
546
- if (used.has(use.name))
547
- fail(
548
- use,
549
- `header ${use.name} is used twice`,
550
- "remove the duplicate use line",
551
- );
552
- used.add(use.name);
553
- const content = (
554
- options.standardLibrary ?? bundledStandardLibrary
555
- ).source(use.name);
556
- if (!content)
557
- fail(use, `unknown header ${use.name}`, "choose a published header");
558
- sources.set(use.name, content);
559
- const bundled = bundledStandardLibrary.source(use.name);
560
- const headerDigest =
561
- bundled === content
562
- ? Array.from(sha256(new TextEncoder().encode(content)), (byte) =>
563
- byte.toString(16).padStart(2, "0"),
564
- ).join("")
565
- : undefined;
613
+ // A qualified name such as booking.reservation loads its header the first
614
+ // time the compile reaches it. A header nothing names never loads.
615
+ const loaded = new Set<string>();
616
+ const loadHeader = (name: string, site?: Site) => {
617
+ if (loaded.has(name)) return true;
618
+ const content = toolchain.std.get(name);
619
+ if (content === undefined) return false;
620
+ loaded.add(name);
621
+ sources.set(name, content);
566
622
  let header;
567
623
  try {
568
- header = parseHeader(content, use.name);
624
+ header = parseHeader(content, name);
569
625
  } catch (error) {
570
- if (!(error instanceof CompileFailure)) throw error;
626
+ if (!(error instanceof CompileFailure) || !site) throw error;
571
627
  throw new CompileFailure({
572
628
  ...error.diagnostic,
573
629
  related: [
574
630
  {
575
- source: "program",
576
- span: use.span,
577
- message: `Imported header ${use.name}`,
631
+ source: site.source ?? "program",
632
+ span: site.span,
633
+ message: `Header ${name} is named here`,
578
634
  },
579
635
  ],
580
636
  });
@@ -583,22 +639,25 @@ export function compile(
583
639
  parentDecl: InstrumentDecl,
584
640
  prefix: string,
585
641
  ) => {
586
- if (templates.has(prefix))
587
- fail(
642
+ const first = templates.get(prefix);
643
+ if (first)
644
+ conflict(
645
+ texts,
646
+ `instrument ${prefix} is declared twice`,
647
+ first,
588
648
  parentDecl,
589
- `duplicate instrument ${prefix}`,
590
649
  "give each instrument a distinct name",
591
650
  );
592
651
  templates.set(prefix, parentDecl);
593
- declarationSources.set(parentDecl, use.name);
594
- if (headerDigest) {
595
- const [, root, ...record] = prefix.split(".");
596
- standardOrigins.set(parentDecl, {
597
- key: `${use.name}.${root}`,
598
- recordPath: record.join("."),
599
- headerDigest,
600
- });
601
- }
652
+ declarationSources.set(parentDecl, name);
653
+ const digest = declarationDigest(parentDecl);
654
+ familyDigests.set(prefix, digest);
655
+ const [, root, ...record] = prefix.split(".");
656
+ standardOrigins.set(parentDecl, {
657
+ key: `${name}.${root}`,
658
+ recordPath: record.join("."),
659
+ digest,
660
+ });
602
661
  const recs = entries(asBlock(entries(parentDecl.body).get("records")));
603
662
  for (const [recName, recBlock] of recs) {
604
663
  const recDecl: InstrumentDecl = {
@@ -612,10 +671,47 @@ export function compile(
612
671
  }
613
672
  };
614
673
  for (const decl of header.decls)
615
- if (decl.kind === "instrument") {
616
- registerTemplates(decl, `${use.name}.${decl.name}`);
617
- }
618
- }
674
+ if (decl.kind === "instrument")
675
+ registerTemplates(decl, `${name}.${decl.name}`);
676
+ return true;
677
+ };
678
+ const loadPath = (path: string, site?: Site) => {
679
+ const dot = path.indexOf(".");
680
+ return dot > 0 && loadHeader(path.slice(0, dot), site);
681
+ };
682
+ const templateAt = (path: string, site?: Site) => {
683
+ loadPath(path, site);
684
+ return templates.get(path);
685
+ };
686
+ // The template a qualified name names, or the reason it names none.
687
+ const requireTemplate = (path: string, site: Site) => {
688
+ const found = templateAt(path, site);
689
+ if (found) return found;
690
+ const root = path.split(".")[0]!;
691
+ const unknown = (noun: string, name: string, candidates: string[]) => {
692
+ const hint = didYouMean(name, candidates);
693
+ return hint
694
+ ? `unknown ${noun} ${name}.${hint}`
695
+ : `unknown ${noun} ${name}`;
696
+ };
697
+ if (path.includes(".") && !toolchain.std.has(root))
698
+ fail(
699
+ site,
700
+ unknown("header", root, [...toolchain.std.keys()]),
701
+ "name a published header",
702
+ );
703
+ return fail(
704
+ site,
705
+ unknown(
706
+ "instrument",
707
+ path,
708
+ [...templates.keys()].filter((name) => name.startsWith(`${root}.`)),
709
+ ),
710
+ path.includes(".")
711
+ ? `choose an instrument header ${root} declares`
712
+ : "choose a declared instrument",
713
+ );
714
+ };
619
715
  for (const decl of program.decls)
620
716
  if (decl.kind === "instrument") templates.set(decl.name, decl);
621
717
  const document: UdlDocument = {
@@ -634,7 +730,7 @@ export function compile(
634
730
  const objects = new Map<string, ObjectDecl>();
635
731
  const assignments = new Map<string, AssignmentDecl>();
636
732
  const attachmentSubjects = new Map<string, string>();
637
- const names = new Set<string>();
733
+ const names = new Map<string, Site>();
638
734
  // A record's create turns public only through a program-level expose,
639
735
  // which applies after lowering, so creation input needs the list first.
640
736
  const exposedCreates = new Set(
@@ -645,15 +741,17 @@ export function compile(
645
741
  ),
646
742
  );
647
743
  for (const decl of program.decls) {
648
- if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
649
- continue;
650
- if (names.has(decl.name))
651
- fail(
744
+ if (decl.kind === "expose") continue;
745
+ const first = names.get(decl.name);
746
+ if (first)
747
+ conflict(
748
+ texts,
749
+ `${decl.name} is declared twice`,
750
+ first,
652
751
  decl,
653
- `duplicate declaration ${decl.name}`,
654
752
  "give this declaration a distinct name",
655
753
  );
656
- names.add(decl.name);
754
+ names.set(decl.name, decl);
657
755
  if (decl.kind === "object") {
658
756
  objects.set(decl.name, decl);
659
757
  for (const entry of decl.body.entries) {
@@ -718,10 +816,9 @@ export function compile(
718
816
  "use module.instrument or module.instrument.record",
719
817
  );
720
818
  }
721
- const moduleName = parts[0]!;
722
- const exportPath = parts.slice(1).join(".");
723
- const targetTemplate = templates.get(rawPath);
724
- if (!targetTemplate) {
819
+ loadPath(rawPath, expr);
820
+ const digest = familyDigests.get(rawPath);
821
+ if (!digest) {
725
822
  if (!required) return undefined;
726
823
  failWithCode(
727
824
  expr,
@@ -730,45 +827,11 @@ export function compile(
730
827
  "choose a declared standard instrument",
731
828
  );
732
829
  }
733
- const topTemplate = templates.get(`${moduleName}.${parts[1]!}`);
734
- if (!topTemplate) {
735
- if (!required) return undefined;
736
- failWithCode(
737
- expr,
738
- "HSX1001",
739
- `unknown family declaration ${moduleName}.${parts[1]!}`,
740
- "choose a declared standard instrument",
741
- );
742
- }
743
- const topBody = entries(topTemplate.body);
744
- let revision: number | undefined;
745
- if (topBody.has("familyRevision")) {
746
- const val = literal(topBody.get("familyRevision")!);
747
- if (typeof val === "number") revision = val;
748
- }
749
- let currentBody = topTemplate.body;
750
- for (const recName of parts.slice(2)) {
751
- const recs = entries(asBlock(entries(currentBody).get("records")));
752
- const child = recs.get(recName);
753
- if (child) {
754
- currentBody = asBlock(child);
755
- const cBody = entries(currentBody);
756
- if (cBody.has("familyRevision")) {
757
- const val = literal(cBody.get("familyRevision")!);
758
- if (typeof val === "number") revision = val;
759
- }
760
- }
761
- }
762
- if (revision === undefined) {
763
- if (!required) return undefined;
764
- failWithCode(
765
- expr,
766
- "HSX1001",
767
- `declaration ${rawPath} has no familyRevision declared`,
768
- "declare familyRevision on the standard instrument",
769
- );
770
- }
771
- return { module: moduleName, exportPath, revision };
830
+ return {
831
+ module: parts[0]!,
832
+ exportPath: parts.slice(1).join("."),
833
+ digest,
834
+ };
772
835
  };
773
836
  const addInstrument = (
774
837
  decl: InstrumentDecl,
@@ -792,14 +855,10 @@ export function compile(
792
855
  /** The template as the founder wrote it, such as money.schedule. */
793
856
  template?: string;
794
857
  },
795
- familyDeclaration?: {
796
- module: string;
797
- exportPath: string;
798
- revision?: number | undefined;
799
- },
858
+ familyDeclaration?: UdlFamily,
800
859
  standardOrigin?: NonNullable<CompileOriginMapEntry["standardBlock"]>,
801
- /** Declared tunables, true where the author bound them. */
802
- inheritedTunables = new Map<string, boolean>(),
860
+ /** A child record's terms are its parent's. */
861
+ inheritedTerms = new Map<string, Term>(),
803
862
  ) => {
804
863
  const resolveFamilyInstruments = (
805
864
  family: UdlFamily,
@@ -836,7 +895,8 @@ export function compile(
836
895
  inst.family &&
837
896
  inst.family.module === family.module &&
838
897
  inst.family.exportPath === family.exportPath &&
839
- inst.family.revision === family.revision
898
+ "digest" in inst.family &&
899
+ inst.family.digest === family.digest
840
900
  ) {
841
901
  found.add(inst.id);
842
902
  }
@@ -900,7 +960,8 @@ export function compile(
900
960
 
901
961
  const getInstrumentFamily = (targetId: string): UdlFamily | undefined => {
902
962
  const existing = document.instruments.find((i) => i.id === targetId);
903
- if (existing?.family) return existing.family;
963
+ // This document is format 2, so every family it holds is a digest.
964
+ if (existing?.family) return existing.family as UdlFamily;
904
965
 
905
966
  if (targetId.startsWith(`${id}_`) && familyDeclaration) {
906
967
  const sub = resolveChildExportPath(
@@ -919,7 +980,7 @@ export function compile(
919
980
 
920
981
  for (const [asgnName, asgn] of assignments) {
921
982
  if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
922
- const tmpl = templates.get(asgn.target);
983
+ const tmpl = templateAt(asgn.target);
923
984
  if (!tmpl) continue;
924
985
  const mod = declarationSources.get(tmpl);
925
986
  if (!mod || mod === "program") continue;
@@ -954,16 +1015,11 @@ export function compile(
954
1015
  "name a declared instrument or a list of instruments",
955
1016
  );
956
1017
  const fam = getInstrumentFamily(tid);
957
- if (
958
- !fam ||
959
- fam.module !== expectedFamily.module ||
960
- fam.exportPath !== expectedFamily.exportPath ||
961
- fam.revision !== expectedFamily.revision
962
- ) {
1018
+ if (!sameFamily(fam, expectedFamily)) {
963
1019
  failWithCode(
964
1020
  expr,
965
1021
  "HSX1001",
966
- `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`,
1022
+ `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath}`,
967
1023
  "ensure target instrument matches the declared family",
968
1024
  );
969
1025
  }
@@ -979,28 +1035,27 @@ export function compile(
979
1035
  if (type.kind === "call" && type.name === "enum")
980
1036
  enums.set(parameter.key, type.args.map(text));
981
1037
  }
982
- const supplied = new Map<string, Expr>();
983
- const suppliedEntries = new Map<string, Entry>();
984
- for (const entry of arguments_.entries) {
985
- if (supplied.has(entry.key))
986
- fail(
987
- entry,
988
- `duplicate tunable ${entry.key}`,
989
- "supply each parameter once",
990
- );
991
- supplied.set(entry.key, entry.value);
992
- suppliedEntries.set(entry.key, entry);
993
- }
1038
+ const suppliedEntries = given(arguments_, () => "equal");
1039
+ const supplied = new Map(
1040
+ [...suppliedEntries].map(([key, entry]) => [key, entry.value]),
1041
+ );
994
1042
  const environment = new Map<string, Expr>(inherited);
995
1043
  // `when every is set` asks whether the author bound a tunable; a
996
1044
  // default leaves it unset.
997
- const tunables = new Map(inheritedTunables);
1045
+ const terms = new Map(inheritedTerms);
1046
+ const isSet = (key: string) => (terms.get(key)?.from ?? "std") !== "std";
998
1047
  const parameterErrors: Diagnostic[] = [];
1048
+ // A std default set back is the header as it was, so its digest too.
1049
+ let revertedHeader: InstrumentDecl | undefined;
999
1050
  for (const param of decl.parameters) {
1000
1051
  const type =
1001
1052
  param.value.kind === "default" ? param.value.type : param.value;
1053
+ const overridden =
1054
+ override?.instrument === id && override.name === param.key;
1002
1055
  const fallback =
1003
- param.value.kind === "default" ? param.value.value : undefined;
1056
+ param.value.kind === "default" && !overridden
1057
+ ? param.value.value
1058
+ : undefined;
1004
1059
  const typeName =
1005
1060
  type.kind === "type" || type.kind === "call" ? type.name : text(type);
1006
1061
  const partyParameter = attachmentInfo && typeName === "party";
@@ -1011,18 +1066,76 @@ export function compile(
1011
1066
  const byName: Expr | undefined =
1012
1067
  partyParameter &&
1013
1068
  !optional &&
1069
+ !overridden &&
1014
1070
  (subjectPartyRoles.includes(param.key as SubjectPartyRole) ||
1015
1071
  document.parties[param.key])
1016
1072
  ? { kind: "name", value: param.key, span: origin }
1017
1073
  : undefined;
1018
- tunables.set(param.key, supplied.has(param.key) || !!byName);
1074
+ const header = declarationSources.get(decl) ?? "program";
1075
+ const forced =
1076
+ !overridden || override.text === "unset"
1077
+ ? undefined
1078
+ : {
1079
+ ...(parseExpression(override.text) ??
1080
+ fail(
1081
+ { span: origin },
1082
+ `${param.key} cannot hold ${override.text}`,
1083
+ "override with text the term once held",
1084
+ )),
1085
+ source: "override",
1086
+ };
1087
+ const written = overridden ? forced : supplied.get(param.key);
1088
+ if (overridden && override.from === "std")
1089
+ revertedHeader = {
1090
+ ...decl,
1091
+ parameters: decl.parameters.map((entry) =>
1092
+ entry !== param
1093
+ ? entry
1094
+ : {
1095
+ ...entry,
1096
+ value: forced
1097
+ ? {
1098
+ kind: "default",
1099
+ type,
1100
+ value: forced,
1101
+ span: entry.span,
1102
+ }
1103
+ : type,
1104
+ },
1105
+ ),
1106
+ };
1107
+ terms.set(
1108
+ param.key,
1109
+ overridden
1110
+ ? {
1111
+ ...(forced ? { value: forced } : {}),
1112
+ from: override.from,
1113
+ span: forced?.span ?? origin,
1114
+ source: "override",
1115
+ }
1116
+ : written
1117
+ ? {
1118
+ value: written,
1119
+ from: "program",
1120
+ span: written.span,
1121
+ source: "program",
1122
+ }
1123
+ : byName
1124
+ ? {
1125
+ value: byName,
1126
+ from: "party",
1127
+ span: origin,
1128
+ source: "program",
1129
+ }
1130
+ : {
1131
+ ...(fallback ? { value: fallback } : {}),
1132
+ from: "std",
1133
+ span: (fallback ?? param).span,
1134
+ source: header,
1135
+ },
1136
+ );
1019
1137
  const actual =
1020
- supplied.get(param.key) ??
1021
- byName ??
1022
- (fallback && {
1023
- ...fallback,
1024
- source: declarationSources.get(decl) ?? "program",
1025
- });
1138
+ written ?? byName ?? (fallback && { ...fallback, source: header });
1026
1139
  if (!actual) {
1027
1140
  if (optional) continue;
1028
1141
  parameterErrors.push({
@@ -1046,45 +1159,21 @@ export function compile(
1046
1159
  }
1047
1160
  environment.set(param.key, actual);
1048
1161
  }
1049
- let policyDiagnostics: ReturnType<typeof parameterDiagnostics>;
1050
- let dependencies: ReturnType<typeof bindingDependencies>;
1051
- try {
1052
- policyDiagnostics = parameterDiagnostics(decl);
1053
- dependencies = bindingDependencies(decl);
1054
- } catch (error) {
1055
- if (!(error instanceof BindingContractError)) throw error;
1056
- throw new CompileFailure({
1057
- code: "HSX1001",
1058
- message: error.message,
1059
- fix: "Repair the header binding contract.",
1060
- span: error.entry.span,
1061
- source: declarationSources.get(decl) ?? "program",
1062
- });
1063
- }
1064
- for (const dependency of dependencies) {
1065
- const selected = environment.get(dependency.when);
1066
- const holds = enums.has(dependency.when)
1067
- ? selected?.kind === "name" && selected.value === dependency.is
1068
- : tunables.get(dependency.when) === (dependency.is === "set");
1069
- if (holds && !environment.has(dependency.binding)) {
1070
- throw new CompileFailure({
1071
- code: "HSX1001",
1072
- message: dependency.message.replaceAll(
1073
- "{attachment}",
1074
- attachmentInfo?.attachmentName ?? id,
1075
- ),
1076
- fix: dependency.fix,
1077
- span: origin,
1078
- related: [
1079
- {
1080
- source: declarationSources.get(decl) ?? "program",
1081
- span: dependency.span,
1082
- message: "Conditional binding requirement",
1083
- },
1084
- ],
1085
- });
1162
+ const checks = instrumentChecks(decl);
1163
+ const say = (message: string) =>
1164
+ message
1165
+ .replaceAll("{attachment}", attachmentInfo?.attachmentName ?? id)
1166
+ .replaceAll("{instrument}", assignments.get(id)?.target ?? decl.name);
1167
+ const collect = (run: () => void) => {
1168
+ try {
1169
+ run();
1170
+ } catch (error) {
1171
+ if (!(error instanceof CompileFailure)) throw error;
1172
+ parameterErrors.push(error.diagnostic);
1086
1173
  }
1087
- }
1174
+ };
1175
+ // Each tunable that failed its type rule, so no check compares it.
1176
+ const broken = new Set<string>();
1088
1177
  for (const key of supplied.keys()) {
1089
1178
  if (decl.parameters.some((p) => p.key === key)) continue;
1090
1179
  const suppliedValue = supplied.get(key)!;
@@ -1241,7 +1330,7 @@ export function compile(
1241
1330
  const subject = attachmentSubjects.get(assignment.name)!;
1242
1331
  return `\`${subject}.${assignment.name.slice(subject.length + 1)}\``;
1243
1332
  });
1244
- const target = templates.get(type);
1333
+ const target = templateAt(type);
1245
1334
  const required =
1246
1335
  target?.parameters
1247
1336
  .filter(
@@ -1429,7 +1518,7 @@ export function compile(
1429
1518
  "reference union needs 1 to 16 objects",
1430
1519
  "use at most 16 distinct object names",
1431
1520
  );
1432
- const seen = new Set<string>();
1521
+ const seen = new Map<string, Expr>();
1433
1522
  for (const value of values) {
1434
1523
  if (value.kind !== "name")
1435
1524
  fail(
@@ -1456,7 +1545,7 @@ export function compile(
1456
1545
  !value.value.startsWith(`${instance}_`)
1457
1546
  )
1458
1547
  continue;
1459
- const template = templates.get(target);
1548
+ const template = templateAt(target);
1460
1549
  if (!template) continue;
1461
1550
  const suffix = value.value
1462
1551
  .slice(instance.length + 1)
@@ -1473,9 +1562,16 @@ export function compile(
1473
1562
  `${param.key} has the wrong object type`,
1474
1563
  `use an object of type ${t.kind === "type" ? t.target : "ref"}`,
1475
1564
  );
1476
- if (seen.has(value.value))
1477
- fail(value, "duplicate reference", "list each object once");
1478
- seen.add(value.value);
1565
+ const first = seen.get(value.value);
1566
+ if (first)
1567
+ conflict(
1568
+ texts,
1569
+ `${param.key} lists ${value.value} twice`,
1570
+ first,
1571
+ value,
1572
+ "list each object once",
1573
+ );
1574
+ seen.set(value.value, value);
1479
1575
  }
1480
1576
  // A many-reference tunable is a list even when one object is bound,
1481
1577
  // so report datasets and other value positions never see a bare name.
@@ -1538,17 +1634,12 @@ export function compile(
1538
1634
  `For a varying deposit, ${advice} and add \`rename { ${param.key}: ${target} }\`. Use a literal only for a fixed charge.`,
1539
1635
  );
1540
1636
  }
1541
- if (type === "money" && v.kind === "percent") {
1542
- const policy = policyDiagnostics.find(
1543
- (policy) => policy.parameter === param.key,
1544
- );
1637
+ if (type === "money" && v.kind === "percent")
1545
1638
  fail(
1546
1639
  v,
1547
- `\`${assignments.get(id)?.target ?? decl.name}.${param.key}\` accepts ${policy?.accepts ?? "a fixed amount"}. It cannot express ${policy?.percentage ?? "a percentage-based charge"}.`,
1548
- policy?.fix ??
1549
- "Use a fixed amount only if that is the intended policy. A percentage charge needs an authored rate calculation; do not approximate it with a cash amount.",
1640
+ `\`${assignments.get(id)?.target ?? decl.name}.${param.key}\` accepts a fixed amount. It cannot express a percentage-based charge.`,
1641
+ "Use a fixed amount only if that is the intended policy. A percentage charge needs an authored rate calculation; do not approximate it with a cash amount.",
1550
1642
  );
1551
- }
1552
1643
  fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
1553
1644
  }
1554
1645
  try {
@@ -1602,119 +1693,163 @@ export function compile(
1602
1693
  span: party.span,
1603
1694
  message: `Declared party ${party.name}`,
1604
1695
  });
1605
- parameterErrors.push({ ...error.diagnostic, related });
1696
+ // A header's `x is <type>` check explains this tunable's type rule.
1697
+ const typed = checks.find(
1698
+ ({ test }) => test.kind === "type" && test.tunable === param.key,
1699
+ );
1700
+ broken.add(param.key);
1701
+ parameterErrors.push({
1702
+ ...error.diagnostic,
1703
+ ...(typed ? { message: say(typed.message), fix: typed.fix } : {}),
1704
+ related,
1705
+ });
1606
1706
  }
1607
1707
  }
1608
- if (parameterErrors.length) {
1609
- bindingDiagnostics.push(...parameterErrors.slice(1));
1610
- throw new CompileFailure(parameterErrors[0]!);
1611
- }
1612
1708
  const body = entries(decl.body);
1613
1709
  // A ceiling caps a tunable by the value of an enum tunable, such as the
1614
1710
  // longest term per loan kind. An unset tunable takes the ceiling; a
1615
1711
  // supplied one may lower it, never raise it.
1616
- for (const ceiling of asBlock(body.get("ceilings")).entries) {
1617
- const table = entries(asBlock(ceiling.value));
1618
- const by = table.get("by");
1619
- const selected = by && environment.get(text(by));
1620
- if (!by || selected?.kind !== "name")
1621
- fail(
1622
- ceiling,
1623
- `ceiling ${ceiling.key} needs by: an enum tunable`,
1624
- "write by: kind and one maximum per enum value",
1625
- );
1626
- const maximum = table.get(selected.value);
1627
- if (maximum?.kind !== "number")
1628
- fail(
1629
- ceiling,
1630
- `ceiling ${ceiling.key} has no maximum for ${selected.value}`,
1631
- `add ${selected.value}: number to the ceiling`,
1632
- );
1633
- if (!tunables.get(ceiling.key)) {
1634
- environment.set(ceiling.key, { ...maximum, span: origin });
1635
- continue;
1636
- }
1637
- const supplied = environment.get(ceiling.key)!;
1638
- if (
1639
- supplied.kind !== "number" ||
1640
- BigInt(supplied.value) > BigInt(maximum.value)
1641
- )
1642
- fail(
1643
- supplied,
1644
- `${ceiling.key} for ${selected.value} is at most ${maximum.value}`,
1645
- `choose ${ceiling.key} of ${maximum.value} or less, or omit it`,
1646
- );
1647
- }
1648
- for (const constraint of asBlock(body.get("constraints")).entries) {
1649
- const rule = constraint.value;
1650
- if (
1651
- rule.kind !== "call" ||
1652
- rule.args.length !== 1 ||
1653
- !["less_than", "at_most", "greater_than"].includes(rule.name)
1654
- )
1655
- fail(
1656
- rule,
1657
- "invalid tunable constraint",
1658
- "use less_than(name), at_most(name), or greater_than(name)",
1659
- );
1660
- const left = environment.get(constraint.key),
1661
- right = environment.get(text(rule.args[0]!));
1662
- if (!left || !right)
1663
- fail(
1664
- rule,
1665
- "constraint needs two declared tunables",
1666
- "name the compared tunables",
1667
- );
1668
- // A runtime value is checked when the agreement is created.
1669
- if (
1670
- [left, right].some(
1671
- (expr) => expr.kind === "name" && expr.value === "runtime",
1712
+ for (const ceiling of asBlock(body.get("ceilings")).entries)
1713
+ collect(() => {
1714
+ const table = entries(asBlock(ceiling.value));
1715
+ const by = table.get("by");
1716
+ const selected = by && environment.get(text(by));
1717
+ if (!by || selected?.kind !== "name")
1718
+ fail(
1719
+ ceiling,
1720
+ `ceiling ${ceiling.key} needs by: an enum tunable`,
1721
+ "write by: kind and one maximum per enum value",
1722
+ );
1723
+ const maximum = table.get(selected.value);
1724
+ if (maximum?.kind !== "number")
1725
+ fail(
1726
+ ceiling,
1727
+ `ceiling ${ceiling.key} has no maximum for ${selected.value}`,
1728
+ `add ${selected.value}: number to the ceiling`,
1729
+ );
1730
+ if (!isSet(ceiling.key)) {
1731
+ const term = terms.get(ceiling.key);
1732
+ if (term)
1733
+ terms.set(ceiling.key, {
1734
+ ...term,
1735
+ value: maximum,
1736
+ span: maximum.span,
1737
+ });
1738
+ environment.set(ceiling.key, { ...maximum, span: origin });
1739
+ return;
1740
+ }
1741
+ const supplied = environment.get(ceiling.key)!;
1742
+ if (
1743
+ supplied.kind !== "number" ||
1744
+ BigInt(supplied.value) > BigInt(maximum.value)
1672
1745
  )
1673
- )
1674
- continue;
1675
- const numericKind = (expr: Expr) =>
1676
- expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)
1677
- ? "duration"
1678
- : expr.kind;
1679
- if (
1680
- !["money", "number", "percent", "duration"].includes(
1681
- numericKind(left),
1682
- ) ||
1683
- numericKind(left) !== numericKind(right)
1684
- )
1685
- fail(
1686
- rule,
1687
- "constraint needs numeric tunables of the same type",
1688
- "compare two amounts, integers, percentages or durations",
1689
- );
1690
- const a = BigInt(String(literal(left))),
1691
- b = BigInt(String(literal(right)));
1692
- if (
1693
- !(rule.name === "less_than"
1694
- ? a < b
1695
- : rule.name === "at_most"
1696
- ? a <= b
1697
- : a > b)
1698
- )
1699
- fail(
1700
- left,
1701
- `${constraint.key} must be ${rule.name} ${text(rule.args[0]!)}`,
1702
- "choose values satisfying the header constraint",
1703
- );
1746
+ fail(
1747
+ supplied,
1748
+ `${ceiling.key} for ${selected.value} is at most ${maximum.value}`,
1749
+ `choose ${ceiling.key} of ${maximum.value} or less, or omit it`,
1750
+ );
1751
+ });
1752
+ // Every check runs, so one attach reports each broken rule at once.
1753
+ for (const check of checks)
1754
+ collect(() => {
1755
+ const { test, when } = check;
1756
+ if (when) {
1757
+ const selected = environment.get(when.selector);
1758
+ const holds = enums.has(when.selector)
1759
+ ? selected?.kind === "name" && selected.value === when.is
1760
+ : terms.has(when.selector) &&
1761
+ isSet(when.selector) === (when.is === "set");
1762
+ if (!holds) return;
1763
+ }
1764
+ if (test.kind === "type") return;
1765
+ let at: Entry | undefined;
1766
+ if (test.kind === "set") {
1767
+ if (isSet(test.tunable)) return;
1768
+ } else {
1769
+ const terms = [...test.left, ...test.right];
1770
+ const value = (term: Expr) =>
1771
+ term.kind === "name" ? environment.get(term.value) : term;
1772
+ // An unset or broken tunable has nothing to compare, and a
1773
+ // runtime value is checked when the agreement is created.
1774
+ if (
1775
+ terms.some(
1776
+ (term) =>
1777
+ (term.kind === "name" && broken.has(term.value)) ||
1778
+ !value(term) ||
1779
+ (value(term)!.kind === "name" &&
1780
+ (value(term) as { value: string }).value === "runtime"),
1781
+ )
1782
+ )
1783
+ return;
1784
+ const kind = (term: Expr) => {
1785
+ const expr = value(term)!;
1786
+ return expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)
1787
+ ? "duration"
1788
+ : expr.kind;
1789
+ };
1790
+ const kinds = new Set(terms.map(kind));
1791
+ if (
1792
+ kinds.size !== 1 ||
1793
+ !["money", "number", "percent", "duration"].includes(
1794
+ kind(terms[0]!),
1795
+ )
1796
+ )
1797
+ fail(
1798
+ check.rule,
1799
+ "check compares values of different types",
1800
+ "compare amounts, integers, percentages or durations of one type",
1801
+ );
1802
+ const total = (side: Expr[]) =>
1803
+ side.reduce(
1804
+ (sum, term) => sum + BigInt(String(literal(value(term)!))),
1805
+ 0n,
1806
+ );
1807
+ const [a, b] = [total(test.left), total(test.right)];
1808
+ const holds = {
1809
+ "<": a < b,
1810
+ "<=": a <= b,
1811
+ ">": a > b,
1812
+ ">=": a >= b,
1813
+ "==": a === b,
1814
+ "!=": a !== b,
1815
+ }[test.operator];
1816
+ if (holds) return;
1817
+ at = terms
1818
+ .map((term) =>
1819
+ term.kind === "name"
1820
+ ? suppliedEntries.get(term.value)
1821
+ : undefined,
1822
+ )
1823
+ .find(Boolean);
1824
+ }
1825
+ throw new CompileFailure({
1826
+ code: "HSX1001",
1827
+ message: say(check.message),
1828
+ fix: check.fix,
1829
+ span: at?.value.span ?? origin,
1830
+ related: [
1831
+ {
1832
+ source: check.source ?? "program",
1833
+ span: check.span,
1834
+ message: "Header check",
1835
+ },
1836
+ ],
1837
+ });
1838
+ });
1839
+ if (parameterErrors.length) {
1840
+ bindingDiagnostics.push(...parameterErrors.slice(1));
1841
+ throw new CompileFailure(parameterErrors[0]!);
1704
1842
  }
1705
1843
  for (const key of body.keys())
1706
1844
  if (
1707
1845
  ![
1708
- "familyRevision",
1709
1846
  "fields",
1710
1847
  "lifecycle",
1711
1848
  "records",
1712
1849
  "summary",
1713
1850
  "invariants",
1714
- "constraints",
1851
+ "checks",
1715
1852
  "ceilings",
1716
- "dependencies",
1717
- "parameterDiagnostics",
1718
1853
  "reports",
1719
1854
  "revisioned",
1720
1855
  "scope",
@@ -1726,24 +1861,6 @@ export function compile(
1726
1861
  `unknown instrument clause ${key}`,
1727
1862
  "use fields, lifecycle, actions, invariants, or records",
1728
1863
  );
1729
- if (body.has("familyRevision")) {
1730
- const revExpr = body.get("familyRevision")!;
1731
- const revVal = literal(revExpr);
1732
- if (
1733
- typeof revVal !== "number" ||
1734
- !Number.isInteger(revVal) ||
1735
- revVal <= 0
1736
- ) {
1737
- fail(
1738
- revExpr,
1739
- "familyRevision must be a positive integer",
1740
- "use a positive integer revision",
1741
- );
1742
- }
1743
- if (familyDeclaration) {
1744
- familyDeclaration.revision = revVal;
1745
- }
1746
- }
1747
1864
  const records = entries(asBlock(body.get("records")));
1748
1865
  if (!inherited.size)
1749
1866
  environment.set("parent", { kind: "name", value: id, span: origin });
@@ -1962,10 +2079,10 @@ export function compile(
1962
2079
  if (
1963
2080
  relation === "is" &&
1964
2081
  (choice === "set" || choice === "unset") &&
1965
- tunables.has(tunable!) &&
2082
+ terms.has(tunable!) &&
1966
2083
  !enums.has(tunable!)
1967
2084
  )
1968
- return tunables.get(tunable!) === (choice === "set");
2085
+ return isSet(tunable!) === (choice === "set");
1969
2086
  const binding = environment.get(tunable!);
1970
2087
  if (!binding)
1971
2088
  fail(
@@ -1980,7 +2097,7 @@ export function compile(
1980
2097
  const [root, ...children] = text(bound).split(".");
1981
2098
  const assignment = assignments.get(root!);
1982
2099
  let target = assignment
1983
- ? templates.get(assignment.target)?.body
2100
+ ? templateAt(assignment.target)?.body
1984
2101
  : program.decls
1985
2102
  .filter(
1986
2103
  (decl): decl is InstrumentDecl => decl.kind === "instrument",
@@ -2468,14 +2585,9 @@ export function compile(
2468
2585
  operation,
2469
2586
  requirements: validatedRequirements,
2470
2587
  };
2471
- const digest = sha256(
2472
- new TextEncoder().encode(canonicalJson(declaration)),
2473
- );
2474
2588
  const snapshot: UdlAdapterSubjectSnapshot = {
2475
2589
  ...declaration,
2476
- declarationDigest: Array.from(digest, (byte) =>
2477
- byte.toString(16).padStart(2, "0"),
2478
- ).join(""),
2590
+ declarationDigest: digest(declaration),
2479
2591
  };
2480
2592
  const adapterRenames: Record<string, string> = {};
2481
2593
  if (attachmentInfo?.renames) {
@@ -2606,7 +2718,6 @@ export function compile(
2606
2718
  summary: slots.has("summary")
2607
2719
  ? String(data(slots.get("summary")!))
2608
2720
  : title(name),
2609
- publicAction: camel(`${name}_${id}`),
2610
2721
  event: `${id}.${name}`,
2611
2722
  actor: "caller",
2612
2723
  input: lowerFields(asBlock(slots.get("input"))),
@@ -3234,20 +3345,16 @@ export function compile(
3234
3345
  }
3235
3346
  if (moveCalculations.length)
3236
3347
  a.calculate = [...(a.calculate ?? []), ...moveCalculations];
3237
- if (attachmentInfo) {
3238
- if (attachmentInfo.exposed.has(name)) {
3239
- if (automatic(a.actor))
3240
- fail(
3241
- row,
3242
- `${name} runs on the clock or its parent, not a caller`,
3243
- "expose only caller actions",
3244
- );
3245
- a.publicAction = attachmentInfo.exposed.get(name)!;
3246
- } else {
3247
- delete a.publicAction;
3248
- }
3348
+ // An action is hidden until an expose line names it.
3349
+ if (attachmentInfo?.exposed.has(name)) {
3350
+ if (automatic(a.actor))
3351
+ fail(
3352
+ row,
3353
+ `${name} runs on the clock or its parent, not a caller`,
3354
+ "expose only caller actions",
3355
+ );
3356
+ a.publicAction = attachmentInfo.exposed.get(name)!;
3249
3357
  }
3250
- if (automatic(a.actor)) delete a.publicAction;
3251
3358
  const checkSubjectPaths = (obj: unknown, key = "") => {
3252
3359
  if (typeof obj === "string") {
3253
3360
  if (
@@ -3434,15 +3541,33 @@ export function compile(
3434
3541
  origins.push({
3435
3542
  path: `$.instruments[${document.instruments.length}]`,
3436
3543
  span: { ...origin, ...lineColAt(source, origin.start) },
3437
- ...(standardOrigin ? { standardBlock: standardOrigin } : {}),
3544
+ ...(standardOrigin
3545
+ ? {
3546
+ standardBlock: standardOrigin,
3547
+ terms: [...terms].map(([name, term]) => {
3548
+ const text = sources.get(term.source)!;
3549
+ return {
3550
+ name,
3551
+ from: term.from,
3552
+ text: !term.value
3553
+ ? "unset"
3554
+ : term.from === "party"
3555
+ ? name
3556
+ : text.slice(term.span.start, term.span.end),
3557
+ source: term.source,
3558
+ ...lineColAt(text, term.span.start),
3559
+ };
3560
+ }),
3561
+ }
3562
+ : {}),
3438
3563
  });
3439
- if (familyDeclaration && familyDeclaration.revision !== undefined) {
3564
+ if (familyDeclaration)
3440
3565
  inst.family = {
3441
- module: familyDeclaration.module,
3442
- exportPath: familyDeclaration.exportPath,
3443
- revision: familyDeclaration.revision,
3566
+ ...familyDeclaration,
3567
+ ...(revertedHeader
3568
+ ? { digest: declarationDigest(revertedHeader) }
3569
+ : {}),
3444
3570
  };
3445
- }
3446
3571
  document.instruments.push(inst);
3447
3572
  declarations.set(inst, decl);
3448
3573
  for (const [key, definition] of records) {
@@ -3476,13 +3601,11 @@ export function compile(
3476
3601
  declarationSources.get(decl) ?? "program",
3477
3602
  );
3478
3603
  const childFamily = familyDeclaration
3479
- ? {
3480
- module: familyDeclaration.module,
3481
- exportPath: `${familyDeclaration.exportPath}.${key}`,
3482
- ...(familyDeclaration.revision !== undefined
3483
- ? { revision: familyDeclaration.revision }
3484
- : {}),
3485
- }
3604
+ ? resolveFamily(
3605
+ `${familyDeclaration.module}.${familyDeclaration.exportPath}.${key}`,
3606
+ { span: origin },
3607
+ false,
3608
+ )
3486
3609
  : undefined;
3487
3610
  addInstrument(
3488
3611
  child,
@@ -3504,7 +3627,7 @@ export function compile(
3504
3627
  .join("."),
3505
3628
  }
3506
3629
  : undefined,
3507
- tunables,
3630
+ terms,
3508
3631
  );
3509
3632
  }
3510
3633
  };
@@ -3554,14 +3677,7 @@ export function compile(
3554
3677
  }
3555
3678
  const attachmentName = match[1]!;
3556
3679
  const targetTemplate = match[2]!;
3557
- const template = templates.get(targetTemplate);
3558
- if (!template) {
3559
- fail(
3560
- entry,
3561
- `unknown instrument ${targetTemplate}`,
3562
- `add use ${targetTemplate.split(".")[0]} and choose a declared instrument`,
3563
- );
3564
- }
3680
+ const template = requireTemplate(targetTemplate, entry);
3565
3681
  const instId = `${decl.name}_${attachmentName}`;
3566
3682
 
3567
3683
  const attachmentBlock = asBlock(entry.value);
@@ -3576,11 +3692,15 @@ export function compile(
3576
3692
  economics.push(row);
3577
3693
  } else if (row.key === "rename") {
3578
3694
  for (const r of asBlock(row.value).entries) {
3579
- if (renames.has(r.key))
3580
- fail(
3695
+ const first = renameEntries.get(r.key);
3696
+ if (first)
3697
+ conflict(
3698
+ texts,
3699
+ `rename ${r.key} is given twice`,
3700
+ first,
3581
3701
  r,
3582
- `duplicate rename ${r.key}`,
3583
3702
  "rename each subject field once",
3703
+ { values: [first.value, r.value] },
3584
3704
  );
3585
3705
  renames.set(r.key, text(r.value));
3586
3706
  renameEntries.set(r.key, r);
@@ -3589,16 +3709,28 @@ export function compile(
3589
3709
  if (row.value.kind === "call") {
3590
3710
  const actionName = row.value.name;
3591
3711
  const publicName = text(row.value.args[0]!);
3712
+ const first = exposures.find(
3713
+ (exposure) =>
3714
+ exposure.instrument === instId &&
3715
+ exposure.action === actionName,
3716
+ );
3717
+ if (first)
3718
+ conflict(
3719
+ texts,
3720
+ `${actionName} is exposed twice`,
3721
+ first.entry,
3722
+ row,
3723
+ "expose each action once",
3724
+ );
3592
3725
  if (
3593
- exposed.has(actionName) ||
3594
3726
  !template.body.entries.some(
3595
3727
  (entry) => entry.key === `action ${actionName}`,
3596
3728
  )
3597
3729
  )
3598
3730
  fail(
3599
3731
  row,
3600
- `unknown or repeated action ${actionName}`,
3601
- "expose one declared action once",
3732
+ `unknown action ${actionName}`,
3733
+ "expose one declared action",
3602
3734
  );
3603
3735
  exposed.set(actionName, publicName);
3604
3736
  exposures.push({
@@ -3642,7 +3774,7 @@ export function compile(
3642
3774
  economics,
3643
3775
  template: targetTemplate,
3644
3776
  },
3645
- templateFamily ? { ...templateFamily } : undefined,
3777
+ templateFamily,
3646
3778
  standardOrigins.get(template),
3647
3779
  );
3648
3780
  } catch (error) {
@@ -3828,13 +3960,7 @@ export function compile(
3828
3960
  addInstrument(decl, decl.name, emptyBlock, decl.span);
3829
3961
  }
3830
3962
  if (decl.kind === "assignment") {
3831
- const template = templates.get(decl.target);
3832
- if (!template)
3833
- fail(
3834
- decl,
3835
- `unknown instrument ${decl.target}`,
3836
- `add use ${decl.target.split(".")[0]} and choose a declared instrument`,
3837
- );
3963
+ const template = requireTemplate(decl.target, decl);
3838
3964
  const templateFamily = resolveFamily(decl.target, decl, false);
3839
3965
  addInstrument(
3840
3966
  template,
@@ -3844,7 +3970,7 @@ export function compile(
3844
3970
  new Map(),
3845
3971
  new Map(),
3846
3972
  undefined,
3847
- templateFamily ? { ...templateFamily } : undefined,
3973
+ templateFamily,
3848
3974
  standardOrigins.get(template),
3849
3975
  );
3850
3976
  }
@@ -3877,10 +4003,12 @@ export function compile(
3877
4003
  if (
3878
4004
  family &&
3879
4005
  families.every(
3880
- (item) => canonicalJson(item) === canonicalJson(family),
4006
+ (item) =>
4007
+ item !== undefined &&
4008
+ canonicalJson(item) === canonicalJson(family),
3881
4009
  )
3882
4010
  )
3883
- selection.family = family;
4011
+ selection.family = family as UdlFamily;
3884
4012
  }
3885
4013
  for (const child of Object.values(record)) retainSelectionFamilies(child);
3886
4014
  };
@@ -4251,7 +4379,7 @@ export function compile(
4251
4379
  const declaredMembers = (selection: UdlSelection) => {
4252
4380
  const members = [selection.instrument].flat();
4253
4381
  const header = selection.family
4254
- ? templates.get(
4382
+ ? templateAt(
4255
4383
  `${selection.family.module}.${selection.family.exportPath}`,
4256
4384
  )
4257
4385
  : undefined;
@@ -4580,48 +4708,63 @@ export function compile(
4580
4708
  `action ${exposure.action} is excluded by these bindings`,
4581
4709
  "expose an action available with these tunables",
4582
4710
  );
4583
- const changed = new Set<string>();
4711
+ const changed = new Map<string, Site>();
4584
4712
  for (const decl of program.decls)
4585
- if (decl.kind === "hide" || decl.kind === "expose") {
4713
+ if (decl.kind === "expose") {
4586
4714
  const parts = decl.target.split(".");
4587
4715
  const key = parts.pop();
4588
4716
  const id = parts.join("_");
4589
4717
  const action = document.instruments.find((i) => i.id === id)?.actions[
4590
4718
  key ?? ""
4591
4719
  ];
4592
- if (!action || changed.has(decl.target))
4720
+ const first = changed.get(decl.target);
4721
+ if (first)
4722
+ conflict(
4723
+ texts,
4724
+ `${decl.target} is exposed twice`,
4725
+ first,
4726
+ decl,
4727
+ "expose each action once",
4728
+ );
4729
+ if (!action)
4593
4730
  fail(
4594
4731
  decl,
4595
- `unknown or repeated action ${decl.target}`,
4596
- "name one declared object.action once",
4732
+ `unknown action ${decl.target}`,
4733
+ "name one declared object.action",
4597
4734
  );
4598
- changed.add(decl.target);
4599
- if (decl.kind === "hide") delete action!.publicAction;
4600
- else if (automatic(action!.actor))
4735
+ changed.set(decl.target, decl);
4736
+ if (automatic(action.actor))
4601
4737
  fail(
4602
4738
  decl,
4603
4739
  `${decl.target} runs on the clock or its parent, not a caller`,
4604
4740
  "expose only caller actions",
4605
4741
  );
4606
- else action!.publicAction = decl.name!;
4742
+ action.publicAction = decl.name;
4607
4743
  }
4744
+ // Two actions of one instrument cannot share a public name.
4745
+ const published = new Map<string, Site>();
4608
4746
  for (const decl of program.decls) {
4609
4747
  if (decl.kind !== "expose") continue;
4610
- const parts = decl.target.split(".");
4611
- parts.pop();
4612
- const instrument = document.instruments.find(
4613
- (item) => item.id === parts.join("_"),
4614
- )!;
4615
- if (
4616
- Object.values(instrument.actions).filter(
4617
- (action) => action.publicAction === decl.name,
4618
- ).length > 1
4619
- )
4620
- fail(
4748
+ const instrument = decl.target.split(".").slice(0, -1).join("_");
4749
+ const key = `${instrument} ${decl.name}`;
4750
+ const first =
4751
+ published.get(key) ??
4752
+ exposures.find(
4753
+ ({ instrument: id, action, entry }) =>
4754
+ id === instrument &&
4755
+ `${decl.target}`.split(".").pop() !== action &&
4756
+ entry.value.kind === "call" &&
4757
+ text(entry.value.args[0]!) === decl.name,
4758
+ )?.entry;
4759
+ if (first)
4760
+ conflict(
4761
+ texts,
4762
+ `public action ${decl.name} is given twice on ${instrument}`,
4763
+ first,
4621
4764
  decl,
4622
- `public action ${decl.name} is used more than once on ${parts.join(".")}`,
4623
4765
  "give each exposed action a distinct public name",
4624
4766
  );
4767
+ published.set(key, decl);
4625
4768
  }
4626
4769
  // Every attachment of an object shares one public action namespace, so
4627
4770
  // two attachments that both `expose create` collide.
@@ -4836,6 +4979,7 @@ export function compile(
4836
4979
  diagnostic(d, "check"),
4837
4980
  ),
4838
4981
  };
4982
+ const checks = lowerChecks(program.checks, validated.value, literal);
4839
4983
  return {
4840
4984
  verdict: "valid",
4841
4985
  diagnostics: [],
@@ -4847,6 +4991,7 @@ export function compile(
4847
4991
  document: validated.value,
4848
4992
  costManifest: buildUdlCostManifest(validated.value),
4849
4993
  originMap: origins,
4994
+ checks,
4850
4995
  },
4851
4996
  };
4852
4997
  } catch (error) {