@hyperscale0/hsx 4.3.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.
- package/CHANGELOG.md +17 -0
- package/README.md +5 -4
- package/dist/src/binding-contract.d.ts +22 -0
- package/dist/src/binding-contract.d.ts.map +1 -0
- package/dist/src/binding-contract.js +74 -0
- package/dist/src/binding-contract.js.map +1 -0
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +340 -112
- package/dist/src/compile.js.map +1 -1
- package/dist/src/examples-bundle.d.ts.map +1 -1
- package/dist/src/examples-bundle.js +2 -1
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/headers.d.ts +13 -0
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +3 -0
- package/dist/src/headers.js.map +1 -1
- package/dist/src/std-bundle.js +8 -8
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/README.md +78 -3
- package/docs/examples.md +3 -2
- package/docs/headers.md +2 -2
- package/examples/lending.hsx +14 -3
- package/examples/repair-approval.hsx +43 -0
- package/package.json +3 -3
- package/src/binding-contract.ts +104 -0
- package/src/compile.ts +476 -171
- package/src/examples-bundle.ts +2 -1
- package/src/headers.ts +8 -0
- package/src/std-bundle.ts +8 -8
- package/src/version.ts +1 -1
- package/std/collections.hsx +1 -1
- package/std/escrow.hsx +1 -1
- package/std/financing.hsx +37 -23
- package/std/lending.hsx +6 -6
- package/std/marketplace.hsx +3 -3
- package/std/money.hsx +6 -6
- package/std/savings.hsx +1 -1
- package/std/travel.hsx +7 -7
package/dist/src/compile.js
CHANGED
|
@@ -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
|
-
|
|
579
|
-
|
|
580
|
-
:
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
760
|
-
if (
|
|
761
|
-
fail(v, `${param.key}
|
|
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
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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,
|
|
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
|
|
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: [
|
|
2356
|
+
diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
|
|
2129
2357
|
};
|
|
2130
2358
|
throw error;
|
|
2131
2359
|
}
|