@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/src/compile.ts
CHANGED
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
type UdlSubjectRequirement,
|
|
23
23
|
type UdlValue,
|
|
24
24
|
} from "@hyperscale0/udl";
|
|
25
|
+
import {
|
|
26
|
+
bindingDependencies,
|
|
27
|
+
parameterDiagnostics,
|
|
28
|
+
BindingContractError,
|
|
29
|
+
} from "./binding-contract.ts";
|
|
25
30
|
import { tunableBounds } from "./tunables.ts";
|
|
26
31
|
import { parseProgram } from "./parse.ts";
|
|
27
32
|
import {
|
|
@@ -401,6 +406,7 @@ export function compile(
|
|
|
401
406
|
diagnostics: parsed.diagnostics.map((d) => diagnostic(d, "parse")),
|
|
402
407
|
};
|
|
403
408
|
const program = parsed.program;
|
|
409
|
+
const bindingDiagnostics: Diagnostic[] = [];
|
|
404
410
|
try {
|
|
405
411
|
if (program.header)
|
|
406
412
|
fail(
|
|
@@ -478,6 +484,14 @@ export function compile(
|
|
|
478
484
|
}
|
|
479
485
|
for (const decl of program.decls)
|
|
480
486
|
if (decl.kind === "instrument") templates.set(decl.name, decl);
|
|
487
|
+
for (const decl of program.decls) {
|
|
488
|
+
if (decl.kind === "instrument" && decl.parameters.length)
|
|
489
|
+
fail(
|
|
490
|
+
decl,
|
|
491
|
+
"This compiler cannot instantiate a parameterized instrument declared inside a program.",
|
|
492
|
+
"For this version, specialize the instrument with fixed bindings and remove its parameters. Custom reusable headers require a host-supplied library.",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
481
495
|
const document: UdlDocument = {
|
|
482
496
|
udl: 4,
|
|
483
497
|
version: 1,
|
|
@@ -545,6 +559,10 @@ export function compile(
|
|
|
545
559
|
}
|
|
546
560
|
}
|
|
547
561
|
const origins: CompileOriginMapEntry[] = [];
|
|
562
|
+
const refundBindings = new Map<
|
|
563
|
+
string,
|
|
564
|
+
{ parameter: string; span: Span; defaultState: string; action: string }[]
|
|
565
|
+
>();
|
|
548
566
|
const resolveFamily = (
|
|
549
567
|
rawPath: string,
|
|
550
568
|
expr: { span: Span; source?: string },
|
|
@@ -821,6 +839,7 @@ export function compile(
|
|
|
821
839
|
supplied.set(entry.key, entry.value);
|
|
822
840
|
}
|
|
823
841
|
const environment = new Map<string, Expr>(inherited);
|
|
842
|
+
const parameterErrors: Diagnostic[] = [];
|
|
824
843
|
for (const param of decl.parameters) {
|
|
825
844
|
const type =
|
|
826
845
|
param.value.kind === "default" ? param.value.type : param.value;
|
|
@@ -844,22 +863,118 @@ export function compile(
|
|
|
844
863
|
});
|
|
845
864
|
if (!actual) {
|
|
846
865
|
if (type.kind === "type" && type.optional) continue;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
partyParameter ? "subject_party_unbound" : "HSX1001",
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
866
|
+
parameterErrors.push({
|
|
867
|
+
span: origin,
|
|
868
|
+
code: partyParameter ? "subject_party_unbound" : "HSX1001",
|
|
869
|
+
message: partyParameter
|
|
870
|
+
? `\`${param.key}\` has no binding. An attached ${param.key} must resolve to a subject role or an eligible declared party.`
|
|
871
|
+
: `${id} needs ${param.key}`,
|
|
872
|
+
fix: partyParameter
|
|
873
|
+
? `Use \`${param.key}: actor\` for the initiating customer or \`${param.key}: owner\` for the object owner. Declare a business for a fixed company counterparty.`
|
|
874
|
+
: `add ${param.key}: value inside ${id}`,
|
|
875
|
+
related: [
|
|
876
|
+
{
|
|
877
|
+
source: declarationSources.get(decl) ?? "program",
|
|
878
|
+
span: param.span,
|
|
879
|
+
message: `Parameter ${param.key}`,
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
});
|
|
883
|
+
continue;
|
|
853
884
|
}
|
|
854
885
|
environment.set(param.key, actual);
|
|
855
886
|
}
|
|
856
|
-
|
|
857
|
-
|
|
887
|
+
let policyDiagnostics: ReturnType<typeof parameterDiagnostics>;
|
|
888
|
+
let dependencies: ReturnType<typeof bindingDependencies>;
|
|
889
|
+
try {
|
|
890
|
+
policyDiagnostics = parameterDiagnostics(decl);
|
|
891
|
+
dependencies = bindingDependencies(decl);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
if (!(error instanceof BindingContractError)) throw error;
|
|
894
|
+
throw new CompileFailure({
|
|
895
|
+
code: "HSX1001",
|
|
896
|
+
message: error.message,
|
|
897
|
+
fix: "Repair the header binding contract.",
|
|
898
|
+
span: error.entry.span,
|
|
899
|
+
source: declarationSources.get(decl) ?? "program",
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
for (const dependency of dependencies) {
|
|
903
|
+
const selected = environment.get(dependency.when);
|
|
904
|
+
if (
|
|
905
|
+
selected?.kind === "name" &&
|
|
906
|
+
selected.value === dependency.is &&
|
|
907
|
+
!environment.has(dependency.binding)
|
|
908
|
+
) {
|
|
909
|
+
throw new CompileFailure({
|
|
910
|
+
code: "HSX1001",
|
|
911
|
+
message: dependency.message.replaceAll(
|
|
912
|
+
"{attachment}",
|
|
913
|
+
attachmentInfo?.attachmentName ?? id,
|
|
914
|
+
),
|
|
915
|
+
fix: dependency.fix,
|
|
916
|
+
span: origin,
|
|
917
|
+
related: [
|
|
918
|
+
{
|
|
919
|
+
source: declarationSources.get(decl) ?? "program",
|
|
920
|
+
span: dependency.span,
|
|
921
|
+
message: "Conditional binding requirement",
|
|
922
|
+
},
|
|
923
|
+
],
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
for (const key of supplied.keys()) {
|
|
928
|
+
if (decl.parameters.some((p) => p.key === key)) continue;
|
|
929
|
+
const suppliedValue = supplied.get(key)!;
|
|
930
|
+
const requirements = decl.body.entries
|
|
931
|
+
.filter((entry) => entry.key.startsWith("action "))
|
|
932
|
+
.flatMap(
|
|
933
|
+
(entry) =>
|
|
934
|
+
asBlock(entries(asBlock(entry.value)).get("subject")).entries,
|
|
935
|
+
);
|
|
936
|
+
const moneyFields = [
|
|
937
|
+
...new Set(
|
|
938
|
+
requirements
|
|
939
|
+
.filter((entry) => {
|
|
940
|
+
const type = entry.value;
|
|
941
|
+
return type.kind === "name"
|
|
942
|
+
? type.value === "money"
|
|
943
|
+
: (type.kind === "type" || type.kind === "call") &&
|
|
944
|
+
type.name === "money";
|
|
945
|
+
})
|
|
946
|
+
.map((entry) => entry.key),
|
|
947
|
+
),
|
|
948
|
+
];
|
|
949
|
+
if (
|
|
950
|
+
attachmentInfo &&
|
|
951
|
+
(suppliedValue.kind === "money" || suppliedValue.kind === "name") &&
|
|
952
|
+
moneyFields.length === 1
|
|
953
|
+
) {
|
|
954
|
+
const field = moneyFields[0]!;
|
|
955
|
+
const object = objects.get(attachmentInfo.subjectKindId)!;
|
|
956
|
+
const candidates = asBlock(
|
|
957
|
+
entries(object.body).get("fields"),
|
|
958
|
+
).entries.filter((entry) =>
|
|
959
|
+
entry.value.kind === "name"
|
|
960
|
+
? entry.value.value === "money"
|
|
961
|
+
: (entry.value.kind === "type" || entry.value.kind === "call") &&
|
|
962
|
+
entry.value.name === "money",
|
|
963
|
+
);
|
|
964
|
+
const target =
|
|
965
|
+
candidates.length === 1 ? candidates[0]!.key : "yourDepositField";
|
|
858
966
|
fail(
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
`
|
|
967
|
+
suppliedValue,
|
|
968
|
+
`\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its funding action requires the object's \`${field}\` field.`,
|
|
969
|
+
`Remove \`${key}\`. To use your deposit field, add \`rename { ${field}: ${target} }\`.`,
|
|
862
970
|
);
|
|
971
|
+
}
|
|
972
|
+
fail(
|
|
973
|
+
suppliedValue,
|
|
974
|
+
`unknown tunable ${key}`,
|
|
975
|
+
`choose ${decl.parameters.map((p) => p.key).join(", ")}`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
863
978
|
const isParty = (name: string) =>
|
|
864
979
|
!!document.parties[name] ||
|
|
865
980
|
(!!attachmentInfo &&
|
|
@@ -907,6 +1022,44 @@ export function compile(
|
|
|
907
1022
|
assignment.name +
|
|
908
1023
|
type.slice(assignment.target.length).replaceAll(".", "_"),
|
|
909
1024
|
);
|
|
1025
|
+
if (
|
|
1026
|
+
expr.name === "object" &&
|
|
1027
|
+
matches.length !== 1 &&
|
|
1028
|
+
attachmentInfo
|
|
1029
|
+
) {
|
|
1030
|
+
const parameter =
|
|
1031
|
+
[...environment].find(([, value]) => value === expr)?.[0] ??
|
|
1032
|
+
decl.parameters.find(
|
|
1033
|
+
(p) =>
|
|
1034
|
+
p.value.kind === "default" &&
|
|
1035
|
+
p.value.value.span.start === expr.span.start,
|
|
1036
|
+
)?.key ??
|
|
1037
|
+
"reference";
|
|
1038
|
+
const local = (name: string) =>
|
|
1039
|
+
attachmentSubjects.has(name)
|
|
1040
|
+
? name.slice(attachmentInfo.subjectKindId.length + 1)
|
|
1041
|
+
: `${name} (program)`;
|
|
1042
|
+
const target = templates.get(type);
|
|
1043
|
+
const required =
|
|
1044
|
+
target?.parameters
|
|
1045
|
+
.filter(
|
|
1046
|
+
(p) =>
|
|
1047
|
+
p.value.kind !== "default" &&
|
|
1048
|
+
!(p.value.kind === "type" && p.value.optional),
|
|
1049
|
+
)
|
|
1050
|
+
.map((p) => p.key) ?? [];
|
|
1051
|
+
fail(
|
|
1052
|
+
{ span: origin },
|
|
1053
|
+
`Attachment \`${attachmentInfo.attachmentName}\` needs a \`${parameter}\` binding. ${
|
|
1054
|
+
matches.length === 0
|
|
1055
|
+
? `No \`${type}\` attachment exists on \`${attachmentInfo.subjectKindId}\`.`
|
|
1056
|
+
: `Several \`${type}\` attachments match on \`${attachmentInfo.subjectKindId}\`: ${matches.map(local).join(", ")}.`
|
|
1057
|
+
}`,
|
|
1058
|
+
matches.length
|
|
1059
|
+
? `Bind \`${parameter}\` explicitly to one of: ${matches.map(local).join(", ")}.`
|
|
1060
|
+
: `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.`,
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
910
1063
|
if (expr.name !== "all" && matches.length !== 1)
|
|
911
1064
|
failWithCode(
|
|
912
1065
|
expr,
|
|
@@ -979,151 +1132,224 @@ export function compile(
|
|
|
979
1132
|
return resolved;
|
|
980
1133
|
};
|
|
981
1134
|
for (const param of decl.parameters) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
if (v.kind !== "list")
|
|
1001
|
-
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
1002
|
-
} else if (type === "party") {
|
|
1003
|
-
if (v.kind !== "name" || !isParty(v.value))
|
|
1004
|
-
failWithCode(
|
|
1005
|
-
actual,
|
|
1006
|
-
attachmentInfo ? "subject_party_unbound" : "HSX1001",
|
|
1007
|
-
`${param.key} needs a declared party`,
|
|
1008
|
-
"declare a party and use its name here",
|
|
1009
|
-
);
|
|
1010
|
-
const party = document.parties[v.value];
|
|
1011
|
-
if (
|
|
1012
|
-
(party?.kind === "staff" && !party.role) ||
|
|
1013
|
-
(attachmentInfo && party?.kind === "person")
|
|
1014
|
-
)
|
|
1015
|
-
failWithCode(
|
|
1016
|
-
actual,
|
|
1017
|
-
"party_kind_mismatch",
|
|
1018
|
-
`${param.key} cannot bind ${v.value}`,
|
|
1019
|
-
"use a subject role, declared business, or staff with a role",
|
|
1020
|
-
);
|
|
1021
|
-
resolvedParties.add(param.key);
|
|
1022
|
-
if (attachmentInfo)
|
|
1023
|
-
attachmentInfo.parties[param.key] = subjectPartyRoles.includes(
|
|
1024
|
-
v.value as SubjectPartyRole,
|
|
1025
|
-
)
|
|
1026
|
-
? { role: v.value as SubjectPartyRole }
|
|
1027
|
-
: { party: v.value };
|
|
1028
|
-
} else if (type === "ref") {
|
|
1029
|
-
const values = v.kind === "list" ? v.items : [v];
|
|
1030
|
-
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
1031
|
-
fail(
|
|
1032
|
-
v,
|
|
1033
|
-
`${param.key} accepts one reference`,
|
|
1034
|
-
"use one object name",
|
|
1035
|
-
);
|
|
1036
|
-
if (!values.length || values.length > 16)
|
|
1037
|
-
fail(
|
|
1038
|
-
v,
|
|
1039
|
-
"reference union needs 1 to 16 objects",
|
|
1040
|
-
"use at most 16 distinct object names",
|
|
1041
|
-
);
|
|
1042
|
-
const seen = new Set<string>();
|
|
1043
|
-
for (const value of values) {
|
|
1044
|
-
if (value.kind !== "name")
|
|
1135
|
+
try {
|
|
1136
|
+
const actual = environment.get(param.key);
|
|
1137
|
+
if (!actual) continue;
|
|
1138
|
+
const t =
|
|
1139
|
+
param.value.kind === "default" ? param.value.type : param.value;
|
|
1140
|
+
const type =
|
|
1141
|
+
t.kind === "type" || t.kind === "call" ? t.name : text(t);
|
|
1142
|
+
const v =
|
|
1143
|
+
(supplied.has(param.key) && !attachmentInfo) || type === "enum"
|
|
1144
|
+
? actual
|
|
1145
|
+
: resolve(
|
|
1146
|
+
actual,
|
|
1147
|
+
new Set(),
|
|
1148
|
+
!!attachmentInfo && type === "party",
|
|
1149
|
+
);
|
|
1150
|
+
environment.set(param.key, v);
|
|
1151
|
+
if (type === "enum" && t.kind === "call") {
|
|
1152
|
+
if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
|
|
1045
1153
|
fail(
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
"
|
|
1154
|
+
v,
|
|
1155
|
+
`invalid ${param.key}`,
|
|
1156
|
+
`choose ${t.args.map(text).join(", ")}`,
|
|
1157
|
+
);
|
|
1158
|
+
} else if (type === "list") {
|
|
1159
|
+
if (v.kind !== "list")
|
|
1160
|
+
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
1161
|
+
} else if (type === "party") {
|
|
1162
|
+
if (v.kind !== "name" || !isParty(v.value))
|
|
1163
|
+
failWithCode(
|
|
1164
|
+
actual,
|
|
1165
|
+
attachmentInfo ? "subject_party_unbound" : "HSX1001",
|
|
1166
|
+
attachmentInfo
|
|
1167
|
+
? `\`${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.`
|
|
1168
|
+
: `${param.key} needs a declared party`,
|
|
1169
|
+
attachmentInfo
|
|
1170
|
+
? `Use \`${param.key}: actor\` for the initiating customer or \`${param.key}: owner\` for the object owner. Declare a business for a fixed company counterparty.`
|
|
1171
|
+
: "declare a party and use its name here",
|
|
1049
1172
|
);
|
|
1050
|
-
const
|
|
1051
|
-
const obj = objects.get(root!);
|
|
1052
|
-
const assignment = assignments.get(root!);
|
|
1053
|
-
const targetType = obj ? obj.name : assignment?.target;
|
|
1173
|
+
const party = document.parties[v.value];
|
|
1054
1174
|
if (
|
|
1055
|
-
(!
|
|
1056
|
-
|
|
1057
|
-
!document.instruments.some(
|
|
1058
|
-
(inst) => inst.id === value.value,
|
|
1059
|
-
)) ||
|
|
1060
|
-
(t.kind === "type" &&
|
|
1061
|
-
t.target &&
|
|
1062
|
-
[targetType, ...tail].join(".") !== t.target)
|
|
1175
|
+
(party?.kind === "staff" && !party.role) ||
|
|
1176
|
+
(attachmentInfo && party?.kind === "person")
|
|
1063
1177
|
)
|
|
1178
|
+
failWithCode(
|
|
1179
|
+
actual,
|
|
1180
|
+
"party_kind_mismatch",
|
|
1181
|
+
party?.kind === "person"
|
|
1182
|
+
? `\`${v.value}\` is a declared person. Attachments resolve customer identity through \`owner\` or \`actor\`, rather than a fixed person declaration.`
|
|
1183
|
+
: `\`${v.value}\` is declared staff without a permission role.`,
|
|
1184
|
+
"Replace this binding with the appropriate subject role. Use declared businesses for fixed counterparties and permission-bearing staff roles for authorized actions.",
|
|
1185
|
+
);
|
|
1186
|
+
resolvedParties.add(param.key);
|
|
1187
|
+
if (attachmentInfo)
|
|
1188
|
+
attachmentInfo.parties[param.key] = subjectPartyRoles.includes(
|
|
1189
|
+
v.value as SubjectPartyRole,
|
|
1190
|
+
)
|
|
1191
|
+
? { role: v.value as SubjectPartyRole }
|
|
1192
|
+
: { party: v.value };
|
|
1193
|
+
} else if (type === "ref") {
|
|
1194
|
+
const values = v.kind === "list" ? v.items : [v];
|
|
1195
|
+
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
1064
1196
|
fail(
|
|
1065
|
-
|
|
1066
|
-
`${param.key}
|
|
1067
|
-
|
|
1197
|
+
v,
|
|
1198
|
+
`${param.key} accepts one reference`,
|
|
1199
|
+
"use one object name",
|
|
1068
1200
|
);
|
|
1069
|
-
if (
|
|
1070
|
-
fail(value, "duplicate reference", "list each object once");
|
|
1071
|
-
seen.add(value.value);
|
|
1072
|
-
}
|
|
1073
|
-
// A many-reference tunable is a list even when one object is bound,
|
|
1074
|
-
// so report datasets and other value positions never see a bare name.
|
|
1075
|
-
if (t.kind === "type" && t.many && v.kind !== "list")
|
|
1076
|
-
environment.set(param.key, {
|
|
1077
|
-
kind: "list",
|
|
1078
|
-
items: [v],
|
|
1079
|
-
span: v.span,
|
|
1080
|
-
});
|
|
1081
|
-
} else if (type === "fee" || type === "split" || type === "policy") {
|
|
1082
|
-
if (v.kind !== "block")
|
|
1083
|
-
fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
|
|
1084
|
-
} else if (
|
|
1085
|
-
type === "money" ||
|
|
1086
|
-
type === "percent" ||
|
|
1087
|
-
type === "date" ||
|
|
1088
|
-
type === "duration" ||
|
|
1089
|
-
type === "integer" ||
|
|
1090
|
-
type === "text"
|
|
1091
|
-
) {
|
|
1092
|
-
if (v.kind === "name" && v.value === "runtime") continue;
|
|
1093
|
-
const expected = type === "integer" ? "number" : type;
|
|
1094
|
-
if (
|
|
1095
|
-
v.kind !== expected &&
|
|
1096
|
-
!(
|
|
1097
|
-
type === "duration" &&
|
|
1098
|
-
(v.kind === "text" || v.kind === "name") &&
|
|
1099
|
-
/^P/.test(v.value)
|
|
1100
|
-
)
|
|
1101
|
-
)
|
|
1102
|
-
fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
|
|
1103
|
-
try {
|
|
1104
|
-
const value = literal(v);
|
|
1105
|
-
const bounds = tunableBounds(t);
|
|
1106
|
-
if (
|
|
1107
|
-
bounds &&
|
|
1108
|
-
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
1109
|
-
BigInt(String(value)) > BigInt(bounds.maximum))
|
|
1110
|
-
)
|
|
1201
|
+
if (!values.length || values.length > 16)
|
|
1111
1202
|
fail(
|
|
1112
1203
|
v,
|
|
1113
|
-
|
|
1114
|
-
"
|
|
1204
|
+
"reference union needs 1 to 16 objects",
|
|
1205
|
+
"use at most 16 distinct object names",
|
|
1115
1206
|
);
|
|
1116
|
-
|
|
1117
|
-
|
|
1207
|
+
const seen = new Set<string>();
|
|
1208
|
+
for (const value of values) {
|
|
1209
|
+
if (value.kind !== "name")
|
|
1210
|
+
fail(
|
|
1211
|
+
value,
|
|
1212
|
+
"reference needs an object name",
|
|
1213
|
+
"name a declared object",
|
|
1214
|
+
);
|
|
1215
|
+
const [root, ...tail] = value.value.split(".");
|
|
1216
|
+
const obj = objects.get(root!);
|
|
1217
|
+
const assignment = assignments.get(root!);
|
|
1218
|
+
const targetType = obj ? obj.name : assignment?.target;
|
|
1219
|
+
if (
|
|
1220
|
+
(!obj &&
|
|
1221
|
+
!assignment &&
|
|
1222
|
+
!document.instruments.some(
|
|
1223
|
+
(inst) => inst.id === value.value,
|
|
1224
|
+
)) ||
|
|
1225
|
+
(t.kind === "type" &&
|
|
1226
|
+
t.target &&
|
|
1227
|
+
[targetType, ...tail].join(".") !== t.target)
|
|
1228
|
+
)
|
|
1229
|
+
fail(
|
|
1230
|
+
value,
|
|
1231
|
+
`${param.key} has the wrong object type`,
|
|
1232
|
+
`use an object of type ${t.kind === "type" ? t.target : "ref"}`,
|
|
1233
|
+
);
|
|
1234
|
+
if (seen.has(value.value))
|
|
1235
|
+
fail(value, "duplicate reference", "list each object once");
|
|
1236
|
+
seen.add(value.value);
|
|
1237
|
+
}
|
|
1238
|
+
// A many-reference tunable is a list even when one object is bound,
|
|
1239
|
+
// so report datasets and other value positions never see a bare name.
|
|
1240
|
+
if (t.kind === "type" && t.many && v.kind !== "list")
|
|
1241
|
+
environment.set(param.key, {
|
|
1242
|
+
kind: "list",
|
|
1243
|
+
items: [v],
|
|
1244
|
+
span: v.span,
|
|
1245
|
+
});
|
|
1246
|
+
} else if (type === "fee" || type === "split" || type === "policy") {
|
|
1247
|
+
if (v.kind !== "block")
|
|
1118
1248
|
fail(
|
|
1119
1249
|
v,
|
|
1120
|
-
`${param.key}
|
|
1121
|
-
|
|
1250
|
+
`${param.key} needs a block`,
|
|
1251
|
+
`write ${param.key} { ... }`,
|
|
1122
1252
|
);
|
|
1123
|
-
|
|
1253
|
+
} else if (
|
|
1254
|
+
type === "money" ||
|
|
1255
|
+
type === "percent" ||
|
|
1256
|
+
type === "date" ||
|
|
1257
|
+
type === "duration" ||
|
|
1258
|
+
type === "integer" ||
|
|
1259
|
+
type === "text"
|
|
1260
|
+
) {
|
|
1261
|
+
if (v.kind === "name" && v.value === "runtime") continue;
|
|
1262
|
+
const expected = type === "integer" ? "number" : type;
|
|
1263
|
+
if (
|
|
1264
|
+
v.kind !== expected &&
|
|
1265
|
+
!(
|
|
1266
|
+
type === "duration" &&
|
|
1267
|
+
(v.kind === "text" || v.kind === "name") &&
|
|
1268
|
+
/^P/.test(v.value)
|
|
1269
|
+
)
|
|
1270
|
+
) {
|
|
1271
|
+
if (type === "money" && v.kind === "name" && attachmentInfo) {
|
|
1272
|
+
const target = v.value.replace(/^subject\./, "");
|
|
1273
|
+
const fallback =
|
|
1274
|
+
param.value.kind === "default"
|
|
1275
|
+
? param.value.value
|
|
1276
|
+
: undefined;
|
|
1277
|
+
const advice =
|
|
1278
|
+
fallback?.kind === "name" && fallback.value === "runtime"
|
|
1279
|
+
? `omit \`${param.key}\``
|
|
1280
|
+
: `set \`${param.key}: runtime\``;
|
|
1281
|
+
fail(
|
|
1282
|
+
v,
|
|
1283
|
+
`\`${param.key}\` cannot read \`${v.value}\` as a tunable. This position accepts a fixed money amount or a runtime amount.`,
|
|
1284
|
+
`For a varying deposit, ${advice} and add \`rename { ${param.key}: ${target} }\`. Use a literal only for a fixed charge.`,
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
if (type === "money" && v.kind === "percent") {
|
|
1288
|
+
const policy = policyDiagnostics.find(
|
|
1289
|
+
(policy) => policy.parameter === param.key,
|
|
1290
|
+
);
|
|
1291
|
+
fail(
|
|
1292
|
+
v,
|
|
1293
|
+
`\`${assignments.get(id)?.target ?? decl.name}.${param.key}\` accepts ${policy?.accepts ?? "a fixed amount"}. It cannot express ${policy?.percentage ?? "a percentage-based charge"}.`,
|
|
1294
|
+
policy?.fix ??
|
|
1295
|
+
"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.",
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
const value = literal(v);
|
|
1302
|
+
const bounds = tunableBounds(t);
|
|
1303
|
+
if (
|
|
1304
|
+
bounds &&
|
|
1305
|
+
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
1306
|
+
BigInt(String(value)) > BigInt(bounds.maximum))
|
|
1307
|
+
)
|
|
1308
|
+
fail(
|
|
1309
|
+
v,
|
|
1310
|
+
`${param.key} is outside ${bounds.minimum}..${bounds.maximum}`,
|
|
1311
|
+
"choose a value inside the tunable's bounds",
|
|
1312
|
+
);
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
if (error instanceof CompileFailure)
|
|
1315
|
+
fail(
|
|
1316
|
+
v,
|
|
1317
|
+
`${param.key}: ${error.diagnostic.message}`,
|
|
1318
|
+
error.diagnostic.fix,
|
|
1319
|
+
);
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1124
1322
|
}
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (!(error instanceof CompileFailure)) throw error;
|
|
1325
|
+
const related = [
|
|
1326
|
+
{
|
|
1327
|
+
source: declarationSources.get(decl) ?? "program",
|
|
1328
|
+
span: param.span,
|
|
1329
|
+
message: `Parameter ${param.key}`,
|
|
1330
|
+
},
|
|
1331
|
+
];
|
|
1332
|
+
const actual = supplied.get(param.key);
|
|
1333
|
+
const party =
|
|
1334
|
+
actual?.kind === "name"
|
|
1335
|
+
? program.decls.find(
|
|
1336
|
+
(entry) =>
|
|
1337
|
+
entry.kind === "party" && entry.name === actual.value,
|
|
1338
|
+
)
|
|
1339
|
+
: undefined;
|
|
1340
|
+
if (party?.kind === "party")
|
|
1341
|
+
related.push({
|
|
1342
|
+
source: "program",
|
|
1343
|
+
span: party.span,
|
|
1344
|
+
message: `Declared party ${party.name}`,
|
|
1345
|
+
});
|
|
1346
|
+
parameterErrors.push({ ...error.diagnostic, related });
|
|
1125
1347
|
}
|
|
1126
1348
|
}
|
|
1349
|
+
if (parameterErrors.length) {
|
|
1350
|
+
bindingDiagnostics.push(...parameterErrors.slice(1));
|
|
1351
|
+
throw new CompileFailure(parameterErrors[0]!);
|
|
1352
|
+
}
|
|
1127
1353
|
const body = entries(decl.body);
|
|
1128
1354
|
for (const constraint of asBlock(body.get("constraints")).entries) {
|
|
1129
1355
|
const rule = constraint.value;
|
|
@@ -1170,6 +1396,8 @@ export function compile(
|
|
|
1170
1396
|
"summary",
|
|
1171
1397
|
"invariants",
|
|
1172
1398
|
"constraints",
|
|
1399
|
+
"dependencies",
|
|
1400
|
+
"parameterDiagnostics",
|
|
1173
1401
|
"reports",
|
|
1174
1402
|
"revisioned",
|
|
1175
1403
|
].includes(key) &&
|
|
@@ -1922,6 +2150,39 @@ export function compile(
|
|
|
1922
2150
|
};
|
|
1923
2151
|
}
|
|
1924
2152
|
}
|
|
2153
|
+
const fromBinding = slots.get("from");
|
|
2154
|
+
if (
|
|
2155
|
+
fromBinding?.kind === "name" &&
|
|
2156
|
+
fromBinding.value.includes(".") &&
|
|
2157
|
+
slots.has("moves")
|
|
2158
|
+
) {
|
|
2159
|
+
const [parameter, member] = fromBinding.value.split(".");
|
|
2160
|
+
const suppliedPolicy = supplied.get(parameter!);
|
|
2161
|
+
if (suppliedPolicy?.kind === "block") {
|
|
2162
|
+
const selected = entries(suppliedPolicy).get(member!);
|
|
2163
|
+
const parameterDecl = decl.parameters.find(
|
|
2164
|
+
(entry) => entry.key === parameter,
|
|
2165
|
+
);
|
|
2166
|
+
const fallback =
|
|
2167
|
+
parameterDecl?.value.kind === "default"
|
|
2168
|
+
? parameterDecl.value.value
|
|
2169
|
+
: undefined;
|
|
2170
|
+
const defaultState =
|
|
2171
|
+
fallback?.kind === "block"
|
|
2172
|
+
? entries(fallback).get(member!)
|
|
2173
|
+
: undefined;
|
|
2174
|
+
if (selected && defaultState?.kind === "name") {
|
|
2175
|
+
const bindings = refundBindings.get(id) ?? [];
|
|
2176
|
+
bindings.push({
|
|
2177
|
+
parameter: member!,
|
|
2178
|
+
span: selected.span,
|
|
2179
|
+
defaultState: defaultState.value,
|
|
2180
|
+
action: name,
|
|
2181
|
+
});
|
|
2182
|
+
refundBindings.set(id, bindings);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
1925
2186
|
const a: UdlAction = {
|
|
1926
2187
|
summary: slots.has("summary")
|
|
1927
2188
|
? String(data(slots.get("summary")!))
|
|
@@ -2485,23 +2746,28 @@ export function compile(
|
|
|
2485
2746
|
};
|
|
2486
2747
|
|
|
2487
2748
|
const templateFamily = resolveFamily(targetTemplate, entry, false);
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2749
|
+
try {
|
|
2750
|
+
addInstrument(
|
|
2751
|
+
template,
|
|
2752
|
+
instId,
|
|
2753
|
+
tunableBlock,
|
|
2754
|
+
entry.span,
|
|
2755
|
+
new Map(),
|
|
2756
|
+
new Map(),
|
|
2757
|
+
{
|
|
2758
|
+
subjectKindId: decl.name,
|
|
2759
|
+
attachmentName,
|
|
2760
|
+
renames,
|
|
2761
|
+
exposed,
|
|
2762
|
+
parties,
|
|
2763
|
+
},
|
|
2764
|
+
templateFamily ? { ...templateFamily } : undefined,
|
|
2765
|
+
);
|
|
2766
|
+
} catch (error) {
|
|
2767
|
+
if (!(error instanceof CompileFailure)) throw error;
|
|
2768
|
+
bindingDiagnostics.push(error.diagnostic);
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2505
2771
|
const attachedInst = document.instruments.find((i) => i.id === instId);
|
|
2506
2772
|
if (attachedInst?.actions.create) {
|
|
2507
2773
|
const owned = new Set(
|
|
@@ -2562,11 +2828,21 @@ export function compile(
|
|
|
2562
2828
|
);
|
|
2563
2829
|
if (!found) {
|
|
2564
2830
|
const renameEntry = renameEntries.get(oldName) ?? entry;
|
|
2831
|
+
const declared = attachedInst
|
|
2832
|
+
? Object.values(attachedInst.actions).flatMap(
|
|
2833
|
+
(action) =>
|
|
2834
|
+
action.subject?.requirements.map(
|
|
2835
|
+
(requirement) => requirement.field.name,
|
|
2836
|
+
) ?? [],
|
|
2837
|
+
)
|
|
2838
|
+
: [];
|
|
2565
2839
|
failWithCode(
|
|
2566
2840
|
renameEntry,
|
|
2567
2841
|
"subject_field_unknown",
|
|
2568
2842
|
`rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`,
|
|
2569
|
-
|
|
2843
|
+
declared.length
|
|
2844
|
+
? `rename one of: ${[...new Set(declared)].join(", ")}`
|
|
2845
|
+
: "rename a declared subject requirement",
|
|
2570
2846
|
);
|
|
2571
2847
|
}
|
|
2572
2848
|
}
|
|
@@ -2587,8 +2863,8 @@ export function compile(
|
|
|
2587
2863
|
if (columns.length > 8) {
|
|
2588
2864
|
fail(
|
|
2589
2865
|
columnsExpr,
|
|
2590
|
-
|
|
2591
|
-
"
|
|
2866
|
+
`The object list selects ${columns.length} columns; this release supports 8. The object may retain all its fields.`,
|
|
2867
|
+
`Remove ${columns.length === 9 ? "one name" : `${columns.length - 8} names`} from \`columns\`, not from \`fields\`.`,
|
|
2592
2868
|
);
|
|
2593
2869
|
}
|
|
2594
2870
|
}
|
|
@@ -2604,12 +2880,6 @@ export function compile(
|
|
|
2604
2880
|
};
|
|
2605
2881
|
for (const decl of program.decls) {
|
|
2606
2882
|
if (decl.kind === "instrument") {
|
|
2607
|
-
if (decl.parameters.length)
|
|
2608
|
-
fail(
|
|
2609
|
-
decl,
|
|
2610
|
-
"program records cannot declare tunables",
|
|
2611
|
-
"put reusable instruments in a header",
|
|
2612
|
-
);
|
|
2613
2883
|
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
2614
2884
|
}
|
|
2615
2885
|
if (decl.kind === "assignment") {
|
|
@@ -2636,6 +2906,13 @@ export function compile(
|
|
|
2636
2906
|
compileObject(decl);
|
|
2637
2907
|
}
|
|
2638
2908
|
}
|
|
2909
|
+
if (bindingDiagnostics.length)
|
|
2910
|
+
return {
|
|
2911
|
+
verdict: "invalid",
|
|
2912
|
+
diagnostics: bindingDiagnostics
|
|
2913
|
+
.sort((a, b) => a.span.start - b.span.start)
|
|
2914
|
+
.map((d) => diagnostic(d, "check")),
|
|
2915
|
+
};
|
|
2639
2916
|
// Propagate mandatory invoked action requirements
|
|
2640
2917
|
let changedInvocations = true;
|
|
2641
2918
|
let invocationIterations = 0;
|
|
@@ -2890,9 +3167,49 @@ export function compile(
|
|
|
2890
3167
|
const origin = [...origins]
|
|
2891
3168
|
.reverse()
|
|
2892
3169
|
.find((o) => i.path.startsWith(o.path));
|
|
3170
|
+
const index = /^\$\.instruments\[(\d+)\]/.exec(i.path)?.[1];
|
|
3171
|
+
const instrument =
|
|
3172
|
+
index === undefined
|
|
3173
|
+
? undefined
|
|
3174
|
+
: document.instruments[Number(index)];
|
|
3175
|
+
const attachment = document.objects
|
|
3176
|
+
.flatMap((object) => object.attachments)
|
|
3177
|
+
.find((attachment) => attachment.instrument === instrument?.id);
|
|
3178
|
+
const stranded = i.stranded;
|
|
3179
|
+
if (instrument && stranded?.accounts.length) {
|
|
3180
|
+
const binding = refundBindings
|
|
3181
|
+
.get(instrument.id)
|
|
3182
|
+
?.find(
|
|
3183
|
+
(binding) =>
|
|
3184
|
+
binding.defaultState === stranded.state &&
|
|
3185
|
+
instrument.actions[binding.action]?.moves.some(
|
|
3186
|
+
(move) =>
|
|
3187
|
+
"amount" in move &&
|
|
3188
|
+
stranded.accounts.some(
|
|
3189
|
+
(account) => move.from === `self.${account}`,
|
|
3190
|
+
),
|
|
3191
|
+
),
|
|
3192
|
+
);
|
|
3193
|
+
return diagnostic(
|
|
3194
|
+
{
|
|
3195
|
+
code: i.code,
|
|
3196
|
+
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.`,
|
|
3197
|
+
fix: binding
|
|
3198
|
+
? `Restore \`${binding.parameter}: ${stranded.state}\`, or author a complete refund path for every reachable funded state.`
|
|
3199
|
+
: "Author a complete refund path for every reachable funded state.",
|
|
3200
|
+
span: binding?.span ?? origin?.span ?? program.span,
|
|
3201
|
+
related: stranded.accounts.map((account) => ({
|
|
3202
|
+
source: "program",
|
|
3203
|
+
span: origin?.span ?? program.span,
|
|
3204
|
+
message: `State path: ${instrument.lifecycle.initial} -> ${(stranded.paths[account] ?? stranded.actions).join(" -> ")} -> ${stranded.state}; owned accounts: ${account}.`,
|
|
3205
|
+
})),
|
|
3206
|
+
},
|
|
3207
|
+
"lower",
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
2893
3210
|
return diagnostic(
|
|
2894
3211
|
{
|
|
2895
|
-
code: i.code
|
|
3212
|
+
code: i.code,
|
|
2896
3213
|
message: `${i.path}: ${i.message}`,
|
|
2897
3214
|
fix: i.fix,
|
|
2898
3215
|
span: origin?.span ?? program.span,
|
|
@@ -2914,7 +3231,9 @@ export function compile(
|
|
|
2914
3231
|
if (error instanceof CompileFailure)
|
|
2915
3232
|
return {
|
|
2916
3233
|
verdict: "invalid",
|
|
2917
|
-
diagnostics: [
|
|
3234
|
+
diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) =>
|
|
3235
|
+
diagnostic(d, "check"),
|
|
3236
|
+
),
|
|
2918
3237
|
};
|
|
2919
3238
|
throw error;
|
|
2920
3239
|
}
|