@hyperscale0/hsx 4.3.0 → 5.0.1

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.
@@ -1,6 +1,7 @@
1
1
  import { hash as sha256 } from "fast-sha256";
2
2
  import { buildUdlCostManifest } from "./cost.js";
3
3
  import { validateUdl, resolveField, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, udlObjectFieldSchema, } from "@hyperscale0/udl";
4
+ import { bindingDependencies, parameterDiagnostics, BindingContractError, } from "./binding-contract.js";
4
5
  import { tunableBounds } from "./tunables.js";
5
6
  import { parseProgram } from "./parse.js";
6
7
  import { lineColAt, } from "./ast.js";
@@ -265,6 +266,7 @@ export function compile(source, options = {}) {
265
266
  diagnostics: parsed.diagnostics.map((d) => diagnostic(d, "parse")),
266
267
  };
267
268
  const program = parsed.program;
269
+ const bindingDiagnostics = [];
268
270
  try {
269
271
  if (program.header)
270
272
  fail(program, "compile a program, not a header", 'start with program product_name "Title"');
@@ -312,6 +314,10 @@ export function compile(source, options = {}) {
312
314
  for (const decl of program.decls)
313
315
  if (decl.kind === "instrument")
314
316
  templates.set(decl.name, decl);
317
+ for (const decl of program.decls) {
318
+ if (decl.kind === "instrument" && decl.parameters.length)
319
+ fail(decl, "This compiler cannot instantiate a parameterized instrument declared inside a program.", "For this version, specialize the instrument with fixed bindings and remove its parameters. Custom reusable headers require a host-supplied library.");
320
+ }
315
321
  const document = {
316
322
  udl: 4,
317
323
  version: 1,
@@ -368,6 +374,7 @@ export function compile(source, options = {}) {
368
374
  }
369
375
  }
370
376
  const origins = [];
377
+ const refundBindings = new Map();
371
378
  const resolveFamily = (rawPath, expr, required = true) => {
372
379
  const parts = rawPath.split(".");
373
380
  if (parts.length < 2) {
@@ -556,6 +563,7 @@ export function compile(source, options = {}) {
556
563
  supplied.set(entry.key, entry.value);
557
564
  }
558
565
  const environment = new Map(inherited);
566
+ const parameterErrors = [];
559
567
  for (const param of decl.parameters) {
560
568
  const type = param.value.kind === "default" ? param.value.type : param.value;
561
569
  const fallback = param.value.kind === "default" ? param.value.value : undefined;
@@ -575,15 +583,96 @@ export function compile(source, options = {}) {
575
583
  if (!actual) {
576
584
  if (type.kind === "type" && type.optional)
577
585
  continue;
578
- failWithCode({ span: origin }, partyParameter ? "subject_party_unbound" : "HSX1001", `${id} needs ${param.key}`, partyParameter
579
- ? `bind ${param.key} to ${[...subjectPartyRoles, ...Object.keys(document.parties)].join(", ")} or declare a party`
580
- : `add ${param.key}: value inside ${id}`);
586
+ parameterErrors.push({
587
+ span: origin,
588
+ code: partyParameter ? "subject_party_unbound" : "HSX1001",
589
+ message: partyParameter
590
+ ? `\`${param.key}\` has no binding. An attached ${param.key} must resolve to a subject role or an eligible declared party.`
591
+ : `${id} needs ${param.key}`,
592
+ fix: partyParameter
593
+ ? `Use \`${param.key}: actor\` for the initiating customer or \`${param.key}: owner\` for the object owner. Declare a business for a fixed company counterparty.`
594
+ : `add ${param.key}: value inside ${id}`,
595
+ related: [
596
+ {
597
+ source: declarationSources.get(decl) ?? "program",
598
+ span: param.span,
599
+ message: `Parameter ${param.key}`,
600
+ },
601
+ ],
602
+ });
603
+ continue;
581
604
  }
582
605
  environment.set(param.key, actual);
583
606
  }
584
- for (const key of supplied.keys())
585
- if (!decl.parameters.some((p) => p.key === key))
586
- fail(supplied.get(key), `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
607
+ let policyDiagnostics;
608
+ let dependencies;
609
+ try {
610
+ policyDiagnostics = parameterDiagnostics(decl);
611
+ dependencies = bindingDependencies(decl);
612
+ }
613
+ catch (error) {
614
+ if (!(error instanceof BindingContractError))
615
+ throw error;
616
+ throw new CompileFailure({
617
+ code: "HSX1001",
618
+ message: error.message,
619
+ fix: "Repair the header binding contract.",
620
+ span: error.entry.span,
621
+ source: declarationSources.get(decl) ?? "program",
622
+ });
623
+ }
624
+ for (const dependency of dependencies) {
625
+ const selected = environment.get(dependency.when);
626
+ if (selected?.kind === "name" &&
627
+ selected.value === dependency.is &&
628
+ !environment.has(dependency.binding)) {
629
+ throw new CompileFailure({
630
+ code: "HSX1001",
631
+ message: dependency.message.replaceAll("{attachment}", attachmentInfo?.attachmentName ?? id),
632
+ fix: dependency.fix,
633
+ span: origin,
634
+ related: [
635
+ {
636
+ source: declarationSources.get(decl) ?? "program",
637
+ span: dependency.span,
638
+ message: "Conditional binding requirement",
639
+ },
640
+ ],
641
+ });
642
+ }
643
+ }
644
+ for (const key of supplied.keys()) {
645
+ if (decl.parameters.some((p) => p.key === key))
646
+ continue;
647
+ const suppliedValue = supplied.get(key);
648
+ const requirements = decl.body.entries
649
+ .filter((entry) => entry.key.startsWith("action "))
650
+ .flatMap((entry) => asBlock(entries(asBlock(entry.value)).get("subject")).entries);
651
+ const moneyFields = [
652
+ ...new Set(requirements
653
+ .filter((entry) => {
654
+ const type = entry.value;
655
+ return type.kind === "name"
656
+ ? type.value === "money"
657
+ : (type.kind === "type" || type.kind === "call") &&
658
+ type.name === "money";
659
+ })
660
+ .map((entry) => entry.key)),
661
+ ];
662
+ if (attachmentInfo &&
663
+ (suppliedValue.kind === "money" || suppliedValue.kind === "name") &&
664
+ moneyFields.length === 1) {
665
+ const field = moneyFields[0];
666
+ const object = objects.get(attachmentInfo.subjectKindId);
667
+ const candidates = asBlock(entries(object.body).get("fields")).entries.filter((entry) => entry.value.kind === "name"
668
+ ? entry.value.value === "money"
669
+ : (entry.value.kind === "type" || entry.value.kind === "call") &&
670
+ entry.value.name === "money");
671
+ const target = candidates.length === 1 ? candidates[0].key : "yourDepositField";
672
+ fail(suppliedValue, `\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its funding action requires the object's \`${field}\` field.`, `Remove \`${key}\`. To use your deposit field, add \`rename { ${field}: ${target} }\`.`);
673
+ }
674
+ fail(suppliedValue, `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
675
+ }
587
676
  const isParty = (name) => !!document.parties[name] ||
588
677
  (!!attachmentInfo &&
589
678
  subjectPartyRoles.includes(name));
@@ -607,6 +696,27 @@ export function compile(source, options = {}) {
607
696
  type.startsWith(`${assignment.target}.`))
608
697
  .map((assignment) => assignment.name +
609
698
  type.slice(assignment.target.length).replaceAll(".", "_"));
699
+ if (expr.name === "object" &&
700
+ matches.length !== 1 &&
701
+ attachmentInfo) {
702
+ const parameter = [...environment].find(([, value]) => value === expr)?.[0] ??
703
+ decl.parameters.find((p) => p.value.kind === "default" &&
704
+ p.value.value.span.start === expr.span.start)?.key ??
705
+ "reference";
706
+ const local = (name) => attachmentSubjects.has(name)
707
+ ? name.slice(attachmentInfo.subjectKindId.length + 1)
708
+ : `${name} (program)`;
709
+ const target = templates.get(type);
710
+ const required = target?.parameters
711
+ .filter((p) => p.value.kind !== "default" &&
712
+ !(p.value.kind === "type" && p.value.optional))
713
+ .map((p) => p.key) ?? [];
714
+ fail({ span: origin }, `Attachment \`${attachmentInfo.attachmentName}\` needs a \`${parameter}\` binding. ${matches.length === 0
715
+ ? `No \`${type}\` attachment exists on \`${attachmentInfo.subjectKindId}\`.`
716
+ : `Several \`${type}\` attachments match on \`${attachmentInfo.subjectKindId}\`: ${matches.map(local).join(", ")}.`}`, matches.length
717
+ ? `Bind \`${parameter}\` explicitly to one of: ${matches.map(local).join(", ")}.`
718
+ : `Declare a ${parameter} attachment and bind \`${parameter}: allowance\`. Choose ${required.map((name) => (name.startsWith("per_") ? `its ${name.slice(4).replaceAll("_", " ")} limit` : `\`${name}\``)).join(", ") || "its required bindings"} explicitly.`);
719
+ }
610
720
  if (expr.name !== "all" && matches.length !== 1)
611
721
  failWithCode(expr, partyBinding ? "subject_party_unbound" : "HSX1001", `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`, "declare the required object or supply this tunable explicitly");
612
722
  const items = matches.map((value) => ({
@@ -664,105 +774,153 @@ export function compile(source, options = {}) {
664
774
  return resolved;
665
775
  };
666
776
  for (const param of decl.parameters) {
667
- const actual = environment.get(param.key);
668
- if (!actual)
669
- continue;
670
- const t = param.value.kind === "default" ? param.value.type : param.value;
671
- const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
672
- const v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
673
- ? actual
674
- : resolve(actual, new Set(), !!attachmentInfo && type === "party");
675
- environment.set(param.key, v);
676
- if (type === "enum" && t.kind === "call") {
677
- if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
678
- fail(v, `invalid ${param.key}`, `choose ${t.args.map(text).join(", ")}`);
679
- }
680
- else if (type === "list") {
681
- if (v.kind !== "list")
682
- fail(v, `${param.key} needs a list`, "write [value, value]");
683
- }
684
- else if (type === "party") {
685
- if (v.kind !== "name" || !isParty(v.value))
686
- failWithCode(actual, attachmentInfo ? "subject_party_unbound" : "HSX1001", `${param.key} needs a declared party`, attachmentInfo
687
- ? `bind ${param.key} to ${[...subjectPartyRoles, ...Object.keys(document.parties)].join(", ")} or declare a party`
688
- : "declare a party and use its name here");
689
- const party = document.parties[v.value];
690
- if ((party?.kind === "staff" && !party.role) ||
691
- (attachmentInfo && party?.kind === "person"))
692
- failWithCode(actual, "party_kind_mismatch", `${param.key} cannot bind ${v.value}`, "use a subject role, declared business, or staff with a role");
693
- resolvedParties.add(param.key);
694
- if (attachmentInfo)
695
- attachmentInfo.parties[param.key] = subjectPartyRoles.includes(v.value)
696
- ? { role: v.value }
697
- : { party: v.value };
698
- }
699
- else if (type === "ref") {
700
- const values = v.kind === "list" ? v.items : [v];
701
- if (v.kind === "list" && (t.kind !== "type" || !t.many))
702
- fail(v, `${param.key} accepts one reference`, "use one object name");
703
- if (!values.length || values.length > 16)
704
- fail(v, "reference union needs 1 to 16 objects", "use at most 16 distinct object names");
705
- const seen = new Set();
706
- for (const value of values) {
707
- if (value.kind !== "name")
708
- fail(value, "reference needs an object name", "name a declared object");
709
- const [root, ...tail] = value.value.split(".");
710
- const obj = objects.get(root);
711
- const assignment = assignments.get(root);
712
- const targetType = obj ? obj.name : assignment?.target;
713
- if ((!obj &&
714
- !assignment &&
715
- !document.instruments.some((inst) => inst.id === value.value)) ||
716
- (t.kind === "type" &&
717
- t.target &&
718
- [targetType, ...tail].join(".") !== t.target))
719
- fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
720
- if (seen.has(value.value))
721
- fail(value, "duplicate reference", "list each object once");
722
- seen.add(value.value);
723
- }
724
- // A many-reference tunable is a list even when one object is bound,
725
- // so report datasets and other value positions never see a bare name.
726
- if (t.kind === "type" && t.many && v.kind !== "list")
727
- environment.set(param.key, {
728
- kind: "list",
729
- items: [v],
730
- span: v.span,
731
- });
732
- }
733
- else if (type === "fee" || type === "split" || type === "policy") {
734
- if (v.kind !== "block")
735
- fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
736
- }
737
- else if (type === "money" ||
738
- type === "percent" ||
739
- type === "date" ||
740
- type === "duration" ||
741
- type === "integer" ||
742
- type === "text") {
743
- if (v.kind === "name" && v.value === "runtime")
777
+ try {
778
+ const actual = environment.get(param.key);
779
+ if (!actual)
744
780
  continue;
745
- const expected = type === "integer" ? "number" : type;
746
- if (v.kind !== expected &&
747
- !(type === "duration" &&
748
- (v.kind === "text" || v.kind === "name") &&
749
- /^P/.test(v.value)))
750
- fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
751
- try {
752
- const value = literal(v);
753
- const bounds = tunableBounds(t);
754
- if (bounds &&
755
- (BigInt(String(value)) < BigInt(bounds.minimum) ||
756
- BigInt(String(value)) > BigInt(bounds.maximum)))
757
- fail(v, `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`, "choose a value inside the tunable's bounds");
781
+ const t = param.value.kind === "default" ? param.value.type : param.value;
782
+ const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
783
+ const v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
784
+ ? actual
785
+ : resolve(actual, new Set(), !!attachmentInfo && type === "party");
786
+ environment.set(param.key, v);
787
+ if (type === "enum" && t.kind === "call") {
788
+ if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
789
+ fail(v, `invalid ${param.key}`, `choose ${t.args.map(text).join(", ")}`);
758
790
  }
759
- catch (error) {
760
- if (error instanceof CompileFailure)
761
- fail(v, `${param.key}: ${error.diagnostic.message}`, error.diagnostic.fix);
762
- throw error;
791
+ else if (type === "list") {
792
+ if (v.kind !== "list")
793
+ fail(v, `${param.key} needs a list`, "write [value, value]");
763
794
  }
795
+ else if (type === "party") {
796
+ if (v.kind !== "name" || !isParty(v.value))
797
+ failWithCode(actual, attachmentInfo ? "subject_party_unbound" : "HSX1001", attachmentInfo
798
+ ? `\`${param.key}: ${"value" in actual && typeof actual.value === "string" ? actual.value : param.key}\` has no binding. An attached ${param.key} must resolve to a subject role or an eligible declared party.`
799
+ : `${param.key} needs a declared party`, attachmentInfo
800
+ ? `Use \`${param.key}: actor\` for the initiating customer or \`${param.key}: owner\` for the object owner. Declare a business for a fixed company counterparty.`
801
+ : "declare a party and use its name here");
802
+ const party = document.parties[v.value];
803
+ if ((party?.kind === "staff" && !party.role) ||
804
+ (attachmentInfo && party?.kind === "person"))
805
+ failWithCode(actual, "party_kind_mismatch", party?.kind === "person"
806
+ ? `\`${v.value}\` is a declared person. Attachments resolve customer identity through \`owner\` or \`actor\`, rather than a fixed person declaration.`
807
+ : `\`${v.value}\` is declared staff without a permission role.`, "Replace this binding with the appropriate subject role. Use declared businesses for fixed counterparties and permission-bearing staff roles for authorized actions.");
808
+ resolvedParties.add(param.key);
809
+ if (attachmentInfo)
810
+ attachmentInfo.parties[param.key] = subjectPartyRoles.includes(v.value)
811
+ ? { role: v.value }
812
+ : { party: v.value };
813
+ }
814
+ else if (type === "ref") {
815
+ const values = v.kind === "list" ? v.items : [v];
816
+ if (v.kind === "list" && (t.kind !== "type" || !t.many))
817
+ fail(v, `${param.key} accepts one reference`, "use one object name");
818
+ if (!values.length || values.length > 16)
819
+ fail(v, "reference union needs 1 to 16 objects", "use at most 16 distinct object names");
820
+ const seen = new Set();
821
+ for (const value of values) {
822
+ if (value.kind !== "name")
823
+ fail(value, "reference needs an object name", "name a declared object");
824
+ const [root, ...tail] = value.value.split(".");
825
+ const obj = objects.get(root);
826
+ const assignment = assignments.get(root);
827
+ const targetType = obj ? obj.name : assignment?.target;
828
+ if ((!obj &&
829
+ !assignment &&
830
+ !document.instruments.some((inst) => inst.id === value.value)) ||
831
+ (t.kind === "type" &&
832
+ t.target &&
833
+ [targetType, ...tail].join(".") !== t.target))
834
+ fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
835
+ if (seen.has(value.value))
836
+ fail(value, "duplicate reference", "list each object once");
837
+ seen.add(value.value);
838
+ }
839
+ // A many-reference tunable is a list even when one object is bound,
840
+ // so report datasets and other value positions never see a bare name.
841
+ if (t.kind === "type" && t.many && v.kind !== "list")
842
+ environment.set(param.key, {
843
+ kind: "list",
844
+ items: [v],
845
+ span: v.span,
846
+ });
847
+ }
848
+ else if (type === "fee" || type === "split" || type === "policy") {
849
+ if (v.kind !== "block")
850
+ fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
851
+ }
852
+ else if (type === "money" ||
853
+ type === "percent" ||
854
+ type === "date" ||
855
+ type === "duration" ||
856
+ type === "integer" ||
857
+ type === "text") {
858
+ if (v.kind === "name" && v.value === "runtime")
859
+ continue;
860
+ const expected = type === "integer" ? "number" : type;
861
+ if (v.kind !== expected &&
862
+ !(type === "duration" &&
863
+ (v.kind === "text" || v.kind === "name") &&
864
+ /^P/.test(v.value))) {
865
+ if (type === "money" && v.kind === "name" && attachmentInfo) {
866
+ const target = v.value.replace(/^subject\./, "");
867
+ const fallback = param.value.kind === "default"
868
+ ? param.value.value
869
+ : undefined;
870
+ const advice = fallback?.kind === "name" && fallback.value === "runtime"
871
+ ? `omit \`${param.key}\``
872
+ : `set \`${param.key}: runtime\``;
873
+ 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.`);
874
+ }
875
+ if (type === "money" && v.kind === "percent") {
876
+ const policy = policyDiagnostics.find((policy) => policy.parameter === param.key);
877
+ 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 ??
878
+ "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.");
879
+ }
880
+ fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
881
+ }
882
+ try {
883
+ const value = literal(v);
884
+ const bounds = tunableBounds(t);
885
+ if (bounds &&
886
+ (BigInt(String(value)) < BigInt(bounds.minimum) ||
887
+ BigInt(String(value)) > BigInt(bounds.maximum)))
888
+ fail(v, `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`, "choose a value inside the tunable's bounds");
889
+ }
890
+ catch (error) {
891
+ if (error instanceof CompileFailure)
892
+ fail(v, `${param.key}: ${error.diagnostic.message}`, error.diagnostic.fix);
893
+ throw error;
894
+ }
895
+ }
896
+ }
897
+ catch (error) {
898
+ if (!(error instanceof CompileFailure))
899
+ throw error;
900
+ const related = [
901
+ {
902
+ source: declarationSources.get(decl) ?? "program",
903
+ span: param.span,
904
+ message: `Parameter ${param.key}`,
905
+ },
906
+ ];
907
+ const actual = supplied.get(param.key);
908
+ const party = actual?.kind === "name"
909
+ ? program.decls.find((entry) => entry.kind === "party" && entry.name === actual.value)
910
+ : undefined;
911
+ if (party?.kind === "party")
912
+ related.push({
913
+ source: "program",
914
+ span: party.span,
915
+ message: `Declared party ${party.name}`,
916
+ });
917
+ parameterErrors.push({ ...error.diagnostic, related });
764
918
  }
765
919
  }
920
+ if (parameterErrors.length) {
921
+ bindingDiagnostics.push(...parameterErrors.slice(1));
922
+ throw new CompileFailure(parameterErrors[0]);
923
+ }
766
924
  const body = entries(decl.body);
767
925
  for (const constraint of asBlock(body.get("constraints")).entries) {
768
926
  const rule = constraint.value;
@@ -790,6 +948,8 @@ export function compile(source, options = {}) {
790
948
  "summary",
791
949
  "invariants",
792
950
  "constraints",
951
+ "dependencies",
952
+ "parameterDiagnostics",
793
953
  "reports",
794
954
  "revisioned",
795
955
  ].includes(key) &&
@@ -1391,6 +1551,33 @@ export function compile(source, options = {}) {
1391
1551
  };
1392
1552
  }
1393
1553
  }
1554
+ const fromBinding = slots.get("from");
1555
+ if (fromBinding?.kind === "name" &&
1556
+ fromBinding.value.includes(".") &&
1557
+ slots.has("moves")) {
1558
+ const [parameter, member] = fromBinding.value.split(".");
1559
+ const suppliedPolicy = supplied.get(parameter);
1560
+ if (suppliedPolicy?.kind === "block") {
1561
+ const selected = entries(suppliedPolicy).get(member);
1562
+ const parameterDecl = decl.parameters.find((entry) => entry.key === parameter);
1563
+ const fallback = parameterDecl?.value.kind === "default"
1564
+ ? parameterDecl.value.value
1565
+ : undefined;
1566
+ const defaultState = fallback?.kind === "block"
1567
+ ? entries(fallback).get(member)
1568
+ : undefined;
1569
+ if (selected && defaultState?.kind === "name") {
1570
+ const bindings = refundBindings.get(id) ?? [];
1571
+ bindings.push({
1572
+ parameter: member,
1573
+ span: selected.span,
1574
+ defaultState: defaultState.value,
1575
+ action: name,
1576
+ });
1577
+ refundBindings.set(id, bindings);
1578
+ }
1579
+ }
1580
+ }
1394
1581
  const a = {
1395
1582
  summary: slots.has("summary")
1396
1583
  ? String(data(slots.get("summary")))
@@ -1799,13 +1986,21 @@ export function compile(source, options = {}) {
1799
1986
  span: entry.value.span,
1800
1987
  };
1801
1988
  const templateFamily = resolveFamily(targetTemplate, entry, false);
1802
- addInstrument(template, instId, tunableBlock, entry.span, new Map(), new Map(), {
1803
- subjectKindId: decl.name,
1804
- attachmentName,
1805
- renames,
1806
- exposed,
1807
- parties,
1808
- }, templateFamily ? { ...templateFamily } : undefined);
1989
+ try {
1990
+ addInstrument(template, instId, tunableBlock, entry.span, new Map(), new Map(), {
1991
+ subjectKindId: decl.name,
1992
+ attachmentName,
1993
+ renames,
1994
+ exposed,
1995
+ parties,
1996
+ }, templateFamily ? { ...templateFamily } : undefined);
1997
+ }
1998
+ catch (error) {
1999
+ if (!(error instanceof CompileFailure))
2000
+ throw error;
2001
+ bindingDiagnostics.push(error.diagnostic);
2002
+ continue;
2003
+ }
1809
2004
  const attachedInst = document.instruments.find((i) => i.id === instId);
1810
2005
  if (attachedInst?.actions.create) {
1811
2006
  const owned = new Set(attachedInst.calculate.map((node) => node.target));
@@ -1870,7 +2065,7 @@ export function compile(source, options = {}) {
1870
2065
  }
1871
2066
  columns = columnsExpr.items.map(text);
1872
2067
  if (columns.length > 8) {
1873
- fail(columnsExpr, "at most 8 columns allowed", "choose up to 8 columns");
2068
+ fail(columnsExpr, `The object list selects ${columns.length} columns; this release supports 8. The object may retain all its fields.`, `Remove ${columns.length === 9 ? "one name" : `${columns.length - 8} names`} from \`columns\`, not from \`fields\`.`);
1874
2069
  }
1875
2070
  }
