@hyperscale0/hsx 1.0.341 → 1.0.347

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 (128) 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/src/ast.d.ts +57 -7
  14. package/dist/src/ast.d.ts.map +1 -1
  15. package/dist/src/ast.js.map +1 -1
  16. package/dist/src/bases-bundle.js +11 -11
  17. package/dist/src/bases-bundle.js.map +1 -1
  18. package/dist/src/catalog-program.d.ts +1 -1
  19. package/dist/src/catalog-program.d.ts.map +1 -1
  20. package/dist/src/catalog-program.js +0 -13
  21. package/dist/src/catalog-program.js.map +1 -1
  22. package/dist/src/checks.d.ts +60 -0
  23. package/dist/src/checks.d.ts.map +1 -0
  24. package/dist/src/checks.js +240 -0
  25. package/dist/src/checks.js.map +1 -0
  26. package/dist/src/cli.d.ts.map +1 -1
  27. package/dist/src/cli.js +6 -25
  28. package/dist/src/cli.js.map +1 -1
  29. package/dist/src/compile.d.ts +29 -12
  30. package/dist/src/compile.d.ts.map +1 -1
  31. package/dist/src/compile.js +414 -316
  32. package/dist/src/compile.js.map +1 -1
  33. package/dist/src/diagnostics.d.ts +14 -1
  34. package/dist/src/diagnostics.d.ts.map +1 -1
  35. package/dist/src/diagnostics.js +31 -0
  36. package/dist/src/diagnostics.js.map +1 -1
  37. package/dist/src/examples-bundle.js +15 -15
  38. package/dist/src/examples-bundle.js.map +1 -1
  39. package/dist/src/header-source.d.ts +30 -0
  40. package/dist/src/header-source.d.ts.map +1 -1
  41. package/dist/src/header-source.js +120 -9
  42. package/dist/src/header-source.js.map +1 -1
  43. package/dist/src/headers.d.ts +9 -18
  44. package/dist/src/headers.d.ts.map +1 -1
  45. package/dist/src/headers.js +25 -23
  46. package/dist/src/headers.js.map +1 -1
  47. package/dist/src/index.d.ts +4 -3
  48. package/dist/src/index.d.ts.map +1 -1
  49. package/dist/src/index.js +2 -2
  50. package/dist/src/index.js.map +1 -1
  51. package/dist/src/keywords.d.ts +2 -4
  52. package/dist/src/keywords.d.ts.map +1 -1
  53. package/dist/src/keywords.js +6 -5
  54. package/dist/src/keywords.js.map +1 -1
  55. package/dist/src/language.d.ts +3 -4
  56. package/dist/src/language.d.ts.map +1 -1
  57. package/dist/src/language.js +123 -60
  58. package/dist/src/language.js.map +1 -1
  59. package/dist/src/lex.d.ts +1 -1
  60. package/dist/src/lex.d.ts.map +1 -1
  61. package/dist/src/lex.js +3 -5
  62. package/dist/src/lex.js.map +1 -1
  63. package/dist/src/parse.d.ts +3 -1
  64. package/dist/src/parse.d.ts.map +1 -1
  65. package/dist/src/parse.js +234 -22
  66. package/dist/src/parse.js.map +1 -1
  67. package/dist/src/std-bundle.d.ts +2 -1
  68. package/dist/src/std-bundle.d.ts.map +1 -1
  69. package/dist/src/std-bundle.js +16 -15
  70. package/dist/src/std-bundle.js.map +1 -1
  71. package/dist/src/template-guide.d.ts +3 -9
  72. package/dist/src/template-guide.d.ts.map +1 -1
  73. package/dist/src/template-guide.js +16 -23
  74. package/dist/src/template-guide.js.map +1 -1
  75. package/dist/src/toolchain.d.ts +23 -0
  76. package/dist/src/toolchain.d.ts.map +1 -0
  77. package/dist/src/toolchain.js +29 -0
  78. package/dist/src/toolchain.js.map +1 -0
  79. package/docs/README.md +69 -28
  80. package/examples/camera-rental.hsx +0 -1
  81. package/examples/cards.hsx +0 -1
  82. package/examples/collections.hsx +0 -2
  83. package/examples/escrow.hsx +0 -1
  84. package/examples/financing.hsx +0 -1
  85. package/examples/insurance.hsx +0 -2
  86. package/examples/lending.hsx +0 -3
  87. package/examples/library.hsx +0 -3
  88. package/examples/marketplace.hsx +0 -2
  89. package/examples/money.hsx +0 -1
  90. package/examples/rental-hire.hsx +0 -1
  91. package/examples/reporting.hsx +0 -2
  92. package/examples/savings.hsx +0 -1
  93. package/examples/serviced.hsx +0 -4
  94. package/examples/wallet.hsx +0 -1
  95. package/package.json +4 -4
  96. package/src/ast.ts +56 -3
  97. package/src/bases-bundle.ts +11 -11
  98. package/src/catalog-program.ts +0 -13
  99. package/src/checks.ts +386 -0
  100. package/src/cli.ts +6 -25
  101. package/src/compile.ts +583 -438
  102. package/src/diagnostics.ts +49 -1
  103. package/src/examples-bundle.ts +15 -15
  104. package/src/header-source.ts +187 -11
  105. package/src/headers.ts +31 -32
  106. package/src/index.ts +10 -4
  107. package/src/keywords.ts +6 -5
  108. package/src/language.ts +153 -68
  109. package/src/lex.ts +3 -5
  110. package/src/parse.ts +216 -24
  111. package/src/std-bundle.ts +17 -15
  112. package/src/template-guide.ts +17 -34
  113. package/src/toolchain.ts +51 -0
  114. package/std/booking.hsx +14 -1
  115. package/std/collections.hsx +12 -2
  116. package/std/financing.hsx +10 -16
  117. package/std/money.hsx +2 -3
  118. package/std/reporting.hsx +10 -1
  119. package/dist/src/binding-contract.d.ts +0 -22
  120. package/dist/src/binding-contract.d.ts.map +0 -1
  121. package/dist/src/binding-contract.js +0 -80
  122. package/dist/src/binding-contract.js.map +0 -1
  123. package/dist/src/std-library.d.ts +0 -5
  124. package/dist/src/std-library.d.ts.map +0 -1
  125. package/dist/src/std-library.js +0 -5
  126. package/dist/src/std-library.js.map +0 -1
  127. package/src/binding-contract.ts +0 -111
  128. package/src/std-library.ts +0 -7
@@ -1,14 +1,13 @@
1
- import { CompileFailure, didYouMean, fail, failWithCode, } from "./diagnostics.js";
2
- import { hash as sha256 } from "fast-sha256";
1
+ import { CompileFailure, conflict, didYouMean, fail, failWithCode, } from "./diagnostics.js";
3
2
  import { buildUdlCostManifest } from "./cost.js";
4
- import { validateUdl, resolveField, sameAccount, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, UDL_FORMAT_VERSION, udlObjectFieldSchema, udlFieldValueSchema, udlInstrumentSchema, } from "@hyperscale0/udl";
5
- import { bindingDependencies, parameterDiagnostics, BindingContractError, } from "./binding-contract.js";
3
+ import { canonicalJson, digest, validateUdl, resolveField, sameAccount, sameFamily, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, UDL_FORMAT_VERSION, udlObjectFieldSchema, udlFieldValueSchema, udlInstrumentSchema, } from "@hyperscale0/udl";
6
4
  import { tunableBounds } from "./tunables.js";
