@hyperscale0/hsx 4.3.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- failWithCode(
848
- { span: origin },
849
- partyParameter ? "subject_party_unbound" : "HSX1001",
850
- `${id} needs ${param.key}`,
851
- partyParameter
852
- ? `bind ${param.key} to ${[...subjectPartyRoles, ...Object.keys(document.parties)].join(", ")} or declare a party`
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
- for (const key of supplied.keys())
859
- 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";
860
966
  fail(
861
- supplied.get(key)!,
862
- `unknown tunable ${key}`,
863
- `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} }\`.`,
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
- const actual = environment.get(param.key);
985
- if (!actual) continue;
986
- const t =
987
- param.value.kind === "default" ? param.value.type : param.value;
988
- const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
989
- const v =
990
- (supplied.has(param.key) && !attachmentInfo) || type === "enum"
991
- ? actual
992
- : resolve(actual, new Set(), !!attachmentInfo && type === "party");
993
- environment.set(param.key, v);
994
- if (type === "enum" && t.kind === "call") {
995
- if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
996
- fail(
997
- v,
998
- `invalid ${param.key}`,
999
- `choose ${t.args.map(text).join(", ")}`,
1000
- );
1001
- } else if (type === "list") {
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
- value,
1051
- "reference needs an object name",
1052
- "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",
1053
1172
  );
1054
- const [root, ...tail] = value.value.split(".");
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
- (!obj &&
1060
- !assignment &&
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
- value,
1070
- `${param.key} has the wrong object type`,
1071
- `use an object of type ${t.kind === "type" ? t.target : "ref"}`,
1197
+ v,
1198
+ `${param.key} accepts one reference`,
1199
+ "use one object name",
1072
1200
  );
1073
- if (seen.has(value.value))
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
- `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`,
1118
- "choose a value inside the tunable's bounds",
1204
+ "reference union needs 1 to 16 objects",
1205
+ "use at most 16 distinct object names",
1119
1206
  );
1120
- } catch (error) {
1121
- 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")
1122
1248
  fail(
1123
1249
  v,
1124
- `${param.key}: ${error.diagnostic.message}`,
1125
- error.diagnostic.fix,
1250
+ `${param.key} needs a block`,
1251
+ `write ${param.key} { ... }`,
1126
1252
  );
1127
- 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
+ }
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
- addInstrument(
2493
- template,
2494
- instId,
2495
- tunableBlock,
2496
- entry.span,
2497
- new Map(),
2498
- new Map(),
2499
- {
2500
- subjectKindId: decl.name,
2501
- attachmentName,
2502
- renames,
2503
- exposed,
2504
- parties,
2505
- },
2506
- templateFamily ? { ...templateFamily } : undefined,
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
- "at most 8 columns allowed",
2605
- "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\`.`,
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.startsWith("UDL") ? "HSX1601" : 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: [diagnostic(error.diagnostic, "check")],
3234
+ diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) =>
3235
+ diagnostic(d, "check"),
3236
+ ),
2932
3237
  };
2933
3238
  throw error;
2934
3239
  }