@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.
- 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 +346 -109
- 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 +17 -0
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +23 -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 +488 -169
- package/src/examples-bundle.ts +2 -1
- package/src/headers.ts +26 -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,13 +583,96 @@ export function compile(source, options = {}) {
|
|
|
575
583
|
if (!actual) {
|
|
576
584
|
if (type.kind === "type" && type.optional)
|
|
577
585
|
continue;
|
|
578
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
756
|
-
if (
|
|
757
|
-
fail(v, `${param.key}
|
|
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
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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: [
|
|
2356
|
+
diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
|
|
2120
2357
|
};
|
|
2121
2358
|
throw error;
|
|
2122
2359
|
}
|