@hyperscale0/hsx 5.2.0 → 5.4.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -2
  3. package/bin/hsx.ts +0 -2
  4. package/dist/bin/hsx.js +0 -2
  5. package/dist/bin/hsx.js.map +1 -1
  6. package/dist/src/ast.d.ts +0 -1
  7. package/dist/src/ast.d.ts.map +1 -1
  8. package/dist/src/ast.js +0 -11
  9. package/dist/src/ast.js.map +1 -1
  10. package/dist/src/cli.d.ts +0 -2
  11. package/dist/src/cli.d.ts.map +1 -1
  12. package/dist/src/cli.js +10 -5
  13. package/dist/src/cli.js.map +1 -1
  14. package/dist/src/compile.d.ts.map +1 -1
  15. package/dist/src/compile.js +204 -107
  16. package/dist/src/compile.js.map +1 -1
  17. package/dist/src/diagnostics.d.ts +14 -0
  18. package/dist/src/diagnostics.d.ts.map +1 -0
  19. package/dist/src/diagnostics.js +20 -0
  20. package/dist/src/diagnostics.js.map +1 -0
  21. package/dist/src/header-source.d.ts +3 -0
  22. package/dist/src/header-source.d.ts.map +1 -0
  23. package/dist/src/header-source.js +41 -0
  24. package/dist/src/header-source.js.map +1 -0
  25. package/dist/src/headers.d.ts.map +1 -1
  26. package/dist/src/headers.js +4 -6
  27. package/dist/src/headers.js.map +1 -1
  28. package/dist/src/keywords.d.ts +8 -2
  29. package/dist/src/keywords.d.ts.map +1 -1
  30. package/dist/src/keywords.js +13 -5
  31. package/dist/src/keywords.js.map +1 -1
  32. package/dist/src/lex.d.ts +1 -1
  33. package/dist/src/lex.d.ts.map +1 -1
  34. package/dist/src/lex.js +11 -3
  35. package/dist/src/lex.js.map +1 -1
  36. package/dist/src/parse.d.ts.map +1 -1
  37. package/dist/src/parse.js +22 -5
  38. package/dist/src/parse.js.map +1 -1
  39. package/dist/src/std-bundle.js +12 -12
  40. package/dist/src/std-bundle.js.map +1 -1
  41. package/dist/src/tunables.d.ts.map +1 -1
  42. package/dist/src/tunables.js +2 -1
  43. package/dist/src/tunables.js.map +1 -1
  44. package/dist/src/version.d.ts +1 -2
  45. package/dist/src/version.d.ts.map +1 -1
  46. package/dist/src/version.js +1 -2
  47. package/dist/src/version.js.map +1 -1
  48. package/docs/README.md +54 -7
  49. package/docs/headers.md +1 -1
  50. package/package.json +3 -3
  51. package/src/ast.ts +0 -10
  52. package/src/cli.ts +13 -7
  53. package/src/compile.ts +296 -133
  54. package/src/diagnostics.ts +28 -0
  55. package/src/header-source.ts +54 -0
  56. package/src/headers.ts +4 -6
  57. package/src/keywords.ts +13 -5
  58. package/src/lex.ts +13 -5
  59. package/src/parse.ts +29 -5
  60. package/src/std-bundle.ts +12 -12
  61. package/src/tunables.ts +6 -1
  62. package/src/version.ts +1 -2
  63. package/std/cards.hsx +12 -0
  64. package/std/collections.hsx +11 -0
  65. package/std/escrow.hsx +16 -2
  66. package/std/financing.hsx +36 -5
  67. package/std/insurance.hsx +20 -5
  68. package/std/lending.hsx +14 -0
  69. package/std/marketplace.hsx +14 -0
  70. package/std/money.hsx +13 -0
  71. package/std/reporting.hsx +10 -0
  72. package/std/savings.hsx +28 -11
  73. package/std/travel.hsx +22 -4
  74. package/std/wallet.hsx +9 -0
package/src/compile.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CompileFailure, fail, failWithCode } from "./diagnostics.ts";
1
2
  import { hash as sha256 } from "fast-sha256";
2
3
  import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
