@hyperscale0/hsx 4.2.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,13 +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}`, `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;
579
604
  }
580
605
  environment.set(param.key, actual);
581
606
  }
582
- for (const key of supplied.keys())
583
- if (!decl.parameters.some((p) => p.key === key))
584
- 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
+ }
585
676
  const isParty = (name) => !!document.parties[name] ||
586
677
  (!!attachmentInfo &&
587
678
  subjectPartyRoles.includes(name));
@@ -605,6 +696,27 @@ export function compile(source, options = {}) {
605
696
  type.startsWith(`${assignment.target}.`))
606
697
  .map((assignment) => assignment.name +
607
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
+ }
608
720
  if (expr.name !== "all" && matches.length !== 1)
609
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");
610
722
  const items = matches.map((value) => ({
@@ -662,103 +774,153 @@ export function compile(source, options = {}) {
662
774
  return resolved;
663
775
  };
664
776
  for (const param of decl.parameters) {
665
- const actual = environment.get(param.key);
666
- if (!actual)
667
- continue;
668
- const t = param.value.kind === "default" ? param.value.type : param.value;
669
- const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
670
- const v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
671
- ? actual
672
- : resolve(actual, new Set(), !!attachmentInfo && type === "party");
673
- environment.set(param.key, v);
674
- if (type === "enum" && t.kind === "call") {
675
- if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
676
- fail(v, `invalid ${param.key}`, `choose ${t.args.map(text).join(", ")}`);
677
- }
678
- else if (type === "list") {
679
- if (v.kind !== "list")
680
- fail(v, `${param.key} needs a list`, "write [value, value]");
681
- }
682
- else if (type === "party") {
683
- if (v.kind !== "name" || !isParty(v.value))
684
- failWithCode(actual, attachmentInfo ? "subject_party_unbound" : "HSX1001", `${param.key} needs a declared party`, "declare a party and use its name here");
685
- const party = document.parties[v.value];
686
- if ((party?.kind === "staff" && !party.role) ||
687
- (attachmentInfo && party?.kind === "person"))
688
- failWithCode(actual, "party_kind_mismatch", `${param.key} cannot bind ${v.value}`, "use a subject role, declared business, or staff with a role");
689
- resolvedParties.add(param.key);
690
- if (attachmentInfo)
691
- attachmentInfo.parties[param.key] = subjectPartyRoles.includes(v.value)
692
- ? { role: v.value }
693
- : { party: v.value };
694
- }
695
- else if (type === "ref") {
696
- const values = v.kind === "list" ? v.items : [v];
697
- if (v.kind === "list" && (t.kind !== "type" || !t.many))
698
- fail(v, `${param.key} accepts one reference`, "use one object name");
699
- if (!values.length || values.length > 16)
700
- fail(v, "reference union needs 1 to 16 objects", "use at most 16 distinct object names");
701
- const seen = new Set();
702
- for (const value of values) {
703
- if (value.kind !== "name")
704
- fail(value, "reference needs an object name", "name a declared object");
705
- const [root, ...tail] = value.value.split(".");
706
- const obj = objects.get(root);
707
- const assignment = assignments.get(root);
708
- const targetType = obj ? obj.name : assignment?.target;
709
- if ((!obj &&
710
- !assignment &&
711
- !document.instruments.some((inst) => inst.id === value.value)) ||
712
- (t.kind === "type" &&
713
- t.target &&
714
- [targetType, ...tail].join(".") !== t.target))
715
- fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
716
- if (seen.has(value.value))
717
- fail(value, "duplicate reference", "list each object once");
718
- seen.add(value.value);
719
- }
720
- // A many-reference tunable is a list even when one object is bound,
721
- // so report datasets and other value positions never see a bare name.
722
- if (t.kind === "type" && t.many && v.kind !== "list")
723
- environment.set(param.key, {
724
- kind: "list",
725
- items: [v],
726
- span: v.span,
727
- });
728
- }
729
- else if (type === "fee" || type === "split" || type === "policy") {
730
- if (v.kind !== "block")
731
- fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
732
- }
733
- else if (type === "money" ||
734
- type === "percent" ||
735
- type === "date" ||
736
- type === "duration" ||
737
- type === "integer" ||
738
- type === "text") {
739
- if (v.kind === "name" && v.value === "runtime")
777
+ try {
778
+ const actual = environment.get(param.key);
779
+ if (!actual)
740
780
  continue;
741
- const expected = type === "integer" ? "number" : type;
742
- if (v.kind !== expected &&
743
- !(type === "duration" &&
744
- (v.kind === "text" || v.kind === "name") &&
745
- /^P/.test(v.value)))
746
- fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
747
- try {
748
- const value = literal(v);
749
- const bounds = tunableBounds(t);
750
- if (bounds &&
751
- (BigInt(String(value)) < BigInt(bounds.minimum) ||
752
- BigInt(String(value)) > BigInt(bounds.maximum)))
753
- 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(", ")}`);
754
790
  }
755
- catch (error) {
756
- if (error instanceof CompileFailure)
757
- fail(v, `${param.key}: ${error.diagnostic.message}`, error.diagnostic.fix);
758
- throw error;
791
+ else if (type === "list") {
792
+ if (v.kind !== "list")
793
+ fail(v, `${param.key} needs a list`, "write [value, value]");
759
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 });
760
918
  }
761
919
  }
920
+ if (parameterErrors.length) {
921
+ bindingDiagnostics.push(...parameterErrors.slice(1));
922
+ throw new CompileFailure(parameterErrors[0]);
923
+ }
762
924
  const body = entries(decl.body);
763
925
  for (const constraint of asBlock(body.get("constraints")).entries) {
764
926
  const rule = constraint.value;
@@ -786,6 +948,8 @@ export function compile(source, options = {}) {
786
948
  "summary",
787
949
  "invariants",
788
950
  "constraints",
951
+ "dependencies",
952
+ "parameterDiagnostics",
789
953
  "reports",
790
954
  "revisioned",
791
955
  ].includes(key) &&
@@ -1387,6 +1551,33 @@ export function compile(source, options = {}) {
1387
1551
  };
1388
1552
  }
1389
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
+ }
1390
1581
  const a = {
1391
1582
  summary: slots.has("summary")
1392
1583
  ? String(data(slots.get("summary")))
@@ -1795,13 +1986,21 @@ export function compile(source, options = {}) {
1795
1986
  span: entry.value.span,
1796
1987
  };
1797
1988
  const templateFamily = resolveFamily(targetTemplate, entry, false);
1798
- addInstrument(template, instId, tunableBlock, entry.span, new Map(), new Map(), {
1799
- subjectKindId: decl.name,
1800
- attachmentName,
1801
- renames,
1802
- exposed,
1803
- parties,
1804
- }, 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
+ }
1805
2004
  const attachedInst = document.instruments.find((i) => i.id === instId);
1806
2005
  if (attachedInst?.actions.create) {
1807
2006
  const owned = new Set(attachedInst.calculate.map((node) => node.target));
@@ -1848,7 +2047,12 @@ export function compile(source, options = {}) {
1848
2047
  Object.values(attachedInst.actions).some((action) => action.subject?.requirements.some((requirement) => requirement.field.name === oldName));
1849
2048
  if (!found) {
1850
2049
  const renameEntry = renameEntries.get(oldName) ?? entry;
1851
- failWithCode(renameEntry, "subject_field_unknown", `rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`, "rename a declared subject requirement");
2050
+ const declared = attachedInst
2051
+ ? Object.values(attachedInst.actions).flatMap((action) => action.subject?.requirements.map((requirement) => requirement.field.name) ?? [])
2052
+ : [];
2053
+ failWithCode(renameEntry, "subject_field_unknown", `rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`, declared.length
2054
+ ? `rename one of: ${[...new Set(declared)].join(", ")}`
2055
+ : "rename a declared subject requirement");
1852
2056
  }
1853
2057
  }
1854
2058
  }
@@ -1861,7 +2065,7 @@ export function compile(source, options = {}) {
1861
2065
  }
1862
2066
  columns = columnsExpr.items.map(text);
1863
2067
  if (columns.length > 8) {
1864
- 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\`.`);
1865
2069
  }
1866
2070
  }
1867
2071
  document.objects.push({
@@ -1875,8 +2079,6 @@ export function compile(source, options = {}) {
1875
2079
  };
1876
2080
  for (const decl of program.decls) {
1877
2081
  if (decl.kind === "instrument") {
1878
- if (decl.parameters.length)
1879
- fail(decl, "program records cannot declare tunables", "put reusable instruments in a header");
1880
2082
  addInstrument(decl, decl.name, emptyBlock, decl.span);
1881
2083
  }
1882
2084
  if (decl.kind === "assignment") {
@@ -1890,6 +2092,13 @@ export function compile(source, options = {}) {
1890
2092
  compileObject(decl);
1891
2093
  }
1892
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
+ };
1893
2102
  // Propagate mandatory invoked action requirements
1894
2103
  let changedInvocations = true;
1895
2104
  let invocationIterations = 0;
@@ -2094,8 +2303,36 @@ export function compile(source, options = {}) {
2094
2303
  const origin = [...origins]
2095
2304
  .reverse()
2096
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
+ }
2097
2334
  return diagnostic({
2098
- code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
2335
+ code: i.code,
2099
2336
  message: `${i.path}: ${i.message}`,
2100
2337
  fix: i.fix,
2101
2338
  span: origin?.span ?? program.span,
@@ -2116,7 +2353,7 @@ export function compile(source, options = {}) {
2116
2353
  if (error instanceof CompileFailure)
2117
2354
  return {
2118
2355
  verdict: "invalid",
2119
- diagnostics: [diagnostic(error.diagnostic, "check")],
2356
+ diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
2120
2357
  };
2121
2358
  throw error;
2122
2359
  }