7
- import { parseProgram } from "./parse.js";
5
+ import { lowerChecks } from "./checks.js";
6
+ import { parseExpression, parseProgram } from "./parse.js";
8
7
  import { applyAttachmentEconomics, purposeWarnings, samePartyMoves, unboundEconomicSources, } from "./attachment-economics.js";
9
- import { parseHeader } from "./header-source.js";
8
+ import { instrumentChecks, parseHeader } from "./header-source.js";
10
9
  import { lineColAt, } from "./ast.js";
11
- import { bundledStandardLibrary } from "./std-library.js";
10
+ import { defaultToolchain, } from "./toolchain.js";
12
11
  function fieldType(row) {
13
12
  const type = row.value.kind === "default" ? row.value.type : row.value;
14
13
  if (type.kind !== "call" || type.name !== "sensitive")
@@ -112,25 +111,11 @@ function lowerObjectField(row, resolveExpr = (e) => e) {
112
111
  fail(row, result.error.message, "use a UDL object field type and its constraints");
113
112
  return result.data;
114
113
  }
115
- function canonicalJson(value) {
116
- if (value === null || typeof value !== "object") {
117
- return JSON.stringify(value);
118
- }
119
- if (Array.isArray(value)) {
120
- return `[${value.map((item) => (item === undefined ? "null" : canonicalJson(item))).join(",")}]`;
121
- }
122
- const record = value;
123
- const keys = Object.keys(record)
124
- .filter((k) => record[k] !== undefined)
125
- .sort();
126
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
127
- }
128
114
  const emptyBlock = {
129
115
  kind: "block",
130
116
  entries: [],
131
117
  span: { start: 0, end: 0 },
132
118
  };
133
- const camel = (name) => name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
134
119
  /** Clock and parent actors run without a caller, so they have no public name. */
135
120
  const automatic = (actor) => actor === "clock" ||
136
121
  (actor !== null && typeof actor === "object" && "parent" in actor);
@@ -153,32 +138,52 @@ function namedParties(instrument) {
153
138
  }
154
139
  return names;
155
140
  }
