@hyperscale0/hsx 4.3.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -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/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,24 +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.`
|
|
853
874
|
: `add ${param.key}: value inside ${id}`,
|
|
854
|
-
|
|
875
|
+
related: [
|
|
876
|
+
{
|
|
877
|
+
source: declarationSources.get(decl) ?? "program",
|
|
878
|
+
span: param.span,
|
|
879
|
+
message: `Parameter ${param.key}`,
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
});
|
|
883
|
+
continue;
|
|
855
884
|
}
|
|
856
885
|
environment.set(param.key, actual);
|
|
857
886
|
}
|
|
858
|
-
|
|
859
|
-
|
|
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";
|
|
860
966
|
fail(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
`
|
|
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} }\`.`,
|
|
864
970
|
);
|
|
971
|
+
}
|
|
972
|
+
fail(
|
|
973
|
+
suppliedValue,
|
|
974
|
+
`unknown tunable ${key}`,
|
|
975
|
+
`choose ${decl.parameters.map((p) => p.key).join(", ")}`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
865
978
|
const isParty = (name: string) =>
|
|
866
979
|
!!document.parties[name] ||
|
|
867
980
|
(!!attachmentInfo &&
|
|
@@ -909,6 +1022,44 @@ export function compile(
|
|
|
909
1022
|
assignment.name +
|
|
910
1023
|
type.slice(assignment.target.length).replaceAll(".", "_"),
|
|
911
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
|
+
}
|
|
912
1063
|
if (expr.name !== "all" && matches.length !== 1)
|
|
913
1064
|
failWithCode(
|
|
914
1065
|
expr,
|
|
@@ -981,153 +1132,224 @@ export function compile(
|
|
|
981
1132
|
return resolved;
|
|
982
1133
|
};
|
|
983
1134
|
for (const param of decl.parameters) {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
if (v.kind !== "list")
|
|
1003
|
-
fail(v, `${param.key} needs a list`, "write [value, value]");
|
|
1004
|
-
} else if (type === "party") {
|
|
1005
|
-
if (v.kind !== "name" || !isParty(v.value))
|
|
1006
|
-
failWithCode(
|
|
1007
|
-
actual,
|
|
1008
|
-
attachmentInfo ? "subject_party_unbound" : "HSX1001",
|
|
1009
|
-
`${param.key} needs a declared party`,
|
|
1010
|
-
attachmentInfo
|
|
1011
|
-
? `bind ${param.key} to ${[...subjectPartyRoles, ...Object.keys(document.parties)].join(", ")} or declare a party`
|
|
1012
|
-
: "declare a party and use its name here",
|
|
1013
|
-
);
|
|
1014
|
-
const party = document.parties[v.value];
|
|
1015
|
-
if (
|
|
1016
|
-
(party?.kind === "staff" && !party.role) ||
|
|
1017
|
-
(attachmentInfo && party?.kind === "person")
|
|
1018
|
-
)
|
|
1019
|
-
failWithCode(
|
|
1020
|
-
actual,
|
|
1021
|
-
"party_kind_mismatch",
|
|
1022
|
-
`${param.key} cannot bind ${v.value}`,
|
|
1023
|
-
"use a subject role, declared business, or staff with a role",
|
|
1024
|
-
);
|
|
1025
|
-
resolvedParties.add(param.key);
|
|
1026
|
-
if (attachmentInfo)
|
|
1027
|
-
attachmentInfo.parties[param.key] = subjectPartyRoles.includes(
|
|
1028
|
-
v.value as SubjectPartyRole,
|
|
1029
|
-
)
|
|
1030
|
-
? { role: v.value as SubjectPartyRole }
|
|
1031
|
-
: { party: v.value };
|
|
1032
|
-
} else if (type === "ref") {
|
|
1033
|
-
const values = v.kind === "list" ? v.items : [v];
|
|
1034
|
-
if (v.kind === "list" && (t.kind !== "type" || !t.many))
|
|
1035
|
-
fail(
|
|
1036
|
-
v,
|
|
1037
|
-
`${param.key} accepts one reference`,
|
|
1038
|
-
"use one object name",
|
|
1039
|
-
);
|
|
1040
|
-
if (!values.length || values.length > 16)
|
|
1041
|
-
fail(
|
|
1042
|
-
v,
|
|
1043
|
-
"reference union needs 1 to 16 objects",
|
|
1044
|
-
"use at most 16 distinct object names",
|
|
1045
|
-
);
|
|
1046
|
-
const seen = new Set<string>();
|
|
1047
|
-
for (const value of values) {
|
|
1048
|
-
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))
|
|
1049
1153
|
fail(
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
"
|
|
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",
|
|
1053
1172
|
);
|
|
1054
|
-
const
|
|
1055
|
-
const obj = objects.get(root!);
|
|
1056
|
-
const assignment = assignments.get(root!);
|
|
1057
|
-
const targetType = obj ? obj.name : assignment?.target;
|
|
1173
|
+
const party = document.parties[v.value];
|
|
1058
1174
|
if (
|
|
1059
|
-
(!
|
|
1060
|
-
|
|
1061
|
-
!document.instruments.some(
|
|
1062
|
-
(inst) => inst.id === value.value,
|
|
1063
|
-
)) ||
|
|
1064
|
-
(t.kind === "type" &&
|
|
1065
|
-
t.target &&
|
|
1066
|
-
[targetType, ...tail].join(".") !== t.target)
|
|
1175
|
+
(party?.kind === "staff" && !party.role) ||
|
|
1176
|
+
(attachmentInfo && party?.kind === "person")
|
|
1067
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))
|
|
1068
1196
|
fail(
|
|
1069
|
-
|
|
1070
|
-
`${param.key}
|
|
1071
|
-
|
|
1197
|
+
v,
|
|
1198
|
+
`${param.key} accepts one reference`,
|
|
1199
|
+
"use one object name",
|
|
1072
1200
|
);
|
|
1073
|
-
if (
|
|
1074
|
-
fail(value, "duplicate reference", "list each object once");
|
|
1075
|
-
seen.add(value.value);
|
|
1076
|
-
}
|
|
1077
|
-
// A many-reference tunable is a list even when one object is bound,
|
|
1078
|
-
// so report datasets and other value positions never see a bare name.
|
|
1079
|
-
if (t.kind === "type" && t.many && v.kind !== "list")
|
|
1080
|
-
environment.set(param.key, {
|
|
1081
|
-
kind: "list",
|
|
1082
|
-
items: [v],
|
|
1083
|
-
span: v.span,
|
|
1084
|
-
});
|
|
1085
|
-
} else if (type === "fee" || type === "split" || type === "policy") {
|
|
1086
|
-
if (v.kind !== "block")
|
|
1087
|
-
fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
|
|
1088
|
-
} else if (
|
|
1089
|
-
type === "money" ||
|
|
1090
|
-
type === "percent" ||
|
|
1091
|
-
type === "date" ||
|
|
1092
|
-
type === "duration" ||
|
|
1093
|
-
type === "integer" ||
|
|
1094
|
-
type === "text"
|
|
1095
|
-
) {
|
|
1096
|
-
if (v.kind === "name" && v.value === "runtime") continue;
|
|
1097
|
-
const expected = type === "integer" ? "number" : type;
|
|
1098
|
-
if (
|
|
1099
|
-
v.kind !== expected &&
|
|
1100
|
-
!(
|
|
1101
|
-
type === "duration" &&
|
|
1102
|
-
(v.kind === "text" || v.kind === "name") &&
|
|
1103
|
-
/^P/.test(v.value)
|
|
1104
|
-
)
|
|
1105
|
-
)
|
|
1106
|
-
fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
|
|
1107
|
-
try {
|
|
1108
|
-
const value = literal(v);
|
|
1109
|
-
const bounds = tunableBounds(t);
|
|
1110
|
-
if (
|
|
1111
|
-
bounds &&
|
|
1112
|
-
(BigInt(String(value)) < BigInt(bounds.minimum) ||
|
|
1113
|
-
BigInt(String(value)) > BigInt(bounds.maximum))
|
|
1114
|
-
)
|
|
1201
|
+
if (!values.length || values.length > 16)
|
|
1115
1202
|
fail(
|
|
1116
1203
|
v,
|
|
1117
|
-
|
|
1118
|
-
"
|
|
1204
|
+
"reference union needs 1 to 16 objects",
|
|
1205
|
+
"use at most 16 distinct object names",
|
|
1119
1206
|
);
|
|
1120
|
-
|
|
1121
|
-
|
|
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")
|
|
1122
1248
|
fail(
|
|
1123
1249
|
v,
|
|
1124
|
-
`${param.key}
|
|
1125
|
-
|
|
1250
|
+
`${param.key} needs a block`,
|
|
1251
|
+
`write ${param.key} { ... }`,
|
|
1126
1252
|
);
|
|
1127
|
-
|
|
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
|
+
}
|
|
1128
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 });
|
|
1129
1347
|
}
|
|
1130
1348
|
}
|
|
1349
|
+
if (parameterErrors.length) {
|
|
1350
|
+
bindingDiagnostics.push(...parameterErrors.slice(1));
|
|
1351
|
+
throw new CompileFailure(parameterErrors[0]!);
|
|
1352
|
+
}
|
|
1131
1353
|
const body = entries(decl.body);
|
|
1132
1354
|
for (const constraint of asBlock(body.get("constraints")).entries) {
|
|
1133
1355
|
const rule = constraint.value;
|
|
@@ -1174,6 +1396,8 @@ export function compile(
|
|
|
1174
1396
|
"summary",
|
|
1175
1397
|
"invariants",
|
|
1176
1398
|
"constraints",
|
|
1399
|
+
"dependencies",
|
|
1400
|
+
"parameterDiagnostics",
|
|
1177
1401
|
"reports",
|
|
1178
1402
|
"revisioned",
|
|
1179
1403
|
].includes(key) &&
|
|
@@ -1926,6 +2150,39 @@ export function compile(
|
|
|
1926
2150
|
};
|
|
1927
2151
|
}
|
|
1928
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
|
+
}
|
|
1929
2186
|
const a: UdlAction = {
|
|
1930
2187
|
summary: slots.has("summary")
|
|
1931
2188
|
? String(data(slots.get("summary")!))
|
|
@@ -2489,23 +2746,28 @@ export function compile(
|
|
|
2489
2746
|
};
|
|
2490
2747
|
|
|
2491
2748
|
const templateFamily = resolveFamily(targetTemplate, entry, false);
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
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
|
+
}
|
|
2509
2771
|
const attachedInst = document.instruments.find((i) => i.id === instId);
|
|
2510
2772
|
if (attachedInst?.actions.create) {
|
|
2511
2773
|
const owned = new Set(
|
|
@@ -2601,8 +2863,8 @@ export function compile(
|
|
|
2601
2863
|
if (columns.length > 8) {
|
|
2602
2864
|
fail(
|
|
2603
2865
|
columnsExpr,
|
|
2604
|
-
|
|
2605
|
-
"
|
|
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\`.`,
|
|
2606
2868
|
);
|
|
2607
2869
|
}
|
|
2608
2870
|
}
|
|
@@ -2618,12 +2880,6 @@ export function compile(
|
|
|
2618
2880
|
};
|
|
2619
2881
|
for (const decl of program.decls) {
|
|
2620
2882
|
if (decl.kind === "instrument") {
|
|
2621
|
-
if (decl.parameters.length)
|
|
2622
|
-
fail(
|
|
2623
|
-
decl,
|
|
2624
|
-
"program records cannot declare tunables",
|
|
2625
|
-
"put reusable instruments in a header",
|
|
2626
|
-
);
|
|
2627
2883
|
addInstrument(decl, decl.name, emptyBlock, decl.span);
|
|
2628
2884
|
}
|
|
2629
2885
|
if (decl.kind === "assignment") {
|
|
@@ -2650,6 +2906,13 @@ export function compile(
|
|
|
2650
2906
|
compileObject(decl);
|
|
2651
2907
|
}
|
|
2652
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
|
+
};
|
|
2653
2916
|
// Propagate mandatory invoked action requirements
|
|
2654
2917
|
let changedInvocations = true;
|
|
2655
2918
|
let invocationIterations = 0;
|
|
@@ -2904,9 +3167,49 @@ export function compile(
|
|
|
2904
3167
|
const origin = [...origins]
|
|
2905
3168
|
.reverse()
|
|
2906
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
|
+
}
|
|
2907
3210
|
return diagnostic(
|
|
2908
3211
|
{
|
|
2909
|
-
code: i.code
|
|
3212
|
+
code: i.code,
|
|
2910
3213
|
message: `${i.path}: ${i.message}`,
|
|
2911
3214
|
fix: i.fix,
|
|
2912
3215
|
span: origin?.span ?? program.span,
|
|
@@ -2928,7 +3231,9 @@ export function compile(
|
|
|
2928
3231
|
if (error instanceof CompileFailure)
|
|
2929
3232
|
return {
|
|
2930
3233
|
verdict: "invalid",
|
|
2931
|
-
diagnostics: [
|
|
3234
|
+
diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) =>
|
|
3235
|
+
diagnostic(d, "check"),
|
|
3236
|
+
),
|
|
2932
3237
|
};
|
|
2933
3238
|
throw error;
|
|
2934
3239
|
}
|