1876
2071
  document.objects.push({
@@ -1884,8 +2079,6 @@ export function compile(source, options = {}) {
1884
2079
  };
1885
2080
  for (const decl of program.decls) {
1886
2081
  if (decl.kind === "instrument") {
1887
- if (decl.parameters.length)
1888
- fail(decl, "program records cannot declare tunables", "put reusable instruments in a header");
1889
2082
  addInstrument(decl, decl.name, emptyBlock, decl.span);
1890
2083
  }
1891
2084
  if (decl.kind === "assignment") {
@@ -1899,6 +2092,13 @@ export function compile(source, options = {}) {
1899
2092
  compileObject(decl);
1900
2093
  }
1901
2094
  }
2095
+ if (bindingDiagnostics.length)
2096
+ return {
2097
+ verdict: "invalid",
2098
+ diagnostics: bindingDiagnostics
2099
+ .sort((a, b) => a.span.start - b.span.start)
2100
+ .map((d) => diagnostic(d, "check")),
2101
+ };
1902
2102
  // Propagate mandatory invoked action requirements
1903
2103
  let changedInvocations = true;
1904
2104
  let invocationIterations = 0;
@@ -2103,8 +2303,36 @@ export function compile(source, options = {}) {
2103
2303
  const origin = [...origins]
2104
2304
  .reverse()
2105
2305
  .find((o) => i.path.startsWith(o.path));
2306
+ const index = /^\$\.instruments\[(\d+)\]/.exec(i.path)?.[1];
2307
+ const instrument = index === undefined
2308
+ ? undefined
2309
+ : document.instruments[Number(index)];
2310
+ const attachment = document.objects
2311
+ .flatMap((object) => object.attachments)
2312
+ .find((attachment) => attachment.instrument === instrument?.id);
2313
+ const stranded = i.stranded;
2314
+ if (instrument && stranded?.accounts.length) {
2315
+ const binding = refundBindings
2316
+ .get(instrument.id)
2317
+ ?.find((binding) => binding.defaultState === stranded.state &&
2318
+ instrument.actions[binding.action]?.moves.some((move) => "amount" in move &&
2319
+ stranded.accounts.some((account) => move.from === `self.${account}`)));
2320
+ return diagnostic({
2321
+ code: i.code,
2322
+ message: `\`${attachment?.name ?? instrument.id}\` can reach \`${stranded.state}\` with money in ${stranded.accounts.map((account) => `\`${account}\``).join(", ")}, but no action leaves that state and disposes of the balance.`,
2323
+ fix: binding
2324
+ ? `Restore \`${binding.parameter}: ${stranded.state}\`, or author a complete refund path for every reachable funded state.`
2325
+ : "Author a complete refund path for every reachable funded state.",
2326
+ span: binding?.span ?? origin?.span ?? program.span,
2327
+ related: stranded.accounts.map((account) => ({
2328
+ source: "program",
2329
+ span: origin?.span ?? program.span,
2330
+ message: `State path: ${instrument.lifecycle.initial} -> ${(stranded.paths[account] ?? stranded.actions).join(" -> ")} -> ${stranded.state}; owned accounts: ${account}.`,
2331
+ })),
2332
+ }, "lower");
2333
+ }
2106
2334
  return diagnostic({
2107
- code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
2335
+ code: i.code,
2108
2336
  message: `${i.path}: ${i.message}`,
2109
2337
  fix: i.fix,
2110
2338
  span: origin?.span ?? program.span,
@@ -2125,7 +2353,7 @@ export function compile(source, options = {}) {
2125
2353
  if (error instanceof CompileFailure)
2126
2354
  return {
2127
2355
  verdict: "invalid",
2128
- diagnostics: [diagnostic(error.diagnostic, "check")],
2356
+ diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
2129
2357
  };
2130
2358
  throw error;
2131
2359
  }