@hyperscale0/hsx 5.2.0 → 5.3.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 (54) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/src/ast.d.ts +0 -1
  3. package/dist/src/ast.d.ts.map +1 -1
  4. package/dist/src/ast.js +0 -11
  5. package/dist/src/ast.js.map +1 -1
  6. package/dist/src/cli.js +1 -1
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/compile.d.ts.map +1 -1
  9. package/dist/src/compile.js +185 -101
  10. package/dist/src/compile.js.map +1 -1
  11. package/dist/src/diagnostics.d.ts +14 -0
  12. package/dist/src/diagnostics.d.ts.map +1 -0
  13. package/dist/src/diagnostics.js +20 -0
  14. package/dist/src/diagnostics.js.map +1 -0
  15. package/dist/src/header-source.d.ts +3 -0
  16. package/dist/src/header-source.d.ts.map +1 -0
  17. package/dist/src/header-source.js +36 -0
  18. package/dist/src/header-source.js.map +1 -0
  19. package/dist/src/headers.d.ts.map +1 -1
  20. package/dist/src/headers.js +4 -6
  21. package/dist/src/headers.js.map +1 -1
  22. package/dist/src/keywords.d.ts +8 -2
  23. package/dist/src/keywords.d.ts.map +1 -1
  24. package/dist/src/keywords.js +13 -5
  25. package/dist/src/keywords.js.map +1 -1
  26. package/dist/src/lex.d.ts +1 -1
  27. package/dist/src/lex.d.ts.map +1 -1
  28. package/dist/src/lex.js +11 -3
  29. package/dist/src/lex.js.map +1 -1
  30. package/dist/src/parse.d.ts.map +1 -1
  31. package/dist/src/parse.js +22 -5
  32. package/dist/src/parse.js.map +1 -1
  33. package/dist/src/std-bundle.js +1 -1
  34. package/dist/src/std-bundle.js.map +1 -1
  35. package/dist/src/tunables.d.ts.map +1 -1
  36. package/dist/src/tunables.js +2 -1
  37. package/dist/src/tunables.js.map +1 -1
  38. package/dist/src/version.d.ts +1 -1
  39. package/dist/src/version.js +1 -1
  40. package/docs/README.md +5 -3
  41. package/package.json +3 -3
  42. package/src/ast.ts +0 -10
  43. package/src/cli.ts +1 -1
  44. package/src/compile.ts +267 -127
  45. package/src/diagnostics.ts +28 -0
  46. package/src/header-source.ts +44 -0
  47. package/src/headers.ts +4 -6
  48. package/src/keywords.ts +13 -5
  49. package/src/lex.ts +13 -5
  50. package/src/parse.ts +29 -5
  51. package/src/std-bundle.ts +1 -1
  52. package/src/tunables.ts +6 -1
  53. package/src/version.ts +1 -1
  54. package/std/financing.hsx +2 -2
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(
@@ -1141,7 +1153,7 @@ export function compile(
1141
1153
  param.value.kind === "default" ? param.value.type : param.value;
1142
1154
  const type =
1143
1155
  t.kind === "type" || t.kind === "call" ? t.name : text(t);
1144
- const v =
1156
+ let v =
1145
1157
  (supplied.has(param.key) && !attachmentInfo) || type === "enum"
1146
1158
  ? actual
1147
1159
  : resolve(
@@ -1149,6 +1161,12 @@ export function compile(
1149
1161
  new Set(),
1150
1162
  !!attachmentInfo && type === "party",
1151
1163
  );
1164
+ if (
1165
+ type === "duration" &&
1166
+ (v.kind === "text" || v.kind === "name") &&
1167
+ /^P/.test(v.value)
1168
+ )
1169
+ v = { ...v, kind: "duration" };
1152
1170
  environment.set(param.key, v);
1153
1171
  if (type === "enum" && t.kind === "call") {
1154
1172
  if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
@@ -1214,19 +1232,36 @@ export function compile(
1214
1232
  "reference needs an object name",
1215
1233
  "name a declared object",
1216
1234
  );
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;
1235
+ let targetType = objects.get(value.value)?.name;
1236
+ const candidates = [
1237
+ ...[...assignments.values()].map(
1238
+ (assignment) => [assignment.name, assignment.target] as const,
1239
+ ),
1240
+ ...program.decls
1241
+ .filter((decl) => decl.kind === "instrument")
1242
+ .map((decl) => [decl.name, decl.name] as const),
1243
+ ];
1244
+ for (const [instance, target] of candidates) {
1245
+ if (value.value === instance) {
1246
+ targetType = target;
1247
+ break;
1248
+ }
1249
+ if (
1250
+ !value.value.startsWith(`${instance}.`) &&
1251
+ !value.value.startsWith(`${instance}_`)
1252
+ )
1253
+ continue;
1254
+ const template = templates.get(target);
1255
+ if (!template) continue;
1256
+ const suffix = value.value
1257
+ .slice(instance.length + 1)
1258
+ .replaceAll(".", "_");
1259
+ const child = resolveChildExportPath(template, suffix);
1260
+ if (child) targetType = `${target}.${child}`;
1261
+ }
1221
1262
  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)
1263
+ !targetType ||
1264
+ (t.kind === "type" && t.target && targetType !== t.target)
1230
1265
  )
1231
1266
  fail(
1232
1267
  value,
@@ -1258,17 +1293,22 @@ export function compile(
1258
1293
  type === "date" ||
1259
1294
  type === "duration" ||
1260
1295
  type === "integer" ||
1261
- type === "text"
1296
+ type === "text" ||
1297
+ type === "boolean"
1262
1298
  ) {
1299
+ const bounds = tunableBounds(t);
1263
1300
  if (v.kind === "name" && v.value === "runtime") continue;
1264
- const expected = type === "integer" ? "number" : type;
1301
+ const expected =
1302
+ type === "integer"
1303
+ ? "number"
1304
+ : type === "boolean"
1305
+ ? "name"
1306
+ : type;
1265
1307
  if (
1266
- v.kind !== expected &&
1267
- !(
1268
- type === "duration" &&
1269
- (v.kind === "text" || v.kind === "name") &&
1270
- /^P/.test(v.value)
1271
- )
1308
+ v.kind !== expected ||
1309
+ (type === "boolean" &&
1310
+ v.kind === "name" &&
1311
+ !["true", "false"].includes(v.value))
1272
1312
  ) {
1273
1313
  if (type === "money" && v.kind === "name" && attachmentInfo) {
1274
1314
  const target = v.value.replace(/^subject\./, "");
@@ -1301,7 +1341,6 @@ export function compile(
1301
1341
  }
1302
1342
  try {
1303
1343
  const value = literal(v);
1304
- const bounds = tunableBounds(t);
1305
1344
  if (
1306
1345
  bounds &&
1307
1346
  (BigInt(String(value)) < BigInt(bounds.minimum) ||
@@ -1321,6 +1360,12 @@ export function compile(
1321
1360
  );
1322
1361
  throw error;
1323
1362
  }
1363
+ } else {
1364
+ fail(
1365
+ t,
1366
+ `unknown tunable type ${type}`,
1367
+ "use a declared HSX tunable type",
1368
+ );
1324
1369
  }
1325
1370
  } catch (error) {
1326
1371
  if (!(error instanceof CompileFailure)) throw error;
@@ -1373,6 +1418,21 @@ export function compile(
1373
1418
  "constraint needs two declared tunables",
1374
1419
  "name the compared tunables",
1375
1420
  );
1421
+ const numericKind = (expr: Expr) =>
1422
+ expr.kind === "name" && /^P/.test(expr.value)
1423
+ ? "duration"
1424
+ : expr.kind;
1425
+ if (
1426
+ !["money", "number", "percent", "duration"].includes(
1427
+ numericKind(left),
1428
+ ) ||
1429
+ numericKind(left) !== numericKind(right)
1430
+ )
1431
+ fail(
1432
+ rule,
1433
+ "constraint needs numeric tunables of the same type",
1434
+ "compare two amounts, integers, percentages or durations",
1435
+ );
1376
1436
  const a = BigInt(String(literal(left))),
1377
1437
  b = BigInt(String(literal(right)));
1378
1438
  if (
@@ -1473,7 +1533,7 @@ export function compile(
1473
1533
  ? se.value.items.map(text)
1474
1534
  : [text(se.value)];
1475
1535
  return names.some((n) => {
1476
- const reg = options.adapterRegistry?.[n];
1536
+ const reg = adapterTarget(n);
1477
1537
  if (!reg) return false;
1478
1538
  const op = reg.adapter.operationMap[reg.operation];
1479
1539
  return op?.subjectRequirements?.some(
@@ -1534,7 +1594,7 @@ export function compile(
1534
1594
  instrumentVal = data(rawEntries.get("instrument")!);
1535
1595
  if (famTuple) {
1536
1596
  checkTargetFamily(
1537
- instrumentVal as string | string[],
1597
+ instrumentVal,
1538
1598
  famTuple,
1539
1599
  rawEntries.get("instrument")!,
1540
1600
  );
@@ -1612,7 +1672,7 @@ export function compile(
1612
1672
  const lowerFields = (block: BlockExpr): UdlField[] => {
1613
1673
  const result: UdlField[] = [];
1614
1674
  for (const row of block.entries) {
1615
- const t = row.value.kind === "default" ? row.value.type : row.value;
1675
+ const { type: t } = fieldType(row);
1616
1676
  const constant =
1617
1677
  row.value.kind === "default" ? resolve(row.value.value) : undefined;
1618
1678
  const f = lowerFieldShape(row, resolve);
@@ -1842,7 +1902,7 @@ export function compile(
1842
1902
  op: "sum",
1843
1903
  values: [val(constant)],
1844
1904
  });
1845
- else f.value = literal(constant);
1905
+ else f.value = literal(constant, String(type));
1846
1906
  }
1847
1907
  result.push(f as UdlField);
1848
1908
  }
@@ -1850,9 +1910,11 @@ export function compile(
1850
1910
  };
1851
1911
  fields.push(...lowerFields(asBlock(body.get("fields"))));
1852
1912
  const lifecycle = data(
1853
- body.get("lifecycle") ?? emptyBlock,
1913
+ asBlock(body.get("lifecycle")),
1854
1914
  ) as UdlInstrument["lifecycle"];
1855
- lifecycle.transitions = {};
1915
+ lifecycle.transitions = Object.create(
1916
+ null,
1917
+ ) as UdlInstrument["lifecycle"]["transitions"];
1856
1918
  const inst: UdlInstrument = {
1857
1919
  id,
1858
1920
  ...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
@@ -1863,7 +1925,7 @@ export function compile(
1863
1925
  fields,
1864
1926
  calculate: calculations,
1865
1927
  lifecycle,
1866
- actions: {},
1928
+ actions: Object.create(null) as UdlInstrument["actions"],
1867
1929
  actionOrder: [],
1868
1930
  };
1869
1931
  for (const row of decl.body.entries.filter((e) =>
@@ -1970,15 +2032,8 @@ export function compile(
1970
2032
  "name a bound ADL adapter",
1971
2033
  );
1972
2034
  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
- )
2035
+ const target = adapterTarget(binding);
2036
+ if (!target)
1982
2037
  fail(
1983
2038
  boundary,
1984
2039
  `unknown boundary adapter ${binding}`,
@@ -2034,7 +2089,7 @@ export function compile(
2034
2089
  ? entry.value.items.map(adapterName)
2035
2090
  : [adapterName(entry.value)];
2036
2091
  for (const bindingName of bindingNames) {
2037
- const target = options.adapterRegistry?.[bindingName];
2092
+ const target = adapterTarget(bindingName);
2038
2093
  if (target) {
2039
2094
  const { adapter, operation } = target;
2040
2095
  const opBinding = adapter.operationMap[operation];
@@ -2562,15 +2617,32 @@ export function compile(
2562
2617
  }
2563
2618
  if (attachmentInfo && !attachmentInfo.child) {
2564
2619
  if (attachmentInfo.exposed.has(name)) {
2620
+ if (automatic(a.actor))
2621
+ fail(
2622
+ row,
2623
+ `${name} runs on the clock or its parent, not a caller`,
2624
+ "expose only caller actions",
2625
+ );
2565
2626
  a.publicAction = attachmentInfo.exposed.get(name)!;
2566
2627
  } else {
2567
2628
  delete a.publicAction;
2568
2629
  }
2569
2630
  }
2570
2631
  if (automatic(a.actor)) delete a.publicAction;
2571
- const checkSubjectPaths = (obj: unknown, span: Span) => {
2632
+ const checkSubjectPaths = (obj: unknown, span: Span, key = "") => {
2572
2633
  if (typeof obj === "string") {
2573
- if (obj.startsWith("subject.")) {
2634
+ if (
2635
+ [
2636
+ "field",
2637
+ "fields",
2638
+ "reference",
2639
+ "anchor",
2640
+ "subject",
2641
+ "at",
2642
+ "instruction",
2643
+ ].includes(key) &&
2644
+ obj.startsWith("subject.")
2645
+ ) {
2574
2646
  const subField = obj.split(".")[1]!;
2575
2647
  const req = a.subject?.requirements.find(
2576
2648
  (r) => r.field.name === subField,
@@ -2585,9 +2657,10 @@ export function compile(
2585
2657
  }
2586
2658
  }
2587
2659
  } else if (Array.isArray(obj)) {
2588
- for (const item of obj) checkSubjectPaths(item, span);
2660
+ for (const item of obj) checkSubjectPaths(item, span, key);
2589
2661
  } else if (obj !== null && typeof obj === "object") {
2590
- for (const val of Object.values(obj)) checkSubjectPaths(val, span);
2662
+ for (const [key, val] of Object.entries(obj))
2663
+ checkSubjectPaths(val, span, key);
2591
2664
  }
2592
2665
  };
2593
2666
  checkSubjectPaths(a.requires, row.span);
@@ -2617,6 +2690,27 @@ export function compile(
2617
2690
  (inst as unknown as Record<string, unknown>)[key] = data(
2618
2691
  body.get(key)!,
2619
2692
  );
2693
+ const shape = udlInstrumentSchema.safeParse(inst);
2694
+ if (!shape.success) {
2695
+ const issue = shape.error.issues[0]!;
2696
+ const actionName =
2697
+ issue.path[0] === "actions" ? String(issue.path[1]) : undefined;
2698
+ const node =
2699
+ decl.body.entries.find(
2700
+ (entry) =>
2701
+ entry.key ===
2702
+ (actionName ? `action ${actionName}` : String(issue.path[0])),
2703
+ ) ?? decl;
2704
+ failWithCode(
2705
+ {
2706
+ span: node.span,
2707
+ source: declarationSources.get(decl) ?? "program",
2708
+ },
2709
+ "UDL1003",
2710
+ `${id}.${issue.path.join(".")}: ${issue.message}`,
2711
+ "use the UDL typed clause shape",
2712
+ );
2713
+ }
2620
2714
  origins.push({
2621
2715
  path: `$.instruments[${document.instruments.length}]`,
2622
2716
  span: { ...origin, ...lineColAt(source, origin.start) },
@@ -2696,7 +2790,6 @@ export function compile(
2696
2790
  }
2697
2791
  }
2698
2792
 
2699
- // 1. Lower authored fields
2700
2793
  const authoredFields: UdlObjectField[] = [];
2701
2794
  const authoredNames: string[] = [];
2702
2795
  const fieldsBlock = asBlock(body.get("fields"));
@@ -2713,7 +2806,6 @@ export function compile(
2713
2806
  authoredNames.push(row.key);
2714
2807
  }
2715
2808
 
2716
- // 2. Process attachments
2717
2809
  const attachments: UdlObjectAttachment[] = [];
2718
2810
  for (const entry of decl.body.entries) {
2719
2811
  if (!entry.key.startsWith("attach ")) continue;
@@ -2746,6 +2838,12 @@ export function compile(
2746
2838
  for (const row of attachmentBlock.entries) {
2747
2839
  if (row.key === "rename") {
2748
2840
  for (const r of asBlock(row.value).entries) {
2841
+ if (renames.has(r.key))
2842
+ fail(
2843
+ r,
2844
+ `duplicate rename ${r.key}`,
2845
+ "rename each subject field once",
2846
+ );
2749
2847
  renames.set(r.key, text(r.value));
2750
2848
  renameEntries.set(r.key, r);
2751
2849
  }
@@ -2753,7 +2851,23 @@ export function compile(
2753
2851
  if (row.value.kind === "call") {
2754
2852
  const actionName = row.value.name;
2755
2853
  const publicName = text(row.value.args[0]!);
2854
+ if (
2855
+ exposed.has(actionName) ||
2856
+ !template.body.entries.some(
2857
+ (entry) => entry.key === `action ${actionName}`,
2858
+ )
2859
+ )
2860
+ fail(
2861
+ row,
2862
+ `unknown or repeated action ${actionName}`,
2863
+ "expose one declared action once",
2864
+ );
2756
2865
  exposed.set(actionName, publicName);
2866
+ exposures.push({
2867
+ instrument: instId,
2868
+ action: actionName,
2869
+ entry: row,
2870
+ });
2757
2871
  }
2758
2872
  } else {
2759
2873
  tunableEntries.push(row);
@@ -2882,7 +2996,6 @@ export function compile(
2882
2996
  }
2883
2997
  }
2884
2998
 
2885
- // 4. Validate columns
2886
2999
  const columnsExpr = body.get("columns");
2887
3000
  let columns: string[] = [];
2888
3001
  if (columnsExpr) {
@@ -2950,7 +3063,11 @@ export function compile(
2950
3063
  // Propagate invoked requirements with the conditions on each invocation path.
2951
3064
  let changedInvocations = true;
2952
3065
  let invocationIterations = 0;
2953
- while (changedInvocations && invocationIterations < 32) {
3066
+ const invocationDepth = document.instruments.reduce(
3067
+ (total, inst) => total + inst.actionOrder.length,
3068
+ 0,
3069
+ );
3070
+ while (changedInvocations && invocationIterations < invocationDepth) {
2954
3071
  changedInvocations = false;
2955
3072
  invocationIterations++;
2956
3073
  for (const inst of document.instruments) {
@@ -3167,6 +3284,17 @@ export function compile(
3167
3284
  const eliminatedStates = new Map<string, Set<string>>();
3168
3285
  // Remove branches excluded by immutable tunables before checking reference states.
3169
3286
  for (const inst of document.instruments) {
3287
+ if (
3288
+ !inst.lifecycle.states.includes(inst.lifecycle.initial) ||
3289
+ Object.values(inst.lifecycle.transitions).some(
3290
+ (edge) =>
3291
+ edge.from.some((state) => !inst.lifecycle.states.includes(state)) ||
3292
+ (edge.to !== "preserve" &&
3293
+ !inst.lifecycle.states.includes(edge.to)),
3294
+ )
3295
+ )
3296
+ continue;
3297
+ let specialized = false;
3170
3298
  for (const [key, action] of Object.entries(inst.actions)) {
3171
3299
  const constant = (value: import("@hyperscale0/udl").UdlValue) => {
3172
3300
  if ("literal" in value) return value.literal;
@@ -3188,8 +3316,10 @@ export function compile(
3188
3316
  ) {
3189
3317
  delete inst.actions[key];
3190
3318
  delete inst.lifecycle.transitions[key];
3319
+ specialized = true;
3191
3320
  }
3192
3321
  }
3322
+ if (!specialized) continue;
3193
3323
  const reachable = new Set([inst.lifecycle.initial]);
3194
3324
  for (let n = 0; n < inst.lifecycle.states.length; n++)
3195
3325
  for (const edge of Object.values(inst.lifecycle.transitions))
@@ -3229,6 +3359,16 @@ export function compile(
3229
3359
  );
3230
3360
  }
3231
3361
  }
3362
+ for (const exposure of exposures)
3363
+ if (
3364
+ !document.instruments.find((inst) => inst.id === exposure.instrument)
3365
+ ?.actions[exposure.action]
3366
+ )
3367
+ fail(
3368
+ exposure.entry,
3369
+ `action ${exposure.action} is excluded by these bindings`,
3370
+ "expose an action available with these tunables",
3371
+ );
3232
3372
  const changed = new Set<string>();
3233
3373
  for (const decl of program.decls)
3234
3374
  if (decl.kind === "hide" || decl.kind === "expose") {