156
- function entries(block) {
141
+ const MERGE = {
142
+ requires: "concat",
143
+ moves: "concat",
144
+ invariants: "concat",
145
+ invoke: "concat",
146
+ calculate: "concat",
147
+ expose: "concat",
148
+ };
149
+ // The sources of the compile in progress, so a conflict prints line:column.
150
+ let texts = new Map();
151
+ /** An expression with spans, sources and comments dropped. */
152
+ const normal = (value) => Array.isArray(value)
153
+ ? value.map(normal)
154
+ : value && typeof value === "object"
155
+ ? Object.fromEntries(Object.entries(value).flatMap(([key, item]) => key === "span" || key === "source" || item === undefined
156
+ ? []
157
+ : [[key, normal(item)]]))
158
+ : value;
159
+ function given(block, merge = (key) => MERGE[key] ?? "unique") {
157
160
  const map = new Map();
158
161
  for (const row of block.entries) {
159
162
  const previous = map.get(row.key);
160
- if (previous) {
161
- if (![
162
- "requires",
163
- "moves",
164
- "invariants",
165
- "invoke",
166
- "calculate",
167
- "expose",
168
- ].includes(row.key))
169
- fail(row, `duplicate ${row.key}`, "keep one value for this name");
163
+ const rule = merge(row.key);
164
+ if (!previous)
165
+ map.set(row.key, row);
166
+ else if (rule === "concat") {
170
167
  const items = (expr) => expr.kind === "list" ? expr.items : [expr];
171
168
  map.set(row.key, {
172
- kind: "list",
173
- items: [...items(previous), ...items(row.value)],
174
- span: row.span,
169
+ ...row,
170
+ value: {
171
+ kind: "list",
172
+ items: [...items(previous.value), ...items(row.value)],
173
+ span: row.span,
174
+ },
175
175
  });
176
176
  }
177
- else
178
- map.set(row.key, row.value);
177
+ else if (rule === "unique" ||
178
+ JSON.stringify(normal(previous.value)) !==
179
+ JSON.stringify(normal(row.value)))
180
+ conflict(texts, `${row.key} is ${rule === "equal" ? "set" : "given"} twice`, previous, row, "keep one", rule === "equal" ? { values: [previous.value, row.value] } : {});
179
181
  }
180
182
  return map;
181
183
  }
184
+ function entries(block) {
185
+ return new Map([...given(block)].map(([key, row]) => [key, row.value]));
186
+ }
182
187
  function asBlock(expr) {
183
188
  if (!expr)
184
189
  return emptyBlock;
@@ -287,9 +292,21 @@ function literal(expr, expectedType) {
287
292
  fail(expr, "this value is fixed when the program compiles, so it cannot be runtime", "write a literal value");
288
293
  return fail(expr, "expected a literal", "write a typed constant");
289
294
  }
290
- export function compile(source, options = {}) {
295
+ /** A std declaration's identity: its AST with spans, sources and comments dropped. */
296
+ function declarationDigest(decl) {
297
+ return digest(normal(decl));
298
+ }
299
+ /**
300
+ * An override compiles the program as if one std term held other text, so a
301
+ * plan can see what that term alone moves.
302
+ */
303
+ export function compile(source, toolchain = defaultToolchain, override) {
291
304
  const parsed = parseProgram(source);
292
- const sources = new Map([["program", source]]);
305
+ const sources = new Map([
306
+ ["program", source],
307
+ ...(override ? [["override", override.text]] : []),
308
+ ]);
309
+ texts = sources;
293
310
  const diagnostic = (d, stage) => {
294
311
  const text = sources.get(d.source ?? "program") ?? source;
295
312
  const end = lineColAt(text, d.span.end);
@@ -316,8 +333,8 @@ export function compile(source, options = {}) {
316
333
  const program = parsed.program;
317
334
  const bindingDiagnostics = [];
318
335
  const adapterTarget = (binding) => {
319
- const registry = options.adapterRegistry;
320
- if (!registry || !Object.hasOwn(registry, binding))
336
+ const registry = toolchain.adapters;
337
+ if (!Object.hasOwn(registry, binding))
321
338
  return;
322
339
  const target = registry[binding];
323
340
  return target &&
@@ -335,55 +352,56 @@ export function compile(source, options = {}) {
335
352
  const attachedSelectors = new Map();
336
353
  const attachEntries = new Map();
337
354
  const declarationSources = new Map();
355
+ // Declaration digest by std path, such as financing.installments.payment.
356
+ const familyDigests = new Map();
338
357
  const standardOrigins = new Map();
339
358
  const requirementOrigins = new Map();
340
359
  // Instruments that lost a std action to an empty all() parent. Their
341
360
  // lifecycles drop the states only that action reached.
342
361
  const trimmed = new Set();
343
- const used = new Set();
344
- for (const use of program.decls.filter((d) => d.kind === "use")) {
345
- if (used.has(use.name))
346
- fail(use, `header ${use.name} is used twice`, "remove the duplicate use line");
347
- used.add(use.name);
348
- const content = (options.standardLibrary ?? bundledStandardLibrary).source(use.name);
349
- if (!content)
350
- fail(use, `unknown header ${use.name}`, "choose a published header");
351
- sources.set(use.name, content);
352
- const bundled = bundledStandardLibrary.source(use.name);
353
- const headerDigest = bundled === content
354
- ? Array.from(sha256(new TextEncoder().encode(content)), (byte) => byte.toString(16).padStart(2, "0")).join("")
355
- : undefined;
362
+ // A qualified name such as booking.reservation loads its header the first
363
+ // time the compile reaches it. A header nothing names never loads.
364
+ const loaded = new Set();
365
+ const loadHeader = (name, site) => {
366
+ if (loaded.has(name))
367
+ return true;
368
+ const content = toolchain.std.get(name);
369
+ if (content === undefined)
370
+ return false;
371
+ loaded.add(name);
372
+ sources.set(name, content);
356
373
  let header;
357
374
  try {
358
- header = parseHeader(content, use.name);
375
+ header = parseHeader(content, name);
359
376
  }
360
377
  catch (error) {
361
- if (!(error instanceof CompileFailure))
378
+ if (!(error instanceof CompileFailure) || !site)
362
379
  throw error;
363
380
  throw new CompileFailure({
364
381
  ...error.diagnostic,
365
382
  related: [
366
383
  {
367
- source: "program",
368
- span: use.span,
369
- message: `Imported header ${use.name}`,
384
+ source: site.source ?? "program",
385
+ span: site.span,
386
+ message: `Header ${name} is named here`,
370
387
  },
371
388
  ],
372
389
  });
373
390
  }
374
391
  const registerTemplates = (parentDecl, prefix) => {
375
- if (templates.has(prefix))
376
- fail(parentDecl, `duplicate instrument ${prefix}`, "give each instrument a distinct name");
392
+ const first = templates.get(prefix);
393
+ if (first)
394
+ conflict(texts, `instrument ${prefix} is declared twice`, first, parentDecl, "give each instrument a distinct name");
377
395
  templates.set(prefix, parentDecl);
378
- declarationSources.set(parentDecl, use.name);
379
- if (headerDigest) {
380
- const [, root, ...record] = prefix.split(".");
381
- standardOrigins.set(parentDecl, {
382
- key: `${use.name}.${root}`,
383
- recordPath: record.join("."),
384
- headerDigest,
385
- });
386
- }
396
+ declarationSources.set(parentDecl, name);
397
+ const digest = declarationDigest(parentDecl);
398
+ familyDigests.set(prefix, digest);
399
+ const [, root, ...record] = prefix.split(".");
400
+ standardOrigins.set(parentDecl, {
401
+ key: `${name}.${root}`,
402
+ recordPath: record.join("."),
403
+ digest,
404
+ });
387
405
  const recs = entries(asBlock(entries(parentDecl.body).get("records")));
388
406
  for (const [recName, recBlock] of recs) {
389
407
  const recDecl = {
@@ -397,10 +415,36 @@ export function compile(source, options = {}) {
397
415
  }
398
416
  };
399
417
  for (const decl of header.decls)
400
- if (decl.kind === "instrument") {
401
- registerTemplates(decl, `${use.name}.${decl.name}`);
402
- }
403
- }
418
+ if (decl.kind === "instrument")
419
+ registerTemplates(decl, `${name}.${decl.name}`);
420
+ return true;
421
+ };
422
+ const loadPath = (path, site) => {
423
+ const dot = path.indexOf(".");
424
+ return dot > 0 && loadHeader(path.slice(0, dot), site);
425
+ };
426
+ const templateAt = (path, site) => {
427
+ loadPath(path, site);
428
+ return templates.get(path);
429
+ };
430
+ // The template a qualified name names, or the reason it names none.
431
+ const requireTemplate = (path, site) => {
432
+ const found = templateAt(path, site);
433
+ if (found)
434
+ return found;
435
+ const root = path.split(".")[0];
436
+ const unknown = (noun, name, candidates) => {
437
+ const hint = didYouMean(name, candidates);
438
+ return hint
439
+ ? `unknown ${noun} ${name}.${hint}`
440
+ : `unknown ${noun} ${name}`;
441
+ };
442
+ if (path.includes(".") && !toolchain.std.has(root))
443
+ fail(site, unknown("header", root, [...toolchain.std.keys()]), "name a published header");
444
+ return fail(site, unknown("instrument", path, [...templates.keys()].filter((name) => name.startsWith(`${root}.`))), path.includes(".")
445
+ ? `choose an instrument header ${root} declares`
446
+ : "choose a declared instrument");
447
+ };
404
448
  for (const decl of program.decls)
405
449
  if (decl.kind === "instrument")
406
450
  templates.set(decl.name, decl);
@@ -420,18 +464,19 @@ export function compile(source, options = {}) {
420
464
  const objects = new Map();
421
465
  const assignments = new Map();
422
466
  const attachmentSubjects = new Map();
423
- const names = new Set();
467
+ const names = new Map();
424
468
  // A record's create turns public only through a program-level expose,
425
469
  // which applies after lowering, so creation input needs the list first.
426
470
  const exposedCreates = new Set(program.decls.flatMap((decl) => decl.kind === "expose" && decl.target.endsWith(".create")
427
471
  ? [decl.target.split(".").slice(0, -1).join("_")]
428
472
  : []));
429
473
  for (const decl of program.decls) {
430
- if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
474
+ if (decl.kind === "expose")
431
475
  continue;
432
- if (names.has(decl.name))
433
- fail(decl, `duplicate declaration ${decl.name}`, "give this declaration a distinct name");
434
- names.add(decl.name);
476
+ const first = names.get(decl.name);
477
+ if (first)
478
+ conflict(texts, `${decl.name} is declared twice`, first, decl, "give this declaration a distinct name");
479
+ names.set(decl.name, decl);
435
480
  if (decl.kind === "object") {
436
481
  objects.set(decl.name, decl);
437
482
  for (const entry of decl.body.entries) {
@@ -477,51 +522,22 @@ export function compile(source, options = {}) {
477
522
  return undefined;
478
523
  failWithCode(expr, "HSX1001", `invalid family ${rawPath}`, "use module.instrument or module.instrument.record");
479
524
  }
480
- const moduleName = parts[0];
481
- const exportPath = parts.slice(1).join(".");
482
- const targetTemplate = templates.get(rawPath);
483
- if (!targetTemplate) {
525
+ loadPath(rawPath, expr);
526
+ const digest = familyDigests.get(rawPath);
527
+ if (!digest) {
484
528
  if (!required)
485
529
  return undefined;
486
530
  failWithCode(expr, "HSX1001", `unknown family declaration ${rawPath}`, "choose a declared standard instrument");
487
531
  }
488
- const topTemplate = templates.get(`${moduleName}.${parts[1]}`);
489
- if (!topTemplate) {
490
- if (!required)
491
- return undefined;
492
- failWithCode(expr, "HSX1001", `unknown family declaration ${moduleName}.${parts[1]}`, "choose a declared standard instrument");
493
- }
494
- const topBody = entries(topTemplate.body);
495
- let revision;
496
- if (topBody.has("familyRevision")) {
497
- const val = literal(topBody.get("familyRevision"));
498
- if (typeof val === "number")
499
- revision = val;
500
- }
501
- let currentBody = topTemplate.body;
502
- for (const recName of parts.slice(2)) {
503
- const recs = entries(asBlock(entries(currentBody).get("records")));
504
- const child = recs.get(recName);
505
- if (child) {
506
- currentBody = asBlock(child);
507
- const cBody = entries(currentBody);
508
- if (cBody.has("familyRevision")) {
509
- const val = literal(cBody.get("familyRevision"));
510
- if (typeof val === "number")
511
- revision = val;
512
- }
513
- }
514
- }
515
- if (revision === undefined) {
516
- if (!required)
517
- return undefined;
518
- failWithCode(expr, "HSX1001", `declaration ${rawPath} has no familyRevision declared`, "declare familyRevision on the standard instrument");
519
- }
520
- return { module: moduleName, exportPath, revision };
532
+ return {
533
+ module: parts[0],
534
+ exportPath: parts.slice(1).join("."),
535
+ digest,
536
+ };
521
537
  };
522
538
  const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedEnums = new Map(), attachmentInfo, familyDeclaration, standardOrigin,
523
- /** Declared tunables, true where the author bound them. */
524
- inheritedTunables = new Map()) => {
539
+ /** A child record's terms are its parent's. */
540
+ inheritedTerms = new Map()) => {
525
541
  const resolveFamilyInstruments = (family, currentInstId, expr) => {
526
542
  const found = new Set();
527
543
  if (currentInstId && familyDeclaration) {
@@ -551,7 +567,8 @@ export function compile(source, options = {}) {
551
567
  if (inst.family &&
552
568
  inst.family.module === family.module &&
553
569
  inst.family.exportPath === family.exportPath &&
554
- inst.family.revision === family.revision) {
570
+ "digest" in inst.family &&
571
+ inst.family.digest === family.digest) {
555
572
  found.add(inst.id);
556
573
  }
557
574
  }
@@ -593,6 +610,7 @@ export function compile(source, options = {}) {
593
610
  };
594
611
  const getInstrumentFamily = (targetId) => {
595
612
  const existing = document.instruments.find((i) => i.id === targetId);
613
+ // This document is format 2, so every family it holds is a digest.
596
614
  if (existing?.family)
597
615
  return existing.family;
598
616
  if (targetId.startsWith(`${id}_`) && familyDeclaration) {
@@ -604,7 +622,7 @@ export function compile(source, options = {}) {
604
622
  }
605
623
  for (const [asgnName, asgn] of assignments) {
606
624
  if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
607
- const tmpl = templates.get(asgn.target);
625
+ const tmpl = templateAt(asgn.target);
608
626
  if (!tmpl)
609
627
  continue;
610
628
  const mod = declarationSources.get(tmpl);
@@ -630,11 +648,8 @@ export function compile(source, options = {}) {
630
648
  if (typeof tid !== "string")
631
649
  fail(expr, "instrument target needs a name", "name a declared instrument or a list of instruments");
632
650
  const fam = getInstrumentFamily(tid);
633
- if (!fam ||
634
- fam.module !== expectedFamily.module ||
635
- fam.exportPath !== expectedFamily.exportPath ||
636
- fam.revision !== expectedFamily.revision) {
637
- failWithCode(expr, "HSX1001", `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`, "ensure target instrument matches the declared family");
651
+ if (!sameFamily(fam, expectedFamily)) {
652
+ failWithCode(expr, "HSX1001", `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath}`, "ensure target instrument matches the declared family");
638
653
  }
639
654
  }
640
655
  };
@@ -646,22 +661,22 @@ export function compile(source, options = {}) {
646
661
  if (type.kind === "call" && type.name === "enum")
647
662
  enums.set(parameter.key, type.args.map(text));
648
663
  }
649
- const supplied = new Map();
650
- const suppliedEntries = new Map();
651
- for (const entry of arguments_.entries) {
652
- if (supplied.has(entry.key))
653
- fail(entry, `duplicate tunable ${entry.key}`, "supply each parameter once");
654
- supplied.set(entry.key, entry.value);
655
- suppliedEntries.set(entry.key, entry);
656
- }
664
+ const suppliedEntries = given(arguments_, () => "equal");
665
+ const supplied = new Map([...suppliedEntries].map(([key, entry]) => [key, entry.value]));
657
666
  const environment = new Map(inherited);
658
667
  // `when every is set` asks whether the author bound a tunable; a
659
668
  // default leaves it unset.
660
- const tunables = new Map(inheritedTunables);
669
+ const terms = new Map(inheritedTerms);
670
+ const isSet = (key) => (terms.get(key)?.from ?? "std") !== "std";
661
671
  const parameterErrors = [];
672
+ // A std default set back is the header as it was, so its digest too.
673
+ let revertedHeader;
662
674
  for (const param of decl.parameters) {
663
675
  const type = param.value.kind === "default" ? param.value.type : param.value;
664
- const fallback = param.value.kind === "default" ? param.value.value : undefined;
676
+ const overridden = override?.instrument === id && override.name === param.key;
677
+ const fallback = param.value.kind === "default" && !overridden
678
+ ? param.value.value
679
+ : undefined;
665
680
  const typeName = type.kind === "type" || type.kind === "call" ? type.name : text(type);
666
681
  const partyParameter = attachmentInfo && typeName === "party";
667
682
  const optional = (type.kind === "type" || type.kind === "call") && !!type.optional;
@@ -669,17 +684,65 @@ export function compile(source, options = {}) {
669
684
  // program party that shares its name does not switch it on.
670
685
  const byName = partyParameter &&
671
686
  !optional &&
687
+ !overridden &&
672
688
  (subjectPartyRoles.includes(param.key) ||
673
689
  document.parties[param.key])
674
690
  ? { kind: "name", value: param.key, span: origin }
675
691
  : undefined;
676
- tunables.set(param.key, supplied.has(param.key) || !!byName);
677
- const actual = supplied.get(param.key) ??
678
- byName ??
679
- (fallback && {
680
- ...fallback,
681
- source: declarationSources.get(decl) ?? "program",
682
- });
692
+ const header = declarationSources.get(decl) ?? "program";
693
+ const forced = !overridden || override.text === "unset"
694
+ ? undefined
695
+ : {
696
+ ...(parseExpression(override.text) ??
697
+ fail({ span: origin }, `${param.key} cannot hold ${override.text}`, "override with text the term once held")),
698
+ source: "override",
699
+ };
700
+ const written = overridden ? forced : supplied.get(param.key);
701
+ if (overridden && override.from === "std")
702
+ revertedHeader = {
703
+ ...decl,
704
+ parameters: decl.parameters.map((entry) => entry !== param
705
+ ? entry
706
+ : {
707
+ ...entry,
708
+ value: forced
709
+ ? {
710
+ kind: "default",
711
+ type,
712
+ value: forced,
713
+ span: entry.span,
714
+ }
715
+ : type,
716
+ }),
717
+ };
718
+ terms.set(param.key, overridden
719
+ ? {
720
+ ...(forced ? { value: forced } : {}),
721
+ from: override.from,
722
+ span: forced?.span ?? origin,
723
+ source: "override",
724
+ }
725
+ : written
726
+ ? {
727
+ value: written,
728
+ from: "program",
729
+ span: written.span,
730
+ source: "program",
731
+ }
732
+ : byName
733
+ ? {
734
+ value: byName,
735
+ from: "party",
736
+ span: origin,
737
+ source: "program",
738
+ }
739
+ : {
740
+ ...(fallback ? { value: fallback } : {}),
741
+ from: "std",
742
+ span: (fallback ?? param).span,
743
+ source: header,
744
+ });
745
+ const actual = written ?? byName ?? (fallback && { ...fallback, source: header });
683
746
  if (!actual) {
684
747
  if (optional)
685
748
  continue;
@@ -704,44 +767,22 @@ export function compile(source, options = {}) {
704
767
  }
705
768
  environment.set(param.key, actual);
706
769
  }
707
- let policyDiagnostics;
708
- let dependencies;
709
- try {
710
- policyDiagnostics = parameterDiagnostics(decl);
711
- dependencies = bindingDependencies(decl);
712
- }
713
- catch (error) {
714
- if (!(error instanceof BindingContractError))
715
- throw error;
716
- throw new CompileFailure({
717
- code: "HSX1001",
718
- message: error.message,
719
- fix: "Repair the header binding contract.",
720
- span: error.entry.span,
721
- source: declarationSources.get(decl) ?? "program",
722
- });
723
- }
724
- for (const dependency of dependencies) {
725
- const selected = environment.get(dependency.when);
726
- const holds = enums.has(dependency.when)
727
- ? selected?.kind === "name" && selected.value === dependency.is
728
- : tunables.get(dependency.when) === (dependency.is === "set");
729
- if (holds && !environment.has(dependency.binding)) {
730
- throw new CompileFailure({
731
- code: "HSX1001",
732
- message: dependency.message.replaceAll("{attachment}", attachmentInfo?.attachmentName ?? id),
733
- fix: dependency.fix,
734
- span: origin,
735
- related: [
736
- {
737
- source: declarationSources.get(decl) ?? "program",
738
- span: dependency.span,
739
- message: "Conditional binding requirement",
740
- },
741
- ],
742
- });
770
+ const checks = instrumentChecks(decl);
771
+ const say = (message) => message
772
+ .replaceAll("{attachment}", attachmentInfo?.attachmentName ?? id)
773
+ .replaceAll("{instrument}", assignments.get(id)?.target ?? decl.name);
774
+ const collect = (run) => {
775
+ try {
776
+ run();
743
777
  }
744
- }
778
+ catch (error) {
779
+ if (!(error instanceof CompileFailure))
780
+ throw error;
781
+ parameterErrors.push(error.diagnostic);
782
+ }
783
+ };
784
+ // Each tunable that failed its type rule, so no check compares it.
785
+ const broken = new Set();
745
786
  for (const key of supplied.keys()) {
746
787
  if (decl.parameters.some((p) => p.key === key))
747
788
  continue;
@@ -834,7 +875,7 @@ export function compile(source, options = {}) {
834
875
  const subject = attachmentSubjects.get(assignment.name);
835
876
  return `\`${subject}.${assignment.name.slice(subject.length + 1)}\``;
836
877
  });
837
- const target = templates.get(type);
878
+ const target = templateAt(type);
838
879
  const required = target?.parameters
839
880
  .filter((p) => p.value.kind !== "default" &&
840
881
  !((p.value.kind === "type" || p.value.kind === "call") &&
@@ -967,7 +1008,7 @@ export function compile(source, options = {}) {
967
1008
  fail(v, `${param.key} accepts one reference`, "use one object name");
968
1009
  if (!values.length || values.length > 16)
969
1010
  fail(v, "reference union needs 1 to 16 objects", "use at most 16 distinct object names");
970
- const seen = new Set();
1011
+ const seen = new Map();
971
1012
  for (const value of values) {
972
1013
  if (value.kind !== "name")
973
1014
  fail(value, "reference needs an object name", "name a declared object");
@@ -986,7 +1027,7 @@ export function compile(source, options = {}) {
986
1027
  if (!value.value.startsWith(`${instance}.`) &&
987
1028
  !value.value.startsWith(`${instance}_`))
988
1029
  continue;
989
- const template = templates.get(target);
1030
+ const template = templateAt(target);
990
1031
  if (!template)
991
1032
  continue;
992
1033
  const suffix = value.value
@@ -999,9 +1040,10 @@ export function compile(source, options = {}) {
999
1040
  if (!targetType ||
1000
1041
  (t.kind === "type" && t.target && targetType !== t.target))
1001
1042
  fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
1002
- if (seen.has(value.value))
1003
- fail(value, "duplicate reference", "list each object once");
1004
- seen.add(value.value);
1043
+ const first = seen.get(value.value);
1044
+ if (first)
1045
+ conflict(texts, `${param.key} lists ${value.value} twice`, first, value, "list each object once");
1046
+ seen.set(value.value, value);
1005
1047
  }
1006
1048
  // A many-reference tunable is a list even when one object is bound,
1007
1049
  // so report datasets and other value positions never see a bare name.
@@ -1049,11 +1091,8 @@ export function compile(source, options = {}) {
1049
1091
  : `set \`${param.key}: runtime\``;
1050
1092
  fail(v, `\`${param.key}\` cannot read \`${v.value}\` as a tunable. This position accepts a fixed money amount or a runtime amount.`, `For a varying deposit, ${advice} and add \`rename { ${param.key}: ${target} }\`. Use a literal only for a fixed charge.`);
1051
1093
  }
1052
- if (type === "money" && v.kind === "percent") {
1053
- const policy = policyDiagnostics.find((policy) => policy.parameter === param.key);
1054
- fail(v, `\`${assignments.get(id)?.target ?? decl.name}.${param.key}\` accepts ${policy?.accepts ?? "a fixed amount"}. It cannot express ${policy?.percentage ?? "a percentage-based charge"}.`, policy?.fix ??
1055
- "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.");
1056
- }
1094
+ if (type === "money" && v.kind === "percent")
1095
+ fail(v, `\`${assignments.get(id)?.target ?? decl.name}.${param.key}\` accepts a fixed amount. It cannot express a percentage-based charge.`, "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.");
1057
1096
  fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
1058
1097
  }
1059
1098
  try {
@@ -1093,91 +1132,137 @@ export function compile(source, options = {}) {
1093
1132
  span: party.span,
1094
1133
  message: `Declared party ${party.name}`,
1095
1134
  });
1096
- parameterErrors.push({ ...error.diagnostic, related });
1135
+ // A header's `x is <type>` check explains this tunable's type rule.
1136
+ const typed = checks.find(({ test }) => test.kind === "type" && test.tunable === param.key);
1137
+ broken.add(param.key);
1138
+ parameterErrors.push({
1139
+ ...error.diagnostic,
1140
+ ...(typed ? { message: say(typed.message), fix: typed.fix } : {}),
1141
+ related,
1142
+ });
1097
1143
  }
1098
1144
  }
1099
- if (parameterErrors.length) {
1100
- bindingDiagnostics.push(...parameterErrors.slice(1));
1101
- throw new CompileFailure(parameterErrors[0]);
1102
- }
1103
1145
  const body = entries(decl.body);
1104
1146
  // A ceiling caps a tunable by the value of an enum tunable, such as the
1105
1147
  // longest term per loan kind. An unset tunable takes the ceiling; a
1106
1148
  // supplied one may lower it, never raise it.
1107
- for (const ceiling of asBlock(body.get("ceilings")).entries) {
1108
- const table = entries(asBlock(ceiling.value));
1109
- const by = table.get("by");
1110
- const selected = by && environment.get(text(by));
1111
- if (!by || selected?.kind !== "name")
1112
- fail(ceiling, `ceiling ${ceiling.key} needs by: an enum tunable`, "write by: kind and one maximum per enum value");
1113
- const maximum = table.get(selected.value);
1114
- if (maximum?.kind !== "number")
1115
- fail(ceiling, `ceiling ${ceiling.key} has no maximum for ${selected.value}`, `add ${selected.value}: number to the ceiling`);
1116
- if (!tunables.get(ceiling.key)) {
1117
- environment.set(ceiling.key, { ...maximum, span: origin });
1118
- continue;
1119
- }
1120
- const supplied = environment.get(ceiling.key);
1121
- if (supplied.kind !== "number" ||
1122
- BigInt(supplied.value) > BigInt(maximum.value))
1123
- fail(supplied, `${ceiling.key} for ${selected.value} is at most ${maximum.value}`, `choose ${ceiling.key} of ${maximum.value} or less, or omit it`);
1124
- }
1125
- for (const constraint of asBlock(body.get("constraints")).entries) {
1126
- const rule = constraint.value;
1127
- if (rule.kind !== "call" ||
1128
- rule.args.length !== 1 ||
1129
- !["less_than", "at_most", "greater_than"].includes(rule.name))
1130
- fail(rule, "invalid tunable constraint", "use less_than(name), at_most(name), or greater_than(name)");
1131
- const left = environment.get(constraint.key), right = environment.get(text(rule.args[0]));
1132
- if (!left || !right)
1133
- fail(rule, "constraint needs two declared tunables", "name the compared tunables");
1134
- // A runtime value is checked when the agreement is created.
1135
- if ([left, right].some((expr) => expr.kind === "name" && expr.value === "runtime"))
1136
- continue;
1137
- const numericKind = (expr) => expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)
1138
- ? "duration"
1139
- : expr.kind;
1140
- if (!["money", "number", "percent", "duration"].includes(numericKind(left)) ||
1141
- numericKind(left) !== numericKind(right))
1142
- fail(rule, "constraint needs numeric tunables of the same type", "compare two amounts, integers, percentages or durations");
1143
- const a = BigInt(String(literal(left))), b = BigInt(String(literal(right)));
1144
- if (!(rule.name === "less_than"
1145
- ? a < b
1146
- : rule.name === "at_most"
1147
- ? a <= b
1148
- : a > b))
1149
- fail(left, `${constraint.key} must be ${rule.name} ${text(rule.args[0])}`, "choose values satisfying the header constraint");
1149
+ for (const ceiling of asBlock(body.get("ceilings")).entries)
1150
+ collect(() => {
1151
+ const table = entries(asBlock(ceiling.value));
1152
+ const by = table.get("by");
1153
+ const selected = by && environment.get(text(by));
1154
+ if (!by || selected?.kind !== "name")
1155
+ fail(ceiling, `ceiling ${ceiling.key} needs by: an enum tunable`, "write by: kind and one maximum per enum value");
1156
+ const maximum = table.get(selected.value);
1157
+ if (maximum?.kind !== "number")
1158
+ fail(ceiling, `ceiling ${ceiling.key} has no maximum for ${selected.value}`, `add ${selected.value}: number to the ceiling`);
1159
+ if (!isSet(ceiling.key)) {
1160
+ const term = terms.get(ceiling.key);
1161
+ if (term)
1162
+ terms.set(ceiling.key, {
1163
+ ...term,
1164
+ value: maximum,
1165
+ span: maximum.span,
1166
+ });
1167
+ environment.set(ceiling.key, { ...maximum, span: origin });
1168
+ return;
1169
+ }
1170
+ const supplied = environment.get(ceiling.key);
1171
+ if (supplied.kind !== "number" ||
1172
+ BigInt(supplied.value) > BigInt(maximum.value))
1173
+ fail(supplied, `${ceiling.key} for ${selected.value} is at most ${maximum.value}`, `choose ${ceiling.key} of ${maximum.value} or less, or omit it`);
1174
+ });
1175
+ // Every check runs, so one attach reports each broken rule at once.
1176
+ for (const check of checks)
1177
+ collect(() => {
1178
+ const { test, when } = check;
1179
+ if (when) {
1180
+ const selected = environment.get(when.selector);
1181
+ const holds = enums.has(when.selector)
1182
+ ? selected?.kind === "name" && selected.value === when.is
1183
+ : terms.has(when.selector) &&
1184
+ isSet(when.selector) === (when.is === "set");
1185
+ if (!holds)
1186
+ return;
1187
+ }
1188
+ if (test.kind === "type")
1189
+ return;
1190
+ let at;
1191
+ if (test.kind === "set") {
1192
+ if (isSet(test.tunable))
1193
+ return;
1194
+ }
1195
+ else {
1196
+ const terms = [...test.left, ...test.right];
1197
+ const value = (term) => term.kind === "name" ? environment.get(term.value) : term;
1198
+ // An unset or broken tunable has nothing to compare, and a
1199
+ // runtime value is checked when the agreement is created.
1200
+ if (terms.some((term) => (term.kind === "name" && broken.has(term.value)) ||
1201
+ !value(term) ||
1202
+ (value(term).kind === "name" &&
1203
+ value(term).value === "runtime")))
1204
+ return;
1205
+ const kind = (term) => {
1206
+ const expr = value(term);
1207
+ return expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)
1208
+ ? "duration"
1209
+ : expr.kind;
1210
+ };
1211
+ const kinds = new Set(terms.map(kind));
1212
+ if (kinds.size !== 1 ||
1213
+ !["money", "number", "percent", "duration"].includes(kind(terms[0])))
1214
+ fail(check.rule, "check compares values of different types", "compare amounts, integers, percentages or durations of one type");
1215
+ const total = (side) => side.reduce((sum, term) => sum + BigInt(String(literal(value(term)))), 0n);
1216
+ const [a, b] = [total(test.left), total(test.right)];
1217
+ const holds = {
1218
+ "<": a < b,
1219
+ "<=": a <= b,
1220
+ ">": a > b,
1221
+ ">=": a >= b,
1222
+ "==": a === b,
1223
+ "!=": a !== b,
1224
+ }[test.operator];
1225
+ if (holds)
1226
+ return;
1227
+ at = terms
1228
+ .map((term) => term.kind === "name"
1229
+ ? suppliedEntries.get(term.value)
1230
+ : undefined)
1231
+ .find(Boolean);
1232
+ }
1233
+ throw new CompileFailure({
1234
+ code: "HSX1001",
1235
+ message: say(check.message),
1236
+ fix: check.fix,
1237
+ span: at?.value.span ?? origin,
1238
+ related: [
1239
+ {
1240
+ source: check.source ?? "program",
1241
+ span: check.span,
1242
+ message: "Header check",
1243
+ },
1244
+ ],
1245
+ });
1246
+ });
1247
+ if (parameterErrors.length) {
1248
+ bindingDiagnostics.push(...parameterErrors.slice(1));
1249
+ throw new CompileFailure(parameterErrors[0]);
1150
1250
  }
1151
1251
  for (const key of body.keys())
1152
1252
  if (![
1153
- "familyRevision",
1154
1253
  "fields",
1155
1254
  "lifecycle",
1156
1255
  "records",
1157
1256
  "summary",
1158
1257
  "invariants",
1159
- "constraints",
1258
+ "checks",
1160
1259
  "ceilings",
1161
- "dependencies",
1162
- "parameterDiagnostics",
1163
1260
  "reports",
1164
1261
  "revisioned",
1165
1262
  "scope",
1166
1263
  ].includes(key) &&
1167
1264
  !key.startsWith("action "))
1168
1265
  fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
1169
- if (body.has("familyRevision")) {
1170
- const revExpr = body.get("familyRevision");
1171
- const revVal = literal(revExpr);
1172
- if (typeof revVal !== "number" ||
1173
- !Number.isInteger(revVal) ||
1174
- revVal <= 0) {
1175
- fail(revExpr, "familyRevision must be a positive integer", "use a positive integer revision");
1176
- }
1177
- if (familyDeclaration) {
1178
- familyDeclaration.revision = revVal;
1179
- }
1180
- }
1181
1266
  const records = entries(asBlock(body.get("records")));
1182
1267
  if (!inherited.size)
1183
1268
  environment.set("parent", { kind: "name", value: id, span: origin });
@@ -1354,9 +1439,9 @@ export function compile(source, options = {}) {
1354
1439
  // optional tunable and a defaulted one both read as unset.
1355
1440
  if (relation === "is" &&
1356
1441
  (choice === "set" || choice === "unset") &&
1357
- tunables.has(tunable) &&
1442
+ terms.has(tunable) &&
1358
1443
  !enums.has(tunable))
1359
- return tunables.get(tunable) === (choice === "set");
1444
+ return isSet(tunable) === (choice === "set");
1360
1445
  const binding = environment.get(tunable);
1361
1446
  if (!binding)
1362
1447
  fail(entry, `unknown branch tunable ${tunable}`, "name an enum, reference or optional tunable declared by this header");
@@ -1367,7 +1452,7 @@ export function compile(source, options = {}) {
1367
1452
  const [root, ...children] = text(bound).split(".");
1368
1453
  const assignment = assignments.get(root);
1369
1454
  let target = assignment
1370
- ? templates.get(assignment.target)?.body
1455
+ ? templateAt(assignment.target)?.body
1371
1456
  : program.decls
1372
1457
  .filter((decl) => decl.kind === "instrument")
1373
1458
  .find((decl) => decl.name === root)?.body;
@@ -1775,10 +1860,9 @@ export function compile(source, options = {}) {
1775
1860
  operation,
1776
1861
  requirements: validatedRequirements,
1777
1862
  };
1778
- const digest = sha256(new TextEncoder().encode(canonicalJson(declaration)));
1779
1863
  const snapshot = {
1780
1864
  ...declaration,
1781
- declarationDigest: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
1865
+ declarationDigest: digest(declaration),
1782
1866
  };
1783
1867
  const adapterRenames = {};
1784
1868
  if (attachmentInfo?.renames) {
@@ -1886,7 +1970,6 @@ export function compile(source, options = {}) {
1886
1970
  summary: slots.has("summary")
1887
1971
  ? String(data(slots.get("summary")))
1888
1972
  : title(name),
1889
- publicAction: camel(`${name}_${id}`),
1890
1973
  event: `${id}.${name}`,
1891
1974
  actor: "caller",
1892
1975
  input: lowerFields(asBlock(slots.get("input"))),
@@ -2315,18 +2398,12 @@ export function compile(source, options = {}) {
2315
2398
  }
2316
2399
  if (moveCalculations.length)
2317
2400
  a.calculate = [...(a.calculate ?? []), ...moveCalculations];
2318
- if (attachmentInfo) {
2319
- if (attachmentInfo.exposed.has(name)) {
2320
- if (automatic(a.actor))
2321
- fail(row, `${name} runs on the clock or its parent, not a caller`, "expose only caller actions");
2322
- a.publicAction = attachmentInfo.exposed.get(name);
2323
- }
2324
- else {
2325
- delete a.publicAction;
2326
- }
2401
+ // An action is hidden until an expose line names it.
2402
+ if (attachmentInfo?.exposed.has(name)) {
2403
+ if (automatic(a.actor))
2404
+ fail(row, `${name} runs on the clock or its parent, not a caller`, "expose only caller actions");
2405
+ a.publicAction = attachmentInfo.exposed.get(name);
2327
2406
  }
2328
- if (automatic(a.actor))
2329
- delete a.publicAction;
2330
2407
  const checkSubjectPaths = (obj, key = "") => {
2331
2408
  if (typeof obj === "string") {
2332
2409
  if ([
@@ -2470,15 +2547,33 @@ export function compile(source, options = {}) {
2470
2547
  origins.push({
2471
2548
  path: `$.instruments[${document.instruments.length}]`,
2472
2549
  span: { ...origin, ...lineColAt(source, origin.start) },
2473
- ...(standardOrigin ? { standardBlock: standardOrigin } : {}),
2550
+ ...(standardOrigin
2551
+ ? {
2552
+ standardBlock: standardOrigin,
2553
+ terms: [...terms].map(([name, term]) => {
2554
+ const text = sources.get(term.source);
2555
+ return {
2556
+ name,
2557
+ from: term.from,
2558
+ text: !term.value
2559
+ ? "unset"
2560
+ : term.from === "party"
2561
+ ? name
2562
+ : text.slice(term.span.start, term.span.end),
2563
+ source: term.source,
2564
+ ...lineColAt(text, term.span.start),
2565
+ };
2566
+ }),
2567
+ }
2568
+ : {}),
2474
2569
  });
2475
- if (familyDeclaration && familyDeclaration.revision !== undefined) {
2570
+ if (familyDeclaration)
2476
2571
  inst.family = {
2477
- module: familyDeclaration.module,
2478
- exportPath: familyDeclaration.exportPath,
2479
- revision: familyDeclaration.revision,
2572
+ ...familyDeclaration,
2573
+ ...(revertedHeader
2574
+ ? { digest: declarationDigest(revertedHeader) }
2575
+ : {}),
2480
2576
  };
2481
- }
2482
2577
  document.instruments.push(inst);
2483
2578
  declarations.set(inst, decl);
2484
2579
  for (const [key, definition] of records) {
@@ -2509,13 +2604,7 @@ export function compile(source, options = {}) {
2509
2604
  };
2510
2605
  declarationSources.set(child, declarationSources.get(decl) ?? "program");
2511
2606
  const childFamily = familyDeclaration
2512
- ? {
2513
- module: familyDeclaration.module,
2514
- exportPath: `${familyDeclaration.exportPath}.${key}`,
2515
- ...(familyDeclaration.revision !== undefined
2516
- ? { revision: familyDeclaration.revision }
2517
- : {}),
2518
- }
2607
+ ? resolveFamily(`${familyDeclaration.module}.${familyDeclaration.exportPath}.${key}`, { span: origin }, false)
2519
2608
  : undefined;
2520
2609
  addInstrument(child, `${id}_${key}`, emptyBlock, origin, new Map([
2521
2610
  ...environment,
@@ -2527,7 +2616,7 @@ export function compile(source, options = {}) {
2527
2616
  .filter(Boolean)
2528
2617
  .join("."),
2529
2618
  }
2530
- : undefined, tunables);
2619
+ : undefined, terms);
2531
2620
  }
2532
2621
  };
2533
2622
  const compileObject = (decl) => {
@@ -2561,10 +2650,7 @@ export function compile(source, options = {}) {
2561
2650
  }
2562
2651
  const attachmentName = match[1];
2563
2652
  const targetTemplate = match[2];
2564
- const template = templates.get(targetTemplate);
2565
- if (!template) {
2566
- fail(entry, `unknown instrument ${targetTemplate}`, `add use ${targetTemplate.split(".")[0]} and choose a declared instrument`);
2567
- }
2653
+ const template = requireTemplate(targetTemplate, entry);
2568
2654
  const instId = `${decl.name}_${attachmentName}`;
2569
2655
  const attachmentBlock = asBlock(entry.value);
2570
2656
  const renames = new Map();
@@ -2578,8 +2664,9 @@ export function compile(source, options = {}) {
2578
2664
  }
2579
2665
  else if (row.key === "rename") {
2580
2666
  for (const r of asBlock(row.value).entries) {
2581
- if (renames.has(r.key))
2582
- fail(r, `duplicate rename ${r.key}`, "rename each subject field once");
2667
+ const first = renameEntries.get(r.key);
2668
+ if (first)
2669
+ conflict(texts, `rename ${r.key} is given twice`, first, r, "rename each subject field once", { values: [first.value, r.value] });
2583
2670
  renames.set(r.key, text(r.value));
2584
2671
  renameEntries.set(r.key, r);
2585
2672
  }
@@ -2588,9 +2675,12 @@ export function compile(source, options = {}) {
2588
2675
  if (row.value.kind === "call") {
2589
2676
  const actionName = row.value.name;
2590
2677
  const publicName = text(row.value.args[0]);
2591
- if (exposed.has(actionName) ||
2592
- !template.body.entries.some((entry) => entry.key === `action ${actionName}`))
2593
- fail(row, `unknown or repeated action ${actionName}`, "expose one declared action once");
2678
+ const first = exposures.find((exposure) => exposure.instrument === instId &&
2679
+ exposure.action === actionName);
2680
+ if (first)
2681
+ conflict(texts, `${actionName} is exposed twice`, first.entry, row, "expose each action once");
2682
+ if (!template.body.entries.some((entry) => entry.key === `action ${actionName}`))
2683
+ fail(row, `unknown action ${actionName}`, "expose one declared action");
2594
2684
  exposed.set(actionName, publicName);
2595
2685
  exposures.push({
2596
2686
  instrument: instId,
@@ -2623,7 +2713,7 @@ export function compile(source, options = {}) {
2623
2713
  attachments,
2624
2714
  economics,
2625
2715
  template: targetTemplate,
2626
- }, templateFamily ? { ...templateFamily } : undefined, standardOrigins.get(template));
2716
+ }, templateFamily, standardOrigins.get(template));
2627
2717
  }
2628
2718
  catch (error) {
2629
2719
  if (!(error instanceof CompileFailure))
@@ -2757,11 +2847,9 @@ export function compile(source, options = {}) {
2757
2847
  addInstrument(decl, decl.name, emptyBlock, decl.span);
2758
2848
  }
2759
2849
  if (decl.kind === "assignment") {
2760
- const template = templates.get(decl.target);
2761
- if (!template)
2762
- fail(decl, `unknown instrument ${decl.target}`, `add use ${decl.target.split(".")[0]} and choose a declared instrument`);
2850
+ const template = requireTemplate(decl.target, decl);
2763
2851
  const templateFamily = resolveFamily(decl.target, decl, false);
2764
- addInstrument(template, decl.name, decl.body, decl.span, new Map(), new Map(), undefined, templateFamily ? { ...templateFamily } : undefined, standardOrigins.get(template));
2852
+ addInstrument(template, decl.name, decl.body, decl.span, new Map(), new Map(), undefined, templateFamily, standardOrigins.get(template));
2765
2853
  }
2766
2854
  if (decl.kind === "object") {
2767
2855
  compileObject(decl);
@@ -2787,7 +2875,8 @@ export function compile(source, options = {}) {
2787
2875
  .map((id) => document.instruments.find((item) => item.id === id)?.family);
2788
2876
  const family = families[0];
2789
2877
  if (family &&
2790
- families.every((item) => canonicalJson(item) === canonicalJson(family)))
2878
+ families.every((item) => item !== undefined &&
2879
+ canonicalJson(item) === canonicalJson(family)))
2791
2880
  selection.family = family;
2792
2881
  }
2793
2882
  for (const child of Object.values(record))
@@ -3070,7 +3159,7 @@ export function compile(source, options = {}) {
3070
3159
  const declaredMembers = (selection) => {
3071
3160
  const members = [selection.instrument].flat();
3072
3161
  const header = selection.family
3073
- ? templates.get(`${selection.family.module}.${selection.family.exportPath}`)
3162
+ ? templateAt(`${selection.family.module}.${selection.family.exportPath}`)
3074
3163
  : undefined;
3075
3164
  if (!header ||
3076
3165
  !entries(asBlock(entries(header.body).get("fields"))).has(selection.reference))
@@ -3305,31 +3394,38 @@ export function compile(source, options = {}) {
3305
3394
  if (!document.instruments.find((inst) => inst.id === exposure.instrument)
3306
3395
  ?.actions[exposure.action])
3307
3396
  fail(exposure.entry, `action ${exposure.action} is excluded by these bindings`, "expose an action available with these tunables");
3308
- const changed = new Set();
3397
+ const changed = new Map();
3309
3398
  for (const decl of program.decls)
3310
- if (decl.kind === "hide" || decl.kind === "expose") {
3399
+ if (decl.kind === "expose") {
3311
3400
  const parts = decl.target.split(".");
3312
3401
  const key = parts.pop();
3313
3402
  const id = parts.join("_");
3314
3403
  const action = document.instruments.find((i) => i.id === id)?.actions[key ?? ""];
3315
- if (!action || changed.has(decl.target))
3316
- fail(decl, `unknown or repeated action ${decl.target}`, "name one declared object.action once");
3317
- changed.add(decl.target);
3318
- if (decl.kind === "hide")
3319
- delete action.publicAction;
3320
- else if (automatic(action.actor))
3404
+ const first = changed.get(decl.target);
3405
+ if (first)
3406
+ conflict(texts, `${decl.target} is exposed twice`, first, decl, "expose each action once");
3407
+ if (!action)
3408
+ fail(decl, `unknown action ${decl.target}`, "name one declared object.action");
3409
+ changed.set(decl.target, decl);
3410
+ if (automatic(action.actor))
3321
3411
  fail(decl, `${decl.target} runs on the clock or its parent, not a caller`, "expose only caller actions");
3322
- else
3323
- action.publicAction = decl.name;
3412
+ action.publicAction = decl.name;
3324
3413
  }
3414
+ // Two actions of one instrument cannot share a public name.
3415
+ const published = new Map();
3325
3416
  for (const decl of program.decls) {
3326
3417
  if (decl.kind !== "expose")
3327
3418
  continue;
3328
- const parts = decl.target.split(".");
3329
- parts.pop();
3330
- const instrument = document.instruments.find((item) => item.id === parts.join("_"));
3331
- if (Object.values(instrument.actions).filter((action) => action.publicAction === decl.name).length > 1)
3332
- fail(decl, `public action ${decl.name} is used more than once on ${parts.join(".")}`, "give each exposed action a distinct public name");
3419
+ const instrument = decl.target.split(".").slice(0, -1).join("_");
3420
+ const key = `${instrument} ${decl.name}`;
3421
+ const first = published.get(key) ??
3422
+ exposures.find(({ instrument: id, action, entry }) => id === instrument &&
3423
+ `${decl.target}`.split(".").pop() !== action &&
3424
+ entry.value.kind === "call" &&
3425
+ text(entry.value.args[0]) === decl.name)?.entry;
3426
+ if (first)
3427
+ conflict(texts, `public action ${decl.name} is given twice on ${instrument}`, first, decl, "give each exposed action a distinct public name");
3428
+ published.set(key, decl);
3333
3429
  }
3334
3430
  // Every attachment of an object shares one public action namespace, so
3335
3431
  // two attachments that both `expose create` collide.
@@ -3477,6 +3573,7 @@ export function compile(source, options = {}) {
3477
3573
  warnings: [],
3478
3574
  diagnostics: [...sameParty.errors, ...unbound].map((d) => diagnostic(d, "check")),
3479
3575
  };
3576
+ const checks = lowerChecks(program.checks, validated.value, literal);
3480
3577
  return {
3481
3578
  verdict: "valid",
3482
3579
  diagnostics: [],
@@ -3488,6 +3585,7 @@ export function compile(source, options = {}) {
3488
3585
  document: validated.value,
3489
3586
  costManifest: buildUdlCostManifest(validated.value),
3490
3587
  originMap: origins,
3588
+ checks,
3491
3589
  },
3492
3590
  };
3493
3591
  }