@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/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
- failWithCode(
848
- { span: origin },
849
- partyParameter ? "subject_party_unbound" : "HSX1001",
850
- `${id} needs ${param.key}`,
851
- `add ${param.key}: value inside ${id}`,
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
- for (const key of supplied.keys())
857
- if (!decl.parameters.some((p) => p.key === key))
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
- supplied.get(key)!,
860
- `unknown tunable ${key}`,
861
- `choose ${decl.parameters.map((p) => p.key).join(", ")}`,
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
- const actual = environment.get(param.key);
983
- if (!actual) continue;
984
- const t =
985
- param.value.kind === "default" ? param.value.type : param.value;
986
- const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
987
- const v =
988
- (supplied.has(param.key) && !attachmentInfo) || type === "enum"
989
- ? actual
990
- : resolve(actual, new Set(), !!attachmentInfo && type === "party");
991
- environment.set(param.key, v);
992
- if (type === "enum" && t.kind === "call") {
993
- if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
994
- fail(
995
- v,
996
- `invalid ${param.key}`,
997
- `choose ${t.args.map(text).join(", ")}`,
998
- );
999
- } else if (type === "list") {
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
- value,
1047
- "reference needs an object name",
1048
- "name a declared object",
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 [root, ...tail] = value.value.split(".");
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
- (!obj &&
1056
- !assignment &&
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
- value,
1066
- `${param.key} has the wrong object type`,
1067
- `use an object of type ${t.kind === "type" ? t.target : "ref"}`,
1197
+ v,
1198
+ `${param.key} accepts one reference`,
1199
+ "use one object name",
1068
1200
  );
1069
- if (seen.has(value.value))
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
- `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`,
1114
- "choose a value inside the tunable's bounds",
1204
+ "reference union needs 1 to 16 objects",
1205
+ "use at most 16 distinct object names",
1115
1206
  );
1116
- } catch (error) {
1117
- if (error instanceof CompileFailure)
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}: ${error.diagnostic.message}`,
1121
- error.diagnostic.fix,
1250
+ `${param.key} needs a block`,
1251
+ `write ${param.key} { ... }`,
1122
1252
  );
1123
- throw error;
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
- addInstrument(
2489
- template,
2490
- instId,
2491
- tunableBlock,
2492
- entry.span,
2493
- new Map(),
2494
- new Map(),
2495
- {
2496
- subjectKindId: decl.name,
2497
- attachmentName,
2498
- renames,
2499
- exposed,
2500
- parties,
2501
- },
2502
- templateFamily ? { ...templateFamily } : undefined,
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
- "rename a declared subject requirement",
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
- "at most 8 columns allowed",
2591
- "choose up to 8 columns",
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.startsWith("UDL") ? "HSX1601" : 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: [diagnostic(error.diagnostic, "check")],
3234
+ diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) =>
3235
+ diagnostic(d, "check"),
3236
+ ),
2918
3237
  };
2919
3238
  throw error;
2920
3239
  }