3
4
  import {
@@ -10,6 +11,7 @@ import {
10
11
  type AttachmentPartyBinding,
11
12
  type SubjectPartyRole,
12
13
  udlObjectFieldSchema,
14
+ udlInstrumentSchema,
13
15
  type UdlAction,
14
16
  type UdlActionSubject,
15
17
  type UdlAdapterSubjectSnapshot,
@@ -29,6 +31,7 @@ import {
29
31
  } from "./binding-contract.ts";
30
32
  import { tunableBounds } from "./tunables.ts";
31
33
  import { parseProgram } from "./parse.ts";
34
+ import { parseHeader } from "./header-source.ts";
32
35
  import {
33
36
  lineColAt,
34
37
  type AssignmentDecl,
@@ -71,43 +74,19 @@ export interface CompileResult {
71
74
  originMap: CompileOriginMapEntry[];
72
75
  };
73
76
  }
74
- class CompileFailure extends Error {
75
- constructor(readonly diagnostic: Diagnostic) {
76
- super(diagnostic.message);
77
- }
78
- }
79
- function fail(
80
- expr: { span: Span; source?: string },
81
- message: string,
82
- fix: string,
83
- ): never {
84
- return failWithCode(expr, "HSX1001", message, fix);
85
- }
86
- function failWithCode(
87
- expr: { span: Span; source?: string },
88
- code: string,
89
- message: string,
90
- fix: string,
91
- ): never {
92
- throw new CompileFailure({
93
- code,
94
- message,
95
- fix,
96
- span: expr.span,
97
- ...(expr.source ? { source: expr.source } : {}),
98
- });
77
+ function fieldType(row: Entry): { type: Expr; sensitive: boolean } {
78
+ const type = row.value.kind === "default" ? row.value.type : row.value;
79
+ if (type.kind !== "call" || type.name !== "sensitive")
80
+ return { type, sensitive: false };
81
+ if (type.args.length !== 1)
82
+ fail(type, "sensitive needs one field type", "write sensitive(text)");
83
+ return { type: type.args[0]!, sensitive: true };
99
84
  }
100
85
  function lowerFieldShape(
101
86
  row: Entry,
102
87
  resolveExpr: (e: Expr) => Expr = (e) => e,
103
88
  ): Record<string, unknown> {
104
- let rawValue = row.value.kind === "default" ? row.value.type : row.value;
105
- let isSensitive = false;
106
- if (rawValue.kind === "call" && rawValue.name === "sensitive") {
107
- isSensitive = true;
108
- rawValue = rawValue.args[0]!;
109
- }
110
- const t = rawValue;
89
+ const { type: t, sensitive: isSensitive } = fieldType(row);
111
90
  if (t.kind === "block") {
112
91
  const b = entries(t);
113
92
  if (b.has("family") || b.has("target") || b.has("instrument")) {
@@ -164,7 +143,22 @@ function lowerFieldShape(
164
143
  f.minimum = literal(resolveExpr(t.args[0]!));
165
144
  f.maximum = literal(resolveExpr(t.args[1]!));
166
145
  }
146
+ if (
147
+ t.kind === "call" &&
148
+ !["enum", "text", "integer", "money", "list", "account"].includes(type)
149
+ )
150
+ fail(
151
+ t,
152
+ `unknown field constructor ${type}`,
153
+ "use a field type without arguments",
154
+ );
167
155
  if (type === "list" && t.kind === "call") {
156
+ if (t.args.length < 1 || t.args.length > 2)
157
+ fail(
158
+ t,
159
+ "list needs an item type and optional bound",
160
+ "write list(text, 12)",
161
+ );
168
162
  const item = t.args[0]!;
169
163
  f.item = item.kind === "type" ? item.name : text(item);
170
164
  if (item.kind === "type" && item.name === "ref") {
@@ -199,7 +193,7 @@ function lowerObjectField(
199
193
  f.value =
200
194
  f.type === "enum" && constant.kind === "name"
201
195
  ? constant.value
202
- : literal(constant);
196
+ : literal(constant, String(f.type));
203
197
  }
204
198
  const result = udlObjectFieldSchema.safeParse(f);
205
199
  if (!result.success)
@@ -233,7 +227,8 @@ const camel = (name: string) =>
233
227
  name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
234
228
  /** Clock and parent actors run without a caller, so they have no public name. */
235
229
  const automatic = (actor: UdlAction["actor"]) =>
236
- actor === "clock" || (typeof actor === "object" && "parent" in actor);
230
+ actor === "clock" ||
231
+ (actor !== null && typeof actor === "object" && "parent" in actor);
237
232
  const title = (name: string) =>
238
233
  name[0]!.toUpperCase() + name.slice(1).replaceAll("_", " ");
239
234
  function entries(block: BlockExpr): Map<string, Expr> {
@@ -290,7 +285,9 @@ function decimal(raw: string, scale: number, expr: Expr): string {
290
285
  BigInt(fraction.padEnd(scale, "0") || "0")
291
286
  ).toString();
292
287
  }
293
- function literal(expr: Expr): string | number | boolean {
288
+ function literal(expr: Expr, expectedType?: string): string | number | boolean {
289
+ if (expr.kind === "text" && expectedType === "duration")
290
+ return literal({ ...expr, kind: "duration" });
294
291
  if (expr.kind === "money") {
295
292
  const [raw, currency] = expr.value.split(" ");
296
293
  if (currency !== "SAR")
@@ -330,8 +327,7 @@ function literal(expr: Expr): string | number | boolean {
330
327
  }
331
328
  if (
332
329
  expr.kind === "duration" ||
333
- (expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)) ||
334
- (expr.kind === "text" && /^P(?:\d|T)/.test(expr.value))
330
+ (expr.kind === "name" && /^P(?:\d|T)/.test(expr.value))
335
331
  ) {
336
332
  const short = /^(\d+)(ms|s|m|h|d|w)$/.exec(expr.value);
337
333
  const units: Record<string, number> = {
@@ -343,7 +339,7 @@ function literal(expr: Expr): string | number | boolean {
343
339
  w: 604800000,
344
340
  };
345
341
  const iso =
346
- /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(
342
+ /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(
347
343
  expr.value,
348
344
  );
349
345
  const n = short
@@ -367,10 +363,11 @@ function literal(expr: Expr): string | number | boolean {
367
363
  const raw =
368
364
  expr.value.length === 10 ? expr.value + "T00:00:00Z" : expr.value;
369
365
  const n = Date.parse(raw);
366
+ const day = Date.parse(raw.slice(0, 10) + "T00:00:00Z");
370
367
  if (
371
368
  !Number.isFinite(n) ||
372
- new Date(raw.slice(0, 10) + "T00:00:00Z").toISOString().slice(0, 10) !==
373
- raw.slice(0, 10)
369
+ !Number.isFinite(day) ||
370
+ new Date(day).toISOString().slice(0, 10) !== raw.slice(0, 10)
374
371
  )
375
372
  fail(
376
373
  expr,
@@ -407,6 +404,15 @@ export function compile(
407
404
  };
408
405
  const program = parsed.program;
409
406
  const bindingDiagnostics: Diagnostic[] = [];
407
+ const adapterTarget = (binding: string): AdapterBindingTarget | undefined => {
408
+ const registry = options.adapterRegistry;
409
+ if (!registry || !Object.hasOwn(registry, binding)) return;
410
+ const target = registry[binding];
411
+ return target &&
412
+ Object.hasOwn(target.adapter.operationMap, target.operation)
413
+ ? target
414
+ : undefined;
415
+ };
410
416
  try {
411
417
  if (program.header)
412
418
  fail(
@@ -422,7 +428,6 @@ export function compile(
422
428
  );
423
429
  const templates = new Map<string, InstrumentDecl>();
424
430
  const declarationSources = new Map<InstrumentDecl, string>();
425
- const declarationExportPaths = new Map<InstrumentDecl, string>();
426
431
  const requirementOrigins = new Map<
427
432
  UdlSubjectRequirement,
428
433
  { source: string; span: Span; message: string }
@@ -442,25 +447,34 @@ export function compile(
442
447
  if (!content)
443
448
  fail(use, `unknown header ${use.name}`, "choose a published header");
444
449
  sources.set(use.name, content);
445
- const header = parseProgram(content);
446
- if (
447
- header.diagnostics.length ||
448
- !header.program.header ||
449
- header.program.name !== use.name
450
- )
451
- fail(
452
- use,
453
- `header ${use.name} is malformed`,
454
- "repair the header source before compiling",
455
- );
450
+ let header;
451
+ try {
452
+ header = parseHeader(content, use.name);
453
+ } catch (error) {
454
+ if (!(error instanceof CompileFailure)) throw error;
455
+ throw new CompileFailure({
456
+ ...error.diagnostic,
457
+ related: [
458
+ {
459
+ source: "program",
460
+ span: use.span,
461
+ message: `Imported header ${use.name}`,
462
+ },
463
+ ],
464
+ });
465
+ }
456
466
  const registerTemplates = (
457
467
  parentDecl: InstrumentDecl,
458
468
  prefix: string,
459
- exportPath: string,
460
469
  ) => {
470
+ if (templates.has(prefix))
471
+ fail(
472
+ parentDecl,
473
+ `duplicate instrument ${prefix}`,
474
+ "give each instrument a distinct name",
475
+ );
461
476
  templates.set(prefix, parentDecl);
462
477
  declarationSources.set(parentDecl, use.name);
463
- declarationExportPaths.set(parentDecl, exportPath);
464
478
  const recs = entries(asBlock(entries(parentDecl.body).get("records")));
465
479
  for (const [recName, recBlock] of recs) {
466
480
  const recDecl: InstrumentDecl = {
@@ -470,16 +484,12 @@ export function compile(
470
484
  body: asBlock(recBlock),
471
485
  span: parentDecl.span,
472
486
  };
473
- registerTemplates(
474
- recDecl,
475
- `${prefix}.${recName}`,
476
- `${exportPath}.${recName}`,
477
- );
487
+ registerTemplates(recDecl, `${prefix}.${recName}`);
478
488
  }
479
489
  };
480
- for (const decl of header.program.decls)
490
+ for (const decl of header.decls)
481
491
  if (decl.kind === "instrument") {
482
- registerTemplates(decl, `${use.name}.${decl.name}`, decl.name);
492
+ registerTemplates(decl, `${use.name}.${decl.name}`);
483
493
  }
484
494
  }
485
495
  for (const decl of program.decls)
@@ -498,12 +508,12 @@ export function compile(
498
508
  product: program.name,
499
509
  title: program.title,
500
510
  currency: "SAR",
501
- parties: {
511
+ parties: Object.assign(Object.create(null) as UdlDocument["parties"], {
502
512
  programOperator: { kind: "business", role: "program_operator" },
503
513
  programTax: { kind: "business", role: "tax_payable" },
504
514
  programFines: { kind: "business", role: "fine_payable" },
505
515
  programCosts: { kind: "business", role: "cost_recovery" },
506
- },
516
+ }),
507
517
  objects: [],
508
518
  instruments: [],
509
519
  };
@@ -559,6 +569,8 @@ export function compile(
559
569
  }
560
570
  }
561
571
  const origins: CompileOriginMapEntry[] = [];
572
+ const exposures: { instrument: string; action: string; entry: Entry }[] =
573
+ [];
562
574
  const refundBindings = new Map<
563
575
  string,
564
576
  { parameter: string; span: Span; defaultState: string; action: string }[]
@@ -760,13 +772,11 @@ export function compile(
760
772
  );
761
773
  if (sub) {
762
774
  const fullExport = `${familyDeclaration.exportPath}.${sub}`;
763
- try {
764
- return resolveFamily(
765
- `${familyDeclaration.module}.${fullExport}`,
766
- { span: origin },
767
- false,
768
- );
769
- } catch {}
775
+ return resolveFamily(
776
+ `${familyDeclaration.module}.${fullExport}`,
777
+ { span: origin },
778
+ false,
779
+ );
770
780
  }
771
781
  }
772
782
 
@@ -777,9 +787,7 @@ export function compile(
777
787
  const mod = declarationSources.get(tmpl);
778
788
  if (!mod || mod === "program") continue;
779
789
  if (targetId === asgnName) {
780
- try {
781
- return resolveFamily(asgn.target, asgn, false);
782
- } catch {}
790
+ return resolveFamily(asgn.target, asgn, false);
783
791
  } else {
784
792
  const sub = resolveChildExportPath(
785
793
  tmpl,
@@ -787,9 +795,7 @@ export function compile(
787
795
  );
788
796
  if (sub) {
789
797
  const fullTarget = `${asgn.target}.${sub}`;
790
- try {
791
- return resolveFamily(fullTarget, asgn, false);
792
- } catch {}
798
+ return resolveFamily(fullTarget, asgn, false);
793
799
  }
794
800
  }
795
801
  }
@@ -798,12 +804,18 @@ export function compile(
798
804
  };
799
805
 
800
806
  const checkTargetFamily = (
801
- targetIds: string | string[],
807
+ targetIds: unknown,
802
808
  expectedFamily: UdlFamily,
803
809
  expr: { span: Span; source?: string },
804
810
  ): void => {
805
811
  const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
806
812
  for (const tid of ids) {
813
+ if (typeof tid !== "string")
814
+ fail(
815
+ expr,
816
+ "instrument target needs a name",
817
+ "name a declared instrument or a list of instruments",
818
+ );
807
819
  const fam = getInstrumentFamily(tid);
808
820
  if (
809
821
  !fam ||
@@ -964,11 +976,11 @@ export function compile(
964
976
  entry.value.name === "money",
965
977
  );
966
978
  const target =
967
- candidates.length === 1 ? candidates[0]!.key : "yourDepositField";
979
+ candidates.length === 1 ? candidates[0]!.key : "yourMoneyField";
968
980
  fail(
969
981
  suppliedValue,
970
- `\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its funding action requires the object's \`${field}\` field.`,
971
- `Remove \`${key}\`. To use your deposit field, add \`rename { ${field}: ${target} }\`.`,
982
+ `\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its actions require the object's \`${field}\` field.`,
983
+ `Remove \`${key}\`. To use your object's money field, add \`rename { ${field}: ${target} }\`.`,
972
984
  );
973
985
  }
974
986
  fail(
@@ -1087,11 +1099,16 @@ export function compile(
1087
1099
  (binding.kind === "name" && binding.value === expr.value))
1088
1100
  )
1089
1101
  return expr;
1090
- // A resolved party parameter takes precedence over a same-named attachment.
1102
+ // A resolved party parameter or an own parameter bound to a value takes
1103
+ // precedence over a same-named sibling attachment; `limits: limits`
1104
+ // binds by identity and still names the sibling.
1091
1105
  if (attachmentInfo && !resolvedParties.has(expr.value)) {
1092
1106
  const [local, ...tail] = expr.value.split(".");
1107
+ const own = environment.get(local!);
1108
+ const ownValue =
1109
+ own !== undefined && !(own.kind === "name" && own.value === local);
1093
1110
  const target = `${attachmentInfo.subjectKindId}_${local}`;
1094
- if (attachmentSubjects.has(target))
1111
+ if (!ownValue && attachmentSubjects.has(target))
1095
1112
  return { ...expr, value: [target, ...tail].join("_") };
1096
1113
  }
1097
1114
  if (expr.value.startsWith("party.")) {
@@ -1141,7 +1158,7 @@ export function compile(
1141
1158
  param.value.kind === "default" ? param.value.type : param.value;
1142
1159
  const type =
1143
1160
  t.kind === "type" || t.kind === "call" ? t.name : text(t);
1144
- const v =
1161
+ let v =
1145
1162
  (supplied.has(param.key) && !attachmentInfo) || type === "enum"
1146
1163
  ? actual
1147
1164
  : resolve(
@@ -1149,6 +1166,12 @@ export function compile(
1149
1166
  new Set(),
1150
1167
  !!attachmentInfo && type === "party",
1151
1168
  );
1169
+ if (
1170
+ type === "duration" &&
1171
+ (v.kind === "text" || v.kind === "name") &&
1172
+ /^P/.test(v.value)
1173
+ )
1174
+ v = { ...v, kind: "duration" };
1152
1175
  environment.set(param.key, v);
1153
1176
  if (type === "enum" && t.kind === "call") {
1154
1177
  if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
@@ -1214,19 +1237,36 @@ export function compile(
1214
1237
  "reference needs an object name",
1215
1238
  "name a declared object",
1216
1239
  );
1217
- const [root, ...tail] = value.value.split(".");
1218
- const obj = objects.get(root!);
1219
- const assignment = assignments.get(root!);
1220
- const targetType = obj ? obj.name : assignment?.target;
1240
+ let targetType = objects.get(value.value)?.name;
1241
+ const candidates = [
1242
+ ...[...assignments.values()].map(
1243
+ (assignment) => [assignment.name, assignment.target] as const,
1244
+ ),
1245
+ ...program.decls
1246
+ .filter((decl) => decl.kind === "instrument")
1247
+ .map((decl) => [decl.name, decl.name] as const),
1248
+ ];
1249
+ for (const [instance, target] of candidates) {
1250
+ if (value.value === instance) {
1251
+ targetType = target;
1252
+ break;
1253
+ }
1254
+ if (
1255
+ !value.value.startsWith(`${instance}.`) &&
1256
+ !value.value.startsWith(`${instance}_`)
1257
+ )
1258
+ continue;
1259
+ const template = templates.get(target);
1260
+ if (!template) continue;
1261
+ const suffix = value.value
1262
+ .slice(instance.length + 1)
1263
+ .replaceAll(".", "_");
1264
+ const child = resolveChildExportPath(template, suffix);
1265
+ if (child) targetType = `${target}.${child}`;
1266
+ }
1221
1267
  if (
1222
- (!obj &&
1223
- !assignment &&
1224
- !document.instruments.some(
1225
- (inst) => inst.id === value.value,
1226
- )) ||
1227
- (t.kind === "type" &&
1228
- t.target &&
1229
- [targetType, ...tail].join(".") !== t.target)
1268
+ !targetType ||
1269
+ (t.kind === "type" && t.target && targetType !== t.target)
1230
1270
  )
1231
1271
  fail(
1232
1272
  value,
@@ -1258,17 +1298,22 @@ export function compile(
1258
1298
  type === "date" ||
1259
1299
  type === "duration" ||
1260
1300
  type === "integer" ||
1261
- type === "text"
1301
+ type === "text" ||
1302
+ type === "boolean"
1262
1303
  ) {
1304
+ const bounds = tunableBounds(t);
1263
1305
  if (v.kind === "name" && v.value === "runtime") continue;
1264
- const expected = type === "integer" ? "number" : type;
1306
+ const expected =
1307
+ type === "integer"
1308
+ ? "number"
1309
+ : type === "boolean"
1310
+ ? "name"
1311
+ : type;
1265
1312
  if (
1266
- v.kind !== expected &&
1267
- !(
1268
- type === "duration" &&
1269
- (v.kind === "text" || v.kind === "name") &&
1270
- /^P/.test(v.value)
1271
- )
1313
+ v.kind !== expected ||
1314
+ (type === "boolean" &&
1315
+ v.kind === "name" &&
1316
+ !["true", "false"].includes(v.value))
1272
1317
  ) {
1273
1318
  if (type === "money" && v.kind === "name" && attachmentInfo) {
1274
1319
  const target = v.value.replace(/^subject\./, "");
@@ -1301,7 +1346,6 @@ export function compile(
1301
1346
  }
1302
1347
  try {
1303
1348
  const value = literal(v);
1304
- const bounds = tunableBounds(t);
1305
1349
  if (
1306
1350
  bounds &&
1307
1351
  (BigInt(String(value)) < BigInt(bounds.minimum) ||
@@ -1321,6 +1365,12 @@ export function compile(
1321
1365
  );
1322
1366
  throw error;
1323
1367
  }
1368
+ } else {
1369
+ fail(
1370
+ t,
1371
+ `unknown tunable type ${type}`,
1372
+ "use a declared HSX tunable type",
1373
+ );
1324
1374
  }
1325
1375
  } catch (error) {
1326
1376
  if (!(error instanceof CompileFailure)) throw error;
@@ -1373,6 +1423,21 @@ export function compile(
1373
1423
  "constraint needs two declared tunables",
1374
1424
  "name the compared tunables",
1375
1425
  );
1426
+ const numericKind = (expr: Expr) =>
1427
+ expr.kind === "name" && /^P/.test(expr.value)
1428
+ ? "duration"
1429
+ : expr.kind;
1430
+ if (
1431
+ !["money", "number", "percent", "duration"].includes(
1432
+ numericKind(left),
1433
+ ) ||
1434
+ numericKind(left) !== numericKind(right)
1435
+ )
1436
+ fail(
1437
+ rule,
1438
+ "constraint needs numeric tunables of the same type",
1439
+ "compare two amounts, integers, percentages or durations",
1440
+ );
1376
1441
  const a = BigInt(String(literal(left))),
1377
1442
  b = BigInt(String(literal(right)));
1378
1443
  if (
@@ -1473,7 +1538,7 @@ export function compile(
1473
1538
  ? se.value.items.map(text)
1474
1539
  : [text(se.value)];
1475
1540
  return names.some((n) => {
1476
- const reg = options.adapterRegistry?.[n];
1541
+ const reg = adapterTarget(n);
1477
1542
  if (!reg) return false;
1478
1543
  const op = reg.adapter.operationMap[reg.operation];
1479
1544
  return op?.subjectRequirements?.some(
@@ -1534,7 +1599,7 @@ export function compile(
1534
1599
  instrumentVal = data(rawEntries.get("instrument")!);
1535
1600
  if (famTuple) {
1536
1601
  checkTargetFamily(
1537
- instrumentVal as string | string[],
1602
+ instrumentVal,
1538
1603
  famTuple,
1539
1604
  rawEntries.get("instrument")!,
1540
1605
  );
@@ -1612,7 +1677,7 @@ export function compile(
1612
1677
  const lowerFields = (block: BlockExpr): UdlField[] => {
1613
1678
  const result: UdlField[] = [];
1614
1679
  for (const row of block.entries) {
1615
- const t = row.value.kind === "default" ? row.value.type : row.value;
1680
+ const { type: t } = fieldType(row);
1616
1681
  const constant =
1617
1682
  row.value.kind === "default" ? resolve(row.value.value) : undefined;
1618
1683
  const f = lowerFieldShape(row, resolve);
@@ -1842,7 +1907,7 @@ export function compile(
1842
1907
  op: "sum",
1843
1908
  values: [val(constant)],
1844
1909
  });
1845
- else f.value = literal(constant);
1910
+ else f.value = literal(constant, String(type));
1846
1911
  }
1847
1912
  result.push(f as UdlField);
1848
1913
  }
@@ -1850,9 +1915,11 @@ export function compile(
1850
1915
  };
1851
1916
  fields.push(...lowerFields(asBlock(body.get("fields"))));
1852
1917
  const lifecycle = data(
1853
- body.get("lifecycle") ?? emptyBlock,
1918
+ asBlock(body.get("lifecycle")),
1854
1919
  ) as UdlInstrument["lifecycle"];
1855
- lifecycle.transitions = {};
1920
+ lifecycle.transitions = Object.create(
1921
+ null,
1922
+ ) as UdlInstrument["lifecycle"]["transitions"];
1856
1923
  const inst: UdlInstrument = {
1857
1924
  id,
1858
1925
  ...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
@@ -1863,7 +1930,7 @@ export function compile(
1863
1930
  fields,
1864
1931
  calculate: calculations,
1865
1932
  lifecycle,
1866
- actions: {},
1933
+ actions: Object.create(null) as UdlInstrument["actions"],
1867
1934
  actionOrder: [],
1868
1935
  };
1869
1936
  for (const row of decl.body.entries.filter((e) =>
@@ -1970,15 +2037,8 @@ export function compile(
1970
2037
  "name a bound ADL adapter",
1971
2038
  );
1972
2039
  const binding = text(resolve(adapterExpr!));
1973
- const target =
1974
- options.adapterRegistry &&
1975
- Object.hasOwn(options.adapterRegistry, binding)
1976
- ? options.adapterRegistry[binding]
1977
- : undefined;
1978
- if (
1979
- !target ||
1980
- !Object.hasOwn(target.adapter.operationMap, target.operation)
1981
- )
2040
+ const target = adapterTarget(binding);
2041
+ if (!target)
1982
2042
  fail(
1983
2043
  boundary,
1984
2044
  `unknown boundary adapter ${binding}`,
@@ -2034,7 +2094,7 @@ export function compile(
2034
2094
  ? entry.value.items.map(adapterName)
2035
2095
  : [adapterName(entry.value)];
2036
2096
  for (const bindingName of bindingNames) {
2037
- const target = options.adapterRegistry?.[bindingName];
2097
+ const target = adapterTarget(bindingName);
2038
2098
  if (target) {
2039
2099
  const { adapter, operation } = target;
2040
2100
  const opBinding = adapter.operationMap[operation];
@@ -2562,22 +2622,39 @@ export function compile(
2562
2622
  }
2563
2623
  if (attachmentInfo && !attachmentInfo.child) {
2564
2624
  if (attachmentInfo.exposed.has(name)) {
2625
+ if (automatic(a.actor))
2626
+ fail(
2627
+ row,
2628
+ `${name} runs on the clock or its parent, not a caller`,
2629
+ "expose only caller actions",
2630
+ );
2565
2631
  a.publicAction = attachmentInfo.exposed.get(name)!;
2566
2632
  } else {
2567
2633
  delete a.publicAction;
2568
2634
  }
2569
2635
  }
2570
2636
  if (automatic(a.actor)) delete a.publicAction;
2571
- const checkSubjectPaths = (obj: unknown, span: Span) => {
2637
+ const checkSubjectPaths = (obj: unknown, key = "") => {
2572
2638
  if (typeof obj === "string") {
2573
- if (obj.startsWith("subject.")) {
2639
+ if (
2640
+ [
2641
+ "field",
2642
+ "fields",
2643
+ "reference",
2644
+ "anchor",
2645
+ "subject",
2646
+ "at",
2647
+ "instruction",
2648
+ ].includes(key) &&
2649
+ obj.startsWith("subject.")
2650
+ ) {
2574
2651
  const subField = obj.split(".")[1]!;
2575
2652
  const req = a.subject?.requirements.find(
2576
2653
  (r) => r.field.name === subField,
2577
2654
  );
2578
2655
  if (!req) {
2579
2656
  failWithCode(
2580
- { span },
2657
+ row,
2581
2658
  "subject_field_unknown",
2582
2659
  `subject.${subField} names no declared subject requirement in action ${name}`,
2583
2660
  `declare ${subField} in subject { ... }`,
@@ -2585,14 +2662,15 @@ export function compile(
2585
2662
  }
2586
2663
  }
2587
2664
  } else if (Array.isArray(obj)) {
2588
- for (const item of obj) checkSubjectPaths(item, span);
2665
+ for (const item of obj) checkSubjectPaths(item, key);
2589
2666
  } else if (obj !== null && typeof obj === "object") {
2590
- for (const val of Object.values(obj)) checkSubjectPaths(val, span);
2667
+ for (const [key, val] of Object.entries(obj))
2668
+ checkSubjectPaths(val, key);
2591
2669
  }
2592
2670
  };
2593
- checkSubjectPaths(a.requires, row.span);
2594
- checkSubjectPaths(a.set, row.span);
2595
- checkSubjectPaths(a.invoke, row.span);
2671
+ checkSubjectPaths(a.requires);
2672
+ checkSubjectPaths(a.set);
2673
+ checkSubjectPaths(a.invoke);
2596
2674
  currentAction = undefined;
2597
2675
  currentActionName = undefined;
2598
2676
  for (const requirement of a.subject?.requirements ?? []) {
@@ -2617,6 +2695,27 @@ export function compile(
2617
2695
  (inst as unknown as Record<string, unknown>)[key] = data(
2618
2696
  body.get(key)!,
2619
2697
  );
2698
+ const shape = udlInstrumentSchema.safeParse(inst);
2699
+ if (!shape.success) {
2700
+ const issue = shape.error.issues[0]!;
2701
+ const actionName =
2702
+ issue.path[0] === "actions" ? String(issue.path[1]) : undefined;
2703
+ const node =
2704
+ decl.body.entries.find(
2705
+ (entry) =>
2706
+ entry.key ===
2707
+ (actionName ? `action ${actionName}` : String(issue.path[0])),
2708
+ ) ?? decl;
2709
+ failWithCode(
2710
+ {
2711
+ span: node.span,
2712
+ source: declarationSources.get(decl) ?? "program",
2713
+ },
2714
+ "UDL1003",
2715
+ `${id}.${issue.path.join(".")}: ${issue.message}`,
2716
+ "use the UDL typed clause shape",
2717
+ );
2718
+ }
2620
2719
  origins.push({
2621
2720
  path: `$.instruments[${document.instruments.length}]`,
2622
2721
  span: { ...origin, ...lineColAt(source, origin.start) },
@@ -2696,7 +2795,6 @@ export function compile(
2696
2795
  }
2697
2796
  }
2698
2797
 
2699
- // 1. Lower authored fields
2700
2798
  const authoredFields: UdlObjectField[] = [];
2701
2799
  const authoredNames: string[] = [];
2702
2800
  const fieldsBlock = asBlock(body.get("fields"));
@@ -2713,7 +2811,6 @@ export function compile(
2713
2811
  authoredNames.push(row.key);
2714
2812
  }
2715
2813
 
2716
- // 2. Process attachments
2717
2814
  const attachments: UdlObjectAttachment[] = [];
2718
2815
  for (const entry of decl.body.entries) {
2719
2816
  if (!entry.key.startsWith("attach ")) continue;
@@ -2746,6 +2843,12 @@ export function compile(
2746
2843
  for (const row of attachmentBlock.entries) {
2747
2844
  if (row.key === "rename") {
2748
2845
  for (const r of asBlock(row.value).entries) {
2846
+ if (renames.has(r.key))
2847
+ fail(
2848
+ r,
2849
+ `duplicate rename ${r.key}`,
2850
+ "rename each subject field once",
2851
+ );
2749
2852
  renames.set(r.key, text(r.value));
2750
2853
  renameEntries.set(r.key, r);
2751
2854
  }
@@ -2753,7 +2856,23 @@ export function compile(
2753
2856
  if (row.value.kind === "call") {
2754
2857
  const actionName = row.value.name;
2755
2858
  const publicName = text(row.value.args[0]!);
2859
+ if (
2860
+ exposed.has(actionName) ||
2861
+ !template.body.entries.some(
2862
+ (entry) => entry.key === `action ${actionName}`,
2863
+ )
2864
+ )
2865
+ fail(
2866
+ row,
2867
+ `unknown or repeated action ${actionName}`,
2868
+ "expose one declared action once",
2869
+ );
2756
2870
  exposed.set(actionName, publicName);
2871
+ exposures.push({
2872
+ instrument: instId,
2873
+ action: actionName,
2874
+ entry: row,
2875
+ });
2757
2876
  }
2758
2877
  } else {
2759
2878
  tunableEntries.push(row);
@@ -2882,7 +3001,6 @@ export function compile(
2882
3001
  }
2883
3002
  }
2884
3003
 
2885
- // 4. Validate columns
2886
3004
  const columnsExpr = body.get("columns");
2887
3005
  let columns: string[] = [];
2888
3006
  if (columnsExpr) {
@@ -2950,7 +3068,11 @@ export function compile(
2950
3068
  // Propagate invoked requirements with the conditions on each invocation path.
2951
3069
  let changedInvocations = true;
2952
3070
  let invocationIterations = 0;
2953
- while (changedInvocations && invocationIterations < 32) {
3071
+ const invocationDepth = document.instruments.reduce(
3072
+ (total, inst) => total + inst.actionOrder.length,
3073
+ 0,
3074
+ );
3075
+ while (changedInvocations && invocationIterations < invocationDepth) {
2954
3076
  changedInvocations = false;
2955
3077
  invocationIterations++;
2956
3078
  for (const inst of document.instruments) {
@@ -3167,6 +3289,17 @@ export function compile(
3167
3289
  const eliminatedStates = new Map<string, Set<string>>();
3168
3290
  // Remove branches excluded by immutable tunables before checking reference states.
3169
3291
  for (const inst of document.instruments) {
3292
+ if (
3293
+ !inst.lifecycle.states.includes(inst.lifecycle.initial) ||
3294
+ Object.values(inst.lifecycle.transitions).some(
3295
+ (edge) =>
3296
+ edge.from.some((state) => !inst.lifecycle.states.includes(state)) ||
3297
+ (edge.to !== "preserve" &&
3298
+ !inst.lifecycle.states.includes(edge.to)),
3299
+ )
3300
+ )
3301
+ continue;
3302
+ let specialized = false;
3170
3303
  for (const [key, action] of Object.entries(inst.actions)) {
3171
3304
  const constant = (value: import("@hyperscale0/udl").UdlValue) => {
3172
3305
  if ("literal" in value) return value.literal;
@@ -3188,8 +3321,10 @@ export function compile(
3188
3321
  ) {
3189
3322
  delete inst.actions[key];
3190
3323
  delete inst.lifecycle.transitions[key];
3324
+ specialized = true;
3191
3325
  }
3192
3326
  }
3327
+ if (!specialized) continue;
3193
3328
  const reachable = new Set([inst.lifecycle.initial]);
3194
3329
  for (let n = 0; n < inst.lifecycle.states.length; n++)
3195
3330
  for (const edge of Object.values(inst.lifecycle.transitions))
@@ -3229,6 +3364,16 @@ export function compile(
3229
3364
  );
3230
3365
  }
3231
3366
  }
3367
+ for (const exposure of exposures)
3368
+ if (
3369
+ !document.instruments.find((inst) => inst.id === exposure.instrument)
3370
+ ?.actions[exposure.action]
3371
+ )
3372
+ fail(
3373
+ exposure.entry,
3374
+ `action ${exposure.action} is excluded by these bindings`,
3375
+ "expose an action available with these tunables",
3376
+ );
3232
3377
  const changed = new Set<string>();
3233
3378
  for (const decl of program.decls)
3234
3379
  if (decl.kind === "hide" || decl.kind === "expose") {
@@ -3254,6 +3399,24 @@ export function compile(
3254
3399
  );
3255
3400
  else action!.publicAction = decl.name!;
3256
3401
  }
3402
+ for (const decl of program.decls) {
3403
+ if (decl.kind !== "expose") continue;
3404
+ const parts = decl.target.split(".");
3405
+ parts.pop();
3406
+ const instrument = document.instruments.find(
3407
+ (item) => item.id === parts.join("_"),
3408
+ )!;
3409
+ if (
3410
+ Object.values(instrument.actions).filter(
3411
+ (action) => action.publicAction === decl.name,
3412
+ ).length > 1
3413
+ )
3414
+ fail(
3415
+ decl,
3416
+ `public action ${decl.name} is used more than once on ${parts.join(".")}`,
3417
+ "give each exposed action a distinct public name",
3418
+ );
3419
+ }
3257
3420
  const usedParties = new Set<string>();
3258
3421
  const collectParties = (value: unknown): void => {
3259
3422
  if (typeof value === "string") {