@hyperscale0/hsx 2.4.1 → 3.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.
Files changed (249) hide show
  1. package/CHANGELOG.md +2 -408
  2. package/README.md +3 -92
  3. package/dist/src/ast.d.ts +99 -310
  4. package/dist/src/ast.d.ts.map +1 -1
  5. package/dist/src/ast.js +16 -69
  6. package/dist/src/ast.js.map +1 -1
  7. package/dist/src/cli.d.ts +6 -19
  8. package/dist/src/cli.d.ts.map +1 -1
  9. package/dist/src/cli.js +76 -315
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/compile.d.ts +23 -73
  12. package/dist/src/compile.d.ts.map +1 -1
  13. package/dist/src/compile.js +1113 -148
  14. package/dist/src/compile.js.map +1 -1
  15. package/dist/src/cost.d.ts +8 -77
  16. package/dist/src/cost.d.ts.map +1 -1
  17. package/dist/src/cost.js +13 -422
  18. package/dist/src/cost.js.map +1 -1
  19. package/dist/src/format.d.ts +8 -7
  20. package/dist/src/format.d.ts.map +1 -1
  21. package/dist/src/format.js +9 -175
  22. package/dist/src/format.js.map +1 -1
  23. package/dist/src/headers.d.ts +35 -0
  24. package/dist/src/headers.d.ts.map +1 -0
  25. package/dist/src/headers.js +108 -0
  26. package/dist/src/headers.js.map +1 -0
  27. package/dist/src/index.d.ts +6 -12
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +4 -9
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/lex.d.ts +8 -28
  32. package/dist/src/lex.d.ts.map +1 -1
  33. package/dist/src/lex.js +88 -146
  34. package/dist/src/lex.js.map +1 -1
  35. package/dist/src/parse.d.ts +4 -17
  36. package/dist/src/parse.d.ts.map +1 -1
  37. package/dist/src/parse.js +402 -1040
  38. package/dist/src/parse.js.map +1 -1
  39. package/dist/src/std-bundle.d.ts.map +1 -1
  40. package/dist/src/std-bundle.js +12 -20
  41. package/dist/src/std-bundle.js.map +1 -1
  42. package/dist/src/std-library.d.ts +1 -1
  43. package/dist/src/std-library.d.ts.map +1 -1
  44. package/dist/src/std-library.js +1 -5
  45. package/dist/src/std-library.js.map +1 -1
  46. package/dist/src/tunables.d.ts +7 -0
  47. package/dist/src/tunables.d.ts.map +1 -0
  48. package/dist/src/tunables.js +26 -0
  49. package/dist/src/tunables.js.map +1 -0
  50. package/dist/src/version.d.ts +2 -4
  51. package/dist/src/version.d.ts.map +1 -1
  52. package/dist/src/version.js +2 -4
  53. package/dist/src/version.js.map +1 -1
  54. package/docs/README.md +179 -41
  55. package/docs/headers.md +44 -0
  56. package/examples/cost-table.json +99 -751
  57. package/examples/library.hsx +59 -0
  58. package/package.json +9 -17
  59. package/src/ast.ts +82 -441
  60. package/src/cli.ts +77 -362
  61. package/src/compile.ts +1535 -247
  62. package/src/cost.ts +28 -671
  63. package/src/format.ts +12 -256
  64. package/src/headers.ts +126 -0
  65. package/src/index.ts +7 -39
  66. package/src/lex.ts +99 -195
  67. package/src/parse.ts +403 -1209
  68. package/src/std-bundle.ts +12 -20
  69. package/src/std-library.ts +2 -7
  70. package/src/tunables.ts +31 -0
  71. package/src/version.ts +2 -5
  72. package/std/approvals.hsx +17 -0
  73. package/std/cards.hsx +63 -0
  74. package/std/collections.hsx +31 -0
  75. package/std/escrow.hsx +43 -0
  76. package/std/financing.hsx +372 -0
  77. package/std/insurance.hsx +59 -0
  78. package/std/lending.hsx +115 -0
  79. package/std/marketplace.hsx +24 -0
  80. package/std/money.hsx +195 -0
  81. package/std/savings.hsx +44 -0
  82. package/std/travel.hsx +98 -0
  83. package/std/wallet.hsx +41 -0
  84. package/dist/src/diagnostics.d.ts +0 -13
  85. package/dist/src/diagnostics.d.ts.map +0 -1
  86. package/dist/src/diagnostics.js +0 -587
  87. package/dist/src/diagnostics.js.map +0 -1
  88. package/dist/src/emit.d.ts +0 -51
  89. package/dist/src/emit.d.ts.map +0 -1
  90. package/dist/src/emit.js +0 -192
  91. package/dist/src/emit.js.map +0 -1
  92. package/dist/src/entry-overrides.d.ts +0 -58
  93. package/dist/src/entry-overrides.d.ts.map +0 -1
  94. package/dist/src/entry-overrides.js +0 -284
  95. package/dist/src/entry-overrides.js.map +0 -1
  96. package/dist/src/ir.d.ts +0 -73
  97. package/dist/src/ir.d.ts.map +0 -1
  98. package/dist/src/ir.js +0 -15
  99. package/dist/src/ir.js.map +0 -1
  100. package/dist/src/limits.d.ts +0 -23
  101. package/dist/src/limits.d.ts.map +0 -1
  102. package/dist/src/limits.js +0 -23
  103. package/dist/src/limits.js.map +0 -1
  104. package/dist/src/lsp/server.d.ts +0 -32
  105. package/dist/src/lsp/server.d.ts.map +0 -1
  106. package/dist/src/lsp/server.js +0 -391
  107. package/dist/src/lsp/server.js.map +0 -1
  108. package/dist/src/modules.d.ts +0 -38
  109. package/dist/src/modules.d.ts.map +0 -1
  110. package/dist/src/modules.js +0 -372
  111. package/dist/src/modules.js.map +0 -1
  112. package/dist/src/typecheck.d.ts +0 -8
  113. package/dist/src/typecheck.d.ts.map +0 -1
  114. package/dist/src/typecheck.js +0 -4160
  115. package/dist/src/typecheck.js.map +0 -1
  116. package/docs/guide/01-first-program.md +0 -24
  117. package/docs/guide/02-money.md +0 -25
  118. package/docs/guide/03-instruments.md +0 -93
  119. package/docs/guide/04-lifecycles.md +0 -40
  120. package/docs/guide/05-fees-and-splits.md +0 -45
  121. package/docs/guide/06-schedules.md +0 -26
  122. package/docs/guide/07-composition.md +0 -52
  123. package/docs/guide/08-writing-a-module.md +0 -85
  124. package/docs/guide/09-cost.md +0 -24
  125. package/docs/guide/10-diagnostics.md +0 -27
  126. package/docs/llms-full.txt +0 -3684
  127. package/docs/llms.txt +0 -44
  128. package/docs/piece-plans.md +0 -120
  129. package/docs/playground.md +0 -55
  130. package/docs/reference/cli.md +0 -36
  131. package/docs/reference/diagnostics.md +0 -901
  132. package/docs/reference/grammar.md +0 -38
  133. package/docs/reference/std/advance.md +0 -112
  134. package/docs/reference/std/cancellable_booking.md +0 -159
  135. package/docs/reference/std/captured_payment.md +0 -115
  136. package/docs/reference/std/conditional_disbursement.md +0 -101
  137. package/docs/reference/std/credit_facility.md +0 -108
  138. package/docs/reference/std/held_payment.md +0 -173
  139. package/docs/reference/std/instant_transfer.md +0 -97
  140. package/docs/reference/std/metered.md +0 -89
  141. package/docs/reference/std/pooled_split.md +0 -95
  142. package/docs/reference/std/premium_forward.md +0 -119
  143. package/docs/reference/std/reconciled_payout.md +0 -103
  144. package/docs/reference/std/rotating_pool.md +0 -130
  145. package/docs/reference/std/scheduled.md +0 -136
  146. package/docs/reference/std/security_deposit.md +0 -138
  147. package/docs/reference/std/settlement_batch.md +0 -123
  148. package/docs/reference/std/swap.md +0 -133
  149. package/docs/reference/std/threshold_pool.md +0 -138
  150. package/docs/reference/std/weighted_distribution.md +0 -135
  151. package/docs/reference/types.md +0 -61
  152. package/docs/reference/udl-output.md +0 -13
  153. package/examples/01-first-program/README.md +0 -45
  154. package/examples/01-first-program/tip-jar.hsx +0 -17
  155. package/examples/02-imports-and-modules/README.md +0 -96
  156. package/examples/02-imports-and-modules/photo-booth.hsx +0 -37
  157. package/examples/03-diagnostics/README.md +0 -68
  158. package/examples/03-diagnostics/corner-shop-fixed.hsx +0 -22
  159. package/examples/03-diagnostics/corner-shop.hsx +0 -19
  160. package/examples/04-complete-product/README.md +0 -84
  161. package/examples/04-complete-product/study-hall.hsx +0 -67
  162. package/examples/05-authored-instrument/README.md +0 -5
  163. package/examples/05-authored-instrument/payment.hsx +0 -37
  164. package/examples/05-watch-club/README.md +0 -6
  165. package/examples/05-watch-club/watch-club.hsx +0 -123
  166. package/examples/README.md +0 -48
  167. package/examples/advance/README.md +0 -3
  168. package/examples/advance/advance.hsx +0 -13
  169. package/examples/advance/advance.udl +0 -367
  170. package/examples/cancellable_booking/README.md +0 -3
  171. package/examples/cancellable_booking/cancellable_booking.hsx +0 -14
  172. package/examples/cancellable_booking/cancellable_booking.udl +0 -413
  173. package/examples/captured_payment/README.md +0 -3
  174. package/examples/captured_payment/captured_payment.hsx +0 -17
  175. package/examples/captured_payment/captured_payment.udl +0 -686
  176. package/examples/conditional_disbursement/README.md +0 -3
  177. package/examples/conditional_disbursement/conditional_disbursement.hsx +0 -15
  178. package/examples/conditional_disbursement/conditional_disbursement.udl +0 -418
  179. package/examples/credit_facility/README.md +0 -3
  180. package/examples/credit_facility/credit_facility.hsx +0 -24
  181. package/examples/credit_facility/credit_facility.udl +0 -1396
  182. package/examples/held_payment/README.md +0 -3
  183. package/examples/held_payment/held_payment.hsx +0 -13
  184. package/examples/held_payment/held_payment.udl +0 -569
  185. package/examples/instant_transfer/README.md +0 -3
  186. package/examples/instant_transfer/instant_transfer.hsx +0 -16
  187. package/examples/instant_transfer/instant_transfer.udl +0 -376
  188. package/examples/metered/README.md +0 -3
  189. package/examples/metered/metered.hsx +0 -13
  190. package/examples/metered/metered.udl +0 -253
  191. package/examples/pooled_split/README.md +0 -3
  192. package/examples/pooled_split/pooled_split.hsx +0 -15
  193. package/examples/pooled_split/pooled_split.udl +0 -432
  194. package/examples/premium_forward/README.md +0 -3
  195. package/examples/premium_forward/premium_forward.hsx +0 -19
  196. package/examples/premium_forward/premium_forward.udl +0 -604
  197. package/examples/reconciled_payout/README.md +0 -3
  198. package/examples/reconciled_payout/reconciled_payout.hsx +0 -13
  199. package/examples/reconciled_payout/reconciled_payout.udl +0 -312
  200. package/examples/rotating_pool/README.md +0 -3
  201. package/examples/rotating_pool/rotating_pool.hsx +0 -18
  202. package/examples/rotating_pool/rotating_pool.udl +0 -4370
  203. package/examples/scheduled/README.md +0 -3
  204. package/examples/scheduled/scheduled.hsx +0 -12
  205. package/examples/scheduled/scheduled.udl +0 -355
  206. package/examples/security_deposit/README.md +0 -3
  207. package/examples/security_deposit/security_deposit.hsx +0 -21
  208. package/examples/security_deposit/security_deposit.udl +0 -320
  209. package/examples/settlement_batch/README.md +0 -3
  210. package/examples/settlement_batch/settlement_batch.hsx +0 -19
  211. package/examples/settlement_batch/settlement_batch.udl +0 -757
  212. package/examples/swap/README.md +0 -3
  213. package/examples/swap/swap.hsx +0 -19
  214. package/examples/swap/swap.udl +0 -892
  215. package/examples/threshold_pool/README.md +0 -3
  216. package/examples/threshold_pool/threshold_pool.hsx +0 -16
  217. package/examples/threshold_pool/threshold_pool.udl +0 -680
  218. package/examples/weighted_distribution/README.md +0 -3
  219. package/examples/weighted_distribution/weighted_distribution.hsx +0 -20
  220. package/examples/weighted_distribution/weighted_distribution.udl +0 -348
  221. package/skills/hsx/SKILL.md +0 -449
  222. package/src/diagnostics.ts +0 -620
  223. package/src/emit.ts +0 -282
  224. package/src/entry-overrides.ts +0 -428
  225. package/src/ir.ts +0 -113
  226. package/src/limits.ts +0 -22
  227. package/src/lsp/server.ts +0 -460
  228. package/src/modules.ts +0 -452
  229. package/src/typecheck.ts +0 -5515
  230. package/std/SEMANTICS.md +0 -44
  231. package/std/money_flows/advance.hsx +0 -235
  232. package/std/money_flows/cancellable_booking.hsx +0 -402
  233. package/std/money_flows/captured_payment.hsx +0 -220
  234. package/std/money_flows/conditional_disbursement.hsx +0 -226
  235. package/std/money_flows/credit_facility.hsx +0 -214
  236. package/std/money_flows/held_payment.hsx +0 -812
  237. package/std/money_flows/index.hsx +0 -4
  238. package/std/money_flows/instant_transfer.hsx +0 -231
  239. package/std/money_flows/metered.hsx +0 -104
  240. package/std/money_flows/pooled_split.hsx +0 -109
  241. package/std/money_flows/premium_forward.hsx +0 -307
  242. package/std/money_flows/reconciled_payout.hsx +0 -119
  243. package/std/money_flows/rotating_pool.hsx +0 -305
  244. package/std/money_flows/scheduled.hsx +0 -599
  245. package/std/money_flows/security_deposit.hsx +0 -248
  246. package/std/money_flows/settlement_batch.hsx +0 -173
  247. package/std/money_flows/swap.hsx +0 -1192
  248. package/std/money_flows/threshold_pool.hsx +0 -307
  249. package/std/money_flows/weighted_distribution.hsx +0 -237
package/src/compile.ts CHANGED
@@ -1,273 +1,1561 @@
1
- /**
2
- * The HSX compiler driver: source text in, three-verdict result out.
3
- *
4
- * - `valid`: the program lowers cleanly; the IR document is ready
5
- * for the independent checker.
6
- * - `warning`: the program lowers, and the compiler's lint voice has notes
7
- * the author should read (the artifacts are still present and usable).
8
- * - `invalid`: the program cannot be lowered; diagnostics say why, in the
9
- * author's language, each anchored to a source line and column.
10
- *
11
- * Every diagnostic points at SOURCE coordinates, never at lowered IR paths.
12
- */
13
-
14
- import type { UdlIssueCode } from "@hyperscale0/udl";
15
- import type { ResolvedProgramActionPlan } from "./ir.ts";
16
- import { lineColIn, lineIndex } from "./ast.ts";
17
- import {
18
- buildCostManifest,
19
- type UdlCostManifest,
20
- type UdlCostTables,
21
- } from "./cost.ts";
22
- import { lowerGeneralProgram, type OriginMapEntry } from "./emit.ts";
1
+ import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
23
2
  import {
24
- resolveProgramModules,
25
- type HsxCompilerHost,
26
- type ModuleOrigin,
27
- } from "./modules.ts";
3
+ validateUdl,
4
+ type UdlAction,
5
+ type UdlCalculation,
6
+ type UdlDocument,
7
+ type UdlField,
8
+ type UdlInstrument,
9
+ type UdlValue,
10
+ } from "@hyperscale0/udl";
11
+ import { tunableBounds } from "./tunables.ts";
28
12
  import { parseProgram } from "./parse.ts";
29
- import { checkGeneralProgram } from "./typecheck.ts";
30
-
31
- type Json = Record<string, unknown>;
32
-
33
- type CompileVerdict = "invalid" | "valid" | "warning";
13
+ import {
14
+ lineColAt,
15
+ type BlockExpr,
16
+ type Diagnostic,
17
+ type Expr,
18
+ type InstrumentDecl,
19
+ type ObjectDecl,
20
+ type Span,
21
+ } from "./ast.ts";
22
+ import { bundledStandardLibrary, type StandardLibrary } from "./std-library.ts";
34
23
 
35
- export interface CompileDiagnostic {
36
- /** 1-indexed source column. */
37
- readonly column: number;
38
- /** Imported module file when the diagnostic belongs to module source. */
39
- readonly file?: string;
40
- /** 1-indexed source line. */
41
- readonly line: number;
42
- readonly message: string;
43
- /** Source offsets in UTF-16 code units, matching parser and checker spans. */
44
- readonly span: { readonly end: number; readonly start: number };
45
- /** UTF-8 byte span in root source, omitted when referring to an imported module. */
46
- readonly byteSpan?: { readonly end: number; readonly start: number };
47
- readonly code?: string;
48
- readonly fix?: string;
49
- /** Canonical UDL path when lowering rejected an emitted clause. */
50
- readonly path?: string;
51
- /** UDL validator category when the emitter rejected its candidate document. */
52
- readonly udlCode?: UdlIssueCode;
53
- readonly severity: "error" | "warning";
54
- /** The stage that raised it: parse, check, or lower. */
55
- readonly stage: "bind" | "check" | "lower" | "parse" | "typecheck";
24
+ export interface CompileDiagnostic extends Diagnostic {
25
+ line: number;
26
+ column: number;
27
+ severity: "error";
28
+ stage: "parse" | "check" | "lower";
56
29
  }
57
-
58
- interface CompileArtifacts {
59
- readonly actionPlans?: readonly ResolvedProgramActionPlan[];
60
- /** The canonical UDL document. */
61
- readonly document: Json;
62
- /** Canonical UDL paths bound to their narrowest authored source terms. */
63
- readonly originMap: readonly CompileOriginMapEntry[];
64
- /** Deterministic compile-time cost, pinned to one versioned table. */
65
- readonly costManifest: UdlCostManifest;
30
+ export interface CompileOriginMapEntry {
31
+ path: string;
32
+ span: Span & { line: number; column: number };
66
33
  }
67
-
68
- export interface CompileOptions extends HsxCompilerHost {
69
- /** One table, or one per billing currency; the program's money fields pick. */
70
- readonly costTable?: UdlCostTables;
34
+ export interface CompileOptions {
35
+ standardLibrary?: StandardLibrary;
71
36
  }
72
-
73
- export interface CompileOriginMapEntry {
74
- readonly path: string;
75
- readonly span: {
76
- /** Exclusive UTF-8 byte offset. */
77
- readonly end: number;
78
- /** 1-indexed source column. */
79
- readonly column: number;
80
- /** 1-indexed source line. */
81
- readonly line: number;
82
- /** Inclusive UTF-8 byte offset. */
83
- readonly start: number;
37
+ export interface CompileResult {
38
+ verdict: "valid" | "invalid";
39
+ diagnostics: CompileDiagnostic[];
40
+ artifacts?: {
41
+ document: UdlDocument;
42
+ costManifest: UdlCostManifest;
43
+ originMap: CompileOriginMapEntry[];
84
44
  };
85
45
  }
86
-
87
- export interface CompileResult {
88
- /** Present exactly when the verdict is not `invalid`. */
89
- readonly artifacts?: CompileArtifacts;
90
- readonly diagnostics: readonly CompileDiagnostic[];
91
- readonly verdict: CompileVerdict;
46
+ class CompileFailure extends Error {
47
+ constructor(readonly diagnostic: Diagnostic) {
48
+ super(diagnostic.message);
49
+ }
50
+ }
51
+ function fail(expr: { span: Span }, message: string, fix: string): never {
52
+ throw new CompileFailure({ code: "HSX1001", message, fix, span: expr.span });
53
+ }
54
+ const emptyBlock: BlockExpr = {
55
+ kind: "block",
56
+ entries: [],
57
+ span: { start: 0, end: 0 },
58
+ };
59
+ const camel = (name: string) =>
60
+ name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
61
+ /** Clock and parent actors run without a caller, so they have no public name. */
62
+ const automatic = (actor: UdlAction["actor"]) =>
63
+ actor === "clock" || (typeof actor === "object" && "parent" in actor);
64
+ const title = (name: string) =>
65
+ name[0]!.toUpperCase() + name.slice(1).replaceAll("_", " ");
66
+ function entries(block: BlockExpr): Map<string, Expr> {
67
+ const map = new Map<string, Expr>();
68
+ for (const row of block.entries) {
69
+ const previous = map.get(row.key);
70
+ if (previous) {
71
+ if (
72
+ !["requires", "moves", "invariants", "invoke", "calculate"].includes(
73
+ row.key,
74
+ )
75
+ )
76
+ fail(row, `duplicate ${row.key}`, "keep one value for this name");
77
+ const items = (expr: Expr) =>
78
+ expr.kind === "list" ? expr.items : [expr];
79
+ map.set(row.key, {
80
+ kind: "list",
81
+ items: [...items(previous), ...items(row.value)],
82
+ span: row.span,
83
+ });
84
+ } else map.set(row.key, row.value);
85
+ }
86
+ return map;
87
+ }
88
+ function asBlock(expr: Expr | undefined): BlockExpr {
89
+ if (!expr) return emptyBlock;
90
+ if (expr.kind !== "block")
91
+ fail(expr, "expected a block", "write { name: value }");
92
+ return expr;
93
+ }
94
+ function text(expr: Expr): string {
95
+ if ("value" in expr && typeof expr.value === "string") return expr.value;
96
+ return fail(
97
+ expr,
98
+ "expected a name or literal",
99
+ "write a single name or literal",
100
+ );
101
+ }
102
+ function decimal(raw: string, scale: number, expr: Expr): string {
103
+ const [whole = "", fraction = ""] = raw.split(".");
104
+ if (!/^\d+$/.test(whole) || fraction.length > scale)
105
+ fail(
106
+ expr,
107
+ `literal has more than ${scale} decimal places`,
108
+ `write at most ${scale} decimal places`,
109
+ );
110
+ return (
111
+ BigInt(whole) * 10n ** BigInt(scale) +
112
+ BigInt(fraction.padEnd(scale, "0") || "0")
113
+ ).toString();
114
+ }
115
+ function literal(expr: Expr): string | number | boolean {
116
+ if (expr.kind === "money") {
117
+ const [raw, currency] = expr.value.split(" ");
118
+ if (currency !== "SAR")
119
+ fail(
120
+ expr,
121
+ `currency ${currency} differs from SAR`,
122
+ "write this amount in SAR",
123
+ );
124
+ const result = decimal(raw!, 2, expr);
125
+ if (result.length > 18)
126
+ fail(
127
+ expr,
128
+ "amount exceeds 18 minor-unit digits",
129
+ "write a smaller amount",
130
+ );
131
+ return result;
132
+ }
133
+ if (expr.kind === "percent") {
134
+ const n = Number(decimal(expr.value, 2, expr));
135
+ if (n > 10000)
136
+ fail(
137
+ expr,
138
+ "percentage exceeds 100%",
139
+ "write a percentage from 0% to 100%",
140
+ );
141
+ return n;
142
+ }
143
+ if (expr.kind === "number") {
144
+ const n = Number(expr.value);
145
+ if (!Number.isSafeInteger(n))
146
+ fail(
147
+ expr,
148
+ "expected a safe integer",
149
+ "write an integer, a percentage, or an amount with SAR",
150
+ );
151
+ return n;
152
+ }
153
+ if (
154
+ expr.kind === "duration" ||
155
+ (expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)) ||
156
+ (expr.kind === "text" && /^P(?:\d|T)/.test(expr.value))
157
+ ) {
158
+ const short = /^(\d+)(ms|s|m|h|d|w)$/.exec(expr.value);
159
+ const units: Record<string, number> = {
160
+ ms: 1,
161
+ s: 1000,
162
+ m: 60000,
163
+ h: 3600000,
164
+ d: 86400000,
165
+ w: 604800000,
166
+ };
167
+ const iso =
168
+ /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(
169
+ expr.value,
170
+ );
171
+ const n = short
172
+ ? Number(short[1]) * units[short[2]!]!
173
+ : iso
174
+ ? Number(iso[1] ?? 0) * units.w! +
175
+ Number(iso[2] ?? 0) * units.d! +
176
+ Number(iso[3] ?? 0) * units.h! +
177
+ Number(iso[4] ?? 0) * units.m! +
178
+ Number(iso[5] ?? 0) * units.s!
179
+ : NaN;
180
+ if (!Number.isSafeInteger(n) || n <= 0)
181
+ fail(
182
+ expr,
183
+ "invalid duration",
184
+ "write a positive fixed duration such as 48h or 3d",
185
+ );
186
+ return n;
187
+ }
188
+ if (expr.kind === "date") {
189
+ const raw =
190
+ expr.value.length === 10 ? expr.value + "T00:00:00Z" : expr.value;
191
+ const n = Date.parse(raw);
192
+ if (
193
+ !Number.isFinite(n) ||
194
+ new Date(raw.slice(0, 10) + "T00:00:00Z").toISOString().slice(0, 10) !==
195
+ raw.slice(0, 10)
196
+ )
197
+ fail(
198
+ expr,
199
+ "invalid date",
200
+ "write a calendar date or a timestamp with an explicit offset",
201
+ );
202
+ return new Date(n).toISOString();
203
+ }
204
+ if (expr.kind === "text") return expr.value;
205
+ if (expr.kind === "name" && ["true", "false"].includes(expr.value))
206
+ return expr.value === "true";
207
+ return fail(expr, "expected a literal", "write a typed constant");
92
208
  }
93
209
 
94
210
  export function compile(
95
211
  source: string,
96
212
  options: CompileOptions = {},
97
213
  ): CompileResult {
98
- const diagnostics: CompileDiagnostic[] = [];
99
- // Scanned once here rather than per diagnostic: a source can carry one
100
- // diagnostic per two bytes, and rescanning from offset zero for each one
101
- // made a 128 KB file cost 2.4 seconds of coordinate arithmetic.
102
- const lines = lineIndex(source);
103
- const byteOffsets = utf8OffsetIndex(source);
104
- const at = (
214
+ const parsed = parseProgram(source);
215
+ const diagnostic = (
216
+ d: Diagnostic,
105
217
  stage: CompileDiagnostic["stage"],
106
- severity: CompileDiagnostic["severity"],
107
- message: string,
108
- span: { readonly end: number; readonly start: number },
109
- details?: {
110
- readonly code: string;
111
- readonly fix: string;
112
- readonly path?: string;
113
- readonly udlCode?: UdlIssueCode;
114
- },
115
- origin?: ModuleOrigin,
116
- ): void => {
117
- const position = origin ?? lineColIn(lines, span.start);
118
- diagnostics.push({
119
- column: position.column,
120
- ...(origin
121
- ? { file: origin.moduleName }
122
- : {
123
- byteSpan: {
124
- end:
125
- byteOffsets[span.end] ?? byteOffsets[byteOffsets.length - 1]!,
126
- start: byteOffsets[span.start] ?? 0,
127
- },
218
+ ): CompileDiagnostic => ({
219
+ ...d,
220
+ ...lineColAt(source, d.span.start),
221
+ severity: "error",
222
+ stage,
223
+ });
224
+ if (parsed.diagnostics.length)
225
+ return {
226
+ verdict: "invalid",
227
+ diagnostics: parsed.diagnostics.map((d) => diagnostic(d, "parse")),
228
+ };
229
+ const program = parsed.program;
230
+ try {
231
+ if (program.header)
232
+ fail(
233
+ program,
234
+ "compile a program, not a header",
235
+ 'start with program product_name "Title"',
236
+ );
237
+ if (program.currency !== "SAR")
238
+ fail(
239
+ program,
240
+ "this release supports SAR",
241
+ "write currency SAR or omit currency",
242
+ );
243
+ const templates = new Map<string, InstrumentDecl>();
244
+ const used = new Set<string>();
245
+ for (const use of program.decls.filter((d) => d.kind === "use")) {
246
+ if (used.has(use.name))
247
+ fail(
248
+ use,
249
+ `header ${use.name} is used twice`,
250
+ "remove the duplicate use line",
251
+ );
252
+ used.add(use.name);
253
+ const content = (
254
+ options.standardLibrary ?? bundledStandardLibrary
255
+ ).source(use.name);
256
+ if (!content)
257
+ fail(use, `unknown header ${use.name}`, "choose a published header");
258
+ const header = parseProgram(content);
259
+ if (
260
+ header.diagnostics.length ||
261
+ !header.program.header ||
262
+ header.program.name !== use.name
263
+ )
264
+ fail(
265
+ use,
266
+ `header ${use.name} is malformed`,
267
+ "repair the header source before compiling",
268
+ );
269
+ for (const decl of header.program.decls)
270
+ if (decl.kind === "instrument")
271
+ templates.set(`${use.name}.${decl.name}`, decl);
272
+ }
273
+ const document: UdlDocument = {
274
+ udl: 3,
275
+ version: 1,
276
+ product: program.name,
277
+ title: program.title,
278
+ currency: "SAR",
279
+ parties: {
280
+ programOperator: { kind: "business", role: "program_operator" },
281
+ programTax: { kind: "business", role: "tax_payable" },
282
+ programFines: { kind: "business", role: "fine_payable" },
283
+ programCosts: { kind: "business", role: "cost_recovery" },
284
+ },
285
+ instruments: [],
286
+ };
287
+ const objects = new Map<string, ObjectDecl>();
288
+ const names = new Set<string>();
289
+ for (const decl of program.decls) {
290
+ if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
291
+ continue;
292
+ if (names.has(decl.name))
293
+ fail(
294
+ decl,
295
+ `duplicate declaration ${decl.name}`,
296
+ "give this declaration a distinct name",
297
+ );
298
+ names.add(decl.name);
299
+ if (decl.kind === "object") objects.set(decl.name, decl);
300
+ if (decl.kind === "party") {
301
+ if (!["person", "business", "staff"].includes(decl.partyKind))
302
+ fail(
303
+ decl,
304
+ `unknown party kind ${decl.partyKind}`,
305
+ "choose person, business, or staff",
306
+ );
307
+ document.parties[decl.name] = {
308
+ kind: decl.partyKind as "person" | "business" | "staff",
309
+ ...(decl.role ? { role: decl.role } : {}),
310
+ };
311
+ }
312
+ }
313
+ const origins: CompileOriginMapEntry[] = [];
314
+ const materialApprovals = new Set<UdlAction>();
315
+ const implicitDecisions = new Map<
316
+ string,
317
+ { target: string; action: string; party: string; origin: Span }
318
+ >();
319
+ const addInstrument = (
320
+ decl: InstrumentDecl,
321
+ id: string,
322
+ arguments_: BlockExpr,
323
+ origin: Span,
324
+ inherited = new Map<string, Expr>(),
325
+ inheritedApprovers = new Set<string>(),
326
+ inheritedEnums = new Map<string, string[]>(),
327
+ ) => {
328
+ const enums = new Map(inheritedEnums);
329
+ for (const parameter of decl.parameters) {
330
+ const type =
331
+ parameter.value.kind === "default"
332
+ ? parameter.value.type
333
+ : parameter.value;
334
+ if (type.kind === "call" && type.name === "enum")
335
+ enums.set(parameter.key, type.args.map(text));
336
+ }
337
+ const approvers = new Set(inheritedApprovers);
338
+ const supplied = entries(arguments_);
339
+ const environment = new Map<string, Expr>(inherited);
340
+ for (const param of decl.parameters) {
341
+ const type =
342
+ param.value.kind === "default" ? param.value.type : param.value;
343
+ const fallback =
344
+ param.value.kind === "default" ? param.value.value : undefined;
345
+ const actual = supplied.get(param.key) ?? fallback;
346
+ if (!actual) {
347
+ if (type.kind === "type" && type.optional) continue;
348
+ fail(
349
+ { span: origin },
350
+ `${id} needs ${param.key}`,
351
+ `add ${param.key}: value inside ${id}`,
352
+ );
353
+ }
354
+ environment.set(param.key, actual);
355
+ }
356
+ for (const key of supplied.keys())
357
+ if (!decl.parameters.some((p) => p.key === key))
358
+ fail(
359
+ supplied.get(key)!,
360
+ `unknown tunable ${key}`,
361
+ `choose ${decl.parameters.map((p) => p.key).join(", ")}`,
362
+ );
363
+ const resolve = (expr: Expr, seen = new Set<string>()): Expr => {
364
+ if (
365
+ expr.kind === "call" &&
366
+ ["object", "all", "party"].includes(expr.name)
367
+ ) {
368
+ if (expr.args.length !== 1)
369
+ fail(
370
+ expr,
371
+ `${expr.name} needs one type`,
372
+ "supply one declared object or party kind",
373
+ );
374
+ const type = text(expr.args[0]!);
375
+ const matches =
376
+ expr.name === "party"
377
+ ? Object.entries(document.parties)
378
+ .filter(([, party]) => party.kind === type)
379
+ .map(([name]) => name)
380
+ : [...objects.values()]
381
+ .filter(
382
+ (object) =>
383
+ object.object === type ||
384
+ type.startsWith(`${object.object}.`),
385
+ )
386
+ .map(
387
+ (object) =>
388
+ object.name +
389
+ type.slice(object.object.length).replaceAll(".", "_"),
390
+ );
391
+ if (expr.name !== "all" && matches.length !== 1)
392
+ fail(
393
+ { span: origin },
394
+ `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`,
395
+ "declare the required object or supply this tunable explicitly",
396
+ );
397
+ const items: Expr[] = matches.map((value) => ({
398
+ kind: "name",
399
+ value,
400
+ span: origin,
401
+ }));
402
+ return expr.name === "all"
403
+ ? { kind: "list", items, span: origin }
404
+ : items[0]!;
405
+ }
406
+ if (expr.kind !== "name") return expr;
407
+ if (expr.value.startsWith("party.")) {
408
+ const binding = environment.get(expr.value.slice(6));
409
+ if (binding?.kind === "name" && document.parties[binding.value])
410
+ return { ...expr, value: `party.${binding.value}` };
411
+ }
412
+ const [root, ...tail] = expr.value.split(".");
413
+ const bound = environment.get(root!);
414
+ if (!bound || (bound.kind === "name" && bound.value === root))
415
+ return expr;
416
+ if (seen.has(root!))
417
+ return fail(
418
+ expr,
419
+ `cyclic tunable ${root}`,
420
+ "replace the cycle with a literal or declared reference",
421
+ );
422
+ let resolved =
423
+ supplied.has(root!) || inherited.has(root!) || enums.has(root!)
424
+ ? bound
425
+ : resolve(bound, new Set([...seen, root!]));
426
+ for (const key of tail) {
427
+ if (resolved.kind !== "block") return expr;
428
+ const child = entries(resolved).get(key);
429
+ if (!child)
430
+ return fail(
431
+ expr,
432
+ `missing tunable ${expr.value}`,
433
+ `declare ${key} in ${root}`,
434
+ );
435
+ resolved = resolve(child, new Set([...seen, root!]));
436
+ }
437
+ return resolved;
438
+ };
439
+ for (const param of decl.parameters) {
440
+ const actual = environment.get(param.key);
441
+ if (!actual) continue;
442
+ const t =
443
+ param.value.kind === "default" ? param.value.type : param.value;
444
+ const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
445
+ const v =
446
+ supplied.has(param.key) || type === "enum" ? actual : resolve(actual);
447
+ environment.set(param.key, v);
448
+ if (type === "enum" && t.kind === "call") {
449
+ if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
450
+ fail(
451
+ v,
452
+ `invalid ${param.key}`,
453
+ `choose ${t.args.map(text).join(", ")}`,
454
+ );
455
+ } else if (type === "list") {
456
+ if (v.kind !== "list")
457
+ fail(v, `${param.key} needs a list`, "write [value, value]");
458
+ } else if (type === "party" || type === "approval") {
459
+ if (v.kind !== "name" || !document.parties[v.value])
460
+ fail(
461
+ v,
462
+ `${param.key} needs a declared party`,
463
+ "declare a party and use its name here",
464
+ );
465
+ if (type === "approval") approvers.add(text(v));
466
+ } else if (type === "ref") {
467
+ const values = v.kind === "list" ? v.items : [v];
468
+ if (v.kind === "list" && (t.kind !== "type" || !t.many))
469
+ fail(
470
+ v,
471
+ `${param.key} accepts one reference`,
472
+ "use one object name",
473
+ );
474
+ if (!values.length || values.length > 16)
475
+ fail(
476
+ v,
477
+ "reference union needs 1 to 16 objects",
478
+ "use at most 16 distinct object names",
479
+ );
480
+ const seen = new Set<string>();
481
+ for (const value of values) {
482
+ if (value.kind !== "name")
483
+ fail(
484
+ value,
485
+ "reference needs an object name",
486
+ "name a declared object",
487
+ );
488
+ const [root, ...tail] = value.value.split(".");
489
+ const obj = objects.get(root!);
490
+ if (
491
+ (!obj &&
492
+ !document.instruments.some(
493
+ (inst) => inst.id === value.value,
494
+ )) ||
495
+ (t.kind === "type" &&
496
+ t.target &&
497
+ [obj?.object, ...tail].join(".") !== t.target)
498
+ )
499
+ fail(
500
+ value,
501
+ `${param.key} has the wrong object type`,
502
+ `use an object of type ${t.kind === "type" ? t.target : "ref"}`,
503
+ );
504
+ if (seen.has(value.value))
505
+ fail(value, "duplicate reference", "list each object once");
506
+ seen.add(value.value);
507
+ }
508
+ } else if (type === "fee" || type === "split" || type === "policy") {
509
+ if (v.kind !== "block")
510
+ fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
511
+ } else if (
512
+ type === "money" ||
513
+ type === "percent" ||
514
+ type === "date" ||
515
+ type === "duration" ||
516
+ type === "integer" ||
517
+ type === "text"
518
+ ) {
519
+ if (v.kind === "name" && v.value === "runtime") continue;
520
+ const expected = type === "integer" ? "number" : type;
521
+ if (
522
+ v.kind !== expected &&
523
+ !(
524
+ type === "duration" &&
525
+ (v.kind === "text" || v.kind === "name") &&
526
+ /^P/.test(v.value)
527
+ )
528
+ )
529
+ fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
530
+ try {
531
+ const value = literal(v);
532
+ const bounds = tunableBounds(t);
533
+ if (
534
+ bounds &&
535
+ (BigInt(String(value)) < BigInt(bounds.minimum) ||
536
+ BigInt(String(value)) > BigInt(bounds.maximum))
537
+ )
538
+ fail(
539
+ v,
540
+ `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`,
541
+ "choose a value inside the tunable's bounds",
542
+ );
543
+ } catch (error) {
544
+ if (error instanceof CompileFailure)
545
+ fail(
546
+ v,
547
+ `${param.key}: ${error.diagnostic.message}`,
548
+ error.diagnostic.fix,
549
+ );
550
+ throw error;
551
+ }
552
+ }
553
+ }
554
+ const body = entries(decl.body);
555
+ for (const constraint of asBlock(body.get("constraints")).entries) {
556
+ const rule = constraint.value;
557
+ if (
558
+ rule.kind !== "call" ||
559
+ rule.args.length !== 1 ||
560
+ !["less_than", "at_most", "greater_than"].includes(rule.name)
561
+ )
562
+ fail(
563
+ rule,
564
+ "invalid tunable constraint",
565
+ "use less_than(name), at_most(name), or greater_than(name)",
566
+ );
567
+ const left = environment.get(constraint.key),
568
+ right = environment.get(text(rule.args[0]!));
569
+ if (!left || !right)
570
+ fail(
571
+ rule,
572
+ "constraint needs two declared tunables",
573
+ "name the compared tunables",
574
+ );
575
+ const a = BigInt(String(literal(left))),
576
+ b = BigInt(String(literal(right)));
577
+ if (
578
+ !(rule.name === "less_than"
579
+ ? a < b
580
+ : rule.name === "at_most"
581
+ ? a <= b
582
+ : a > b)
583
+ )
584
+ fail(
585
+ left,
586
+ `${constraint.key} must be ${rule.name} ${text(rule.args[0]!)}`,
587
+ "choose values satisfying the header constraint",
588
+ );
589
+ }
590
+ for (const key of body.keys())
591
+ if (
592
+ ![
593
+ "fields",
594
+ "lifecycle",
595
+ "records",
596
+ "summary",
597
+ "invariants",
598
+ "constraints",
599
+ ].includes(key) &&
600
+ !key.startsWith("action ")
601
+ )
602
+ fail(
603
+ decl,
604
+ `unknown instrument clause ${key}`,
605
+ "use fields, lifecycle, actions, invariants, or records",
606
+ );
607
+ const records = entries(asBlock(body.get("records")));
608
+ if (!inherited.size)
609
+ environment.set("parent", { kind: "name", value: id, span: origin });
610
+ for (const key of records.keys())
611
+ if (!decl.parameters.some((parameter) => parameter.key === key))
612
+ environment.set(key, {
613
+ kind: "name",
614
+ value: `${id}_${key}`,
615
+ span: origin,
616
+ });
617
+ const fields: UdlField[] = [];
618
+ const calculations: UdlCalculation[] = [];
619
+ const path = (expr: Expr): string => {
620
+ const value = resolve(expr);
621
+ const name = text(value);
622
+ if (document.parties[name]) return `party.${name}`;
623
+ return /^(self|input|party)\./.test(name) ? name : `self.${name}`;
624
+ };
625
+ const val = (expr: Expr): UdlValue => {
626
+ const v = resolve(expr);
627
+ return v.kind === "name" && !["true", "false"].includes(v.value)
628
+ ? { field: path(v) }
629
+ : { literal: literal(v) };
630
+ };
631
+ const data = (expr: Expr): unknown => {
632
+ if (
633
+ expr.kind === "call" &&
634
+ expr.name === "current" &&
635
+ !expr.args.length
636
+ )
637
+ return id;
638
+ const value = resolve(expr);
639
+ if (value.kind === "block") {
640
+ const result = Object.fromEntries(
641
+ [...entries(value)].map(([key, value]) => [key, data(value)]),
642
+ );
643
+ const selection = result.selection as
644
+ | { instrument?: unknown }
645
+ | undefined;
646
+ if (
647
+ Array.isArray(selection?.instrument) &&
648
+ selection.instrument.length === 0
649
+ ) {
650
+ if (result.op === "aggregate")
651
+ return {
652
+ target: result.target,
653
+ op: "sum",
654
+ values: [{ literal: result.measure === "count" ? 0 : "0" }],
655
+ };
656
+ if (result.kind === "aggregate")
657
+ return {
658
+ kind: "compare",
659
+ left: { literal: result.measure === "count" ? 0 : "0" },
660
+ operator: result.operator,
661
+ right: result.value,
662
+ };
663
+ return null;
664
+ }
665
+ return result;
666
+ }
667
+ if (value.kind === "list")
668
+ return value.items.map(data).filter((item) => item !== null);
669
+ if (
670
+ value.kind === "call" &&
671
+ value.name === "child" &&
672
+ value.args.length === 2
673
+ ) {
674
+ const parent = resolve(value.args[0]!);
675
+ const child = (item: Expr) =>
676
+ `${text(item).replaceAll(".", "_")}_${text(value.args[1]!)}`;
677
+ return parent.kind === "list"
678
+ ? parent.items.map(child)
679
+ : child(parent);
680
+ }
681
+ if (value.kind === "name")
682
+ return value.value === "true" || value.value === "false"
683
+ ? value.value === "true"
684
+ : value.value;
685
+ return literal(value);
686
+ };
687
+ const lowerFields = (block: BlockExpr): UdlField[] => {
688
+ const result: UdlField[] = [];
689
+ for (const row of block.entries) {
690
+ const t = row.value.kind === "default" ? row.value.type : row.value;
691
+ const constant =
692
+ row.value.kind === "default" ? resolve(row.value.value) : undefined;
693
+ const type =
694
+ t.kind === "type" || t.kind === "call" ? t.name : text(t);
695
+ const f: Record<string, unknown> = {
696
+ name: row.key,
697
+ type,
698
+ ...(t.kind === "type" && t.optional ? { optional: true } : {}),
699
+ };
700
+ if (type === "enum" && t.kind === "call") f.values = t.args.map(text);
701
+ if (["integer", "money"].includes(type) && t.kind === "call") {
702
+ if (t.args.length !== 2)
703
+ fail(
704
+ t,
705
+ "bounded fields need a minimum and maximum",
706
+ "write integer(1, 12) or money(0 SAR, 100 SAR)",
707
+ );
708
+ f.minimum = literal(resolve(t.args[0]!));
709
+ f.maximum = literal(resolve(t.args[1]!));
710
+ }
711
+ if (type === "list" && t.kind === "call") {
712
+ f.item = text(t.args[0]!);
713
+ f.maxItems = t.args[1] ? literal(resolve(t.args[1])) : 366;
714
+ }
715
+ if (type === "list" && t.kind === "type") {
716
+ f.item = t.target;
717
+ f.maxItems = 366;
718
+ }
719
+ if (type === "ref") {
720
+ const target = t.kind === "type" ? t.target : undefined;
721
+ if (!target)
722
+ fail(row, "reference needs a target", "write ref<object>");
723
+ const [root, ...tail] = target.split(".");
724
+ const resolved = environment.get(root!);
725
+ const resolvedTargets =
726
+ resolved?.kind === "list"
727
+ ? resolved.items
728
+ : resolved
729
+ ? [resolved]
730
+ : [];
731
+ const targets = resolvedTargets.map((value) =>
732
+ [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"),
733
+ );
734
+ f.target =
735
+ target === "self"
736
+ ? id
737
+ : targets.length === 1
738
+ ? targets[0]
739
+ : targets.length
740
+ ? targets
741
+ : target;
742
+ } else if (type === "account") {
743
+ if (t.kind === "call") {
744
+ if (t.args.length < 1 || t.args.length > 4)
745
+ fail(
746
+ t,
747
+ "account needs an owner, optional book, mode and key",
748
+ "write account(buyer, claim, contra)",
749
+ );
750
+ f.owner = text(resolve(t.args[0]!));
751
+ f.book = t.args[1] ? text(t.args[1]) : "cash";
752
+ if (t.args[2]) {
753
+ const mode = text(t.args[2]);
754
+ if (["contra", "external"].includes(mode)) f[mode] = true;
755
+ else f.key = mode;
756
+ }
757
+ if (t.args[3]) f.key = text(t.args[3]);
758
+ } else if (t.kind === "type" && t.owner) {
759
+ f.owner =
760
+ t.owner === "self"
761
+ ? "self"
762
+ : text(
763
+ resolve({ kind: "name", value: t.owner, span: row.span }),
764
+ );
765
+ f.book = "cash";
766
+ } else
767
+ fail(
768
+ row,
769
+ "account needs an owner",
770
+ "write account of buyer or account of self",
771
+ );
772
+ }
773
+ if (
774
+ constant &&
775
+ !(constant.kind === "name" && constant.value === "runtime")
776
+ ) {
777
+ if (constant.kind === "call") {
778
+ const args =
779
+ constant.name === "aggregate" ? [] : constant.args.map(val);
780
+ const target = row.key;
781
+ if (constant.name === "aggregate" && constant.args.length === 2)
782
+ calculations.push({
783
+ target,
784
+ op: "aggregate",
785
+ selection: data(
786
+ constant.args[0]!,
787
+ ) as import("@hyperscale0/udl").UdlSelection,
788
+ measure:
789
+ text(constant.args[1]!) === "count"
790
+ ? "count"
791
+ : { sum: text(constant.args[1]!) },
792
+ });
793
+ else if (constant.name === "at" && args.length === 2)
794
+ calculations.push({
795
+ target,
796
+ op: "at",
797
+ list: path(constant.args[0]!),
798
+ position: args[1]!,
799
+ });
800
+ else if (constant.name === "ratio" && args.length === 3)
801
+ calculations.push({
802
+ target,
803
+ op: "ratio",
804
+ amount: args[0]!,
805
+ numerator: args[1]!,
806
+ denominator: args[2]!,
807
+ rounding: "floor",
808
+ });
809
+ else if (constant.name === "rate" && args.length === 2)
810
+ calculations.push({
811
+ target,
812
+ op: "rate",
813
+ base: args[0]!,
814
+ bps: args[1]!,
815
+ rounding: "floor",
816
+ });
817
+ else if (constant.name === "sum" || constant.name === "minimum")
818
+ calculations.push({ target, op: constant.name, values: args });
819
+ else if (constant.name === "subtract" && args.length >= 2)
820
+ calculations.push({
821
+ target,
822
+ op: "subtract",
823
+ base: args[0]!,
824
+ subtract: args.slice(1),
825
+ });
826
+ else if (constant.name === "multiply" && args.length === 2)
827
+ calculations.push({
828
+ target,
829
+ op: "multiply",
830
+ amount: args[0]!,
831
+ units: args[1]!,
832
+ });
833
+ else if (constant.name === "divide" && args.length === 2)
834
+ calculations.push({
835
+ target,
836
+ op: "divide",
837
+ amount: args[0]!,
838
+ divisor: args[1]!,
839
+ rounding: "floor",
840
+ });
841
+ else if (
842
+ ["before", "after"].includes(constant.name) &&
843
+ args.length === 2
844
+ )
845
+ calculations.push({
846
+ target,
847
+ op: "shift",
848
+ date: args[0]!,
849
+ milliseconds: args[1]!,
850
+ direction: constant.name as "before" | "after",
851
+ });
852
+ else
853
+ fail(
854
+ constant,
855
+ `unknown calculation ${constant.name}`,
856
+ "use rate, sum, minimum, subtract, multiply, divide, before or after",
857
+ );
858
+ } else if (type === "enum" && constant.kind === "name")
859
+ f.value = constant.value;
860
+ else if (type === "money" && constant.kind === "name")
861
+ calculations.push({
862
+ target: row.key,
863
+ op: "sum",
864
+ values: [val(constant)],
865
+ });
866
+ else f.value = literal(constant);
867
+ }
868
+ result.push(f as UdlField);
869
+ }
870
+ return result;
871
+ };
872
+ fields.push(...lowerFields(asBlock(body.get("fields"))));
873
+ const lifecycle = data(
874
+ body.get("lifecycle") ?? emptyBlock,
875
+ ) as UdlInstrument["lifecycle"];
876
+ lifecycle.transitions = {};
877
+ const inst: UdlInstrument = {
878
+ id,
879
+ title: title(id),
880
+ summary: body.has("summary")
881
+ ? String(data(body.get("summary")!))
882
+ : title(id),
883
+ fields,
884
+ calculate: calculations,
885
+ lifecycle,
886
+ actions: {},
887
+ actionOrder: [],
888
+ };
889
+ for (const row of decl.body.entries.filter((e) =>
890
+ e.key.startsWith("action "),
891
+ )) {
892
+ const name = row.key.slice(7);
893
+ const selected = (block: BlockExpr): BlockExpr => ({
894
+ ...block,
895
+ entries: block.entries.flatMap((entry) => {
896
+ if (!entry.key.startsWith("when ")) return [entry];
897
+ const [, tunable, , choice] = entry.key.split(" ");
898
+ const binding = environment.get(tunable!);
899
+ if (!binding)
900
+ fail(
901
+ entry,
902
+ `unknown branch tunable ${tunable}`,
903
+ "name an enum tunable declared by this header",
904
+ );
905
+ if (!enums.get(tunable!)?.includes(choice!))
906
+ fail(
907
+ entry,
908
+ `unknown enum branch ${choice}`,
909
+ "use a declared enum value",
910
+ );
911
+ const body = asBlock(entry.value);
912
+ for (const clause of body.entries)
913
+ if (
914
+ !["requires", "moves", "invoke", "calculate"].includes(
915
+ clause.key,
916
+ ) &&
917
+ !clause.key.startsWith("when ")
918
+ )
919
+ fail(
920
+ clause,
921
+ "when permits requirements, calculations, moves and invocations",
922
+ "keep lifecycle and actor clauses outside the branch",
923
+ );
924
+ return text(binding) === choice ? selected(body).entries : [];
128
925
  }),
129
- line: position.line,
130
- message,
131
- span: { ...span },
132
- ...(details ? details : {}),
133
- severity,
134
- stage,
135
- });
136
- };
137
-
138
- const optionKeys = new Set([
139
- "costTable",
140
- "moduleName",
141
- "publishedCatalog",
142
- "resolveModule",
143
- "standardLibrary",
144
- ]);
145
- for (const key of Object.keys(options)) {
146
- if (optionKeys.has(key)) continue;
147
- at("bind", "error", `unknown compile option ${key}`, { start: 0, end: 0 });
148
- }
149
- if (diagnostics.length > 0) return { diagnostics, verdict: "invalid" };
150
-
151
- const parsed = parseProgram(source);
152
- for (const diagnostic of parsed.diagnostics) {
153
- at(
154
- "parse",
155
- "error",
156
- diagnostic.message,
157
- diagnostic.span,
158
- diagnostic.code && diagnostic.fix
159
- ? { code: diagnostic.code, fix: diagnostic.fix }
160
- : undefined,
161
- );
162
- }
163
- if (parsed.diagnostics.length > 0) {
164
- return { diagnostics, verdict: "invalid" };
165
- }
166
-
167
- const resolved = resolveProgramModules(parsed.program, options);
168
- if (!resolved.ok) {
169
- for (const issue of resolved.issues) {
170
- at("bind", "error", issue.message, issue.span, {
171
- code: issue.code,
172
- fix: issue.fix,
926
+ });
927
+ const slots = entries(selected(asBlock(row.value)));
928
+ const a: UdlAction = {
929
+ summary: slots.has("summary")
930
+ ? String(data(slots.get("summary")!))
931
+ : `${title(name)} ${id}`,
932
+ publicAction: camel(`${name}_${id}`),
933
+ event: `${id}.${name}`,
934
+ actor: "caller",
935
+ input: lowerFields(asBlock(slots.get("input"))),
936
+ requires: [],
937
+ moves: [],
938
+ };
939
+ if (name !== "create") {
940
+ const from = slots.get("from");
941
+ const to = slots.get("to");
942
+ if (!from || !to)
943
+ fail(
944
+ row,
945
+ `action ${name} needs from and to`,
946
+ "declare its source and destination states",
947
+ );
948
+ lifecycle.transitions[name] = {
949
+ from: (from!.kind === "list" ? from!.items : [from!]).map((e) =>
950
+ String(data(e)),
951
+ ),
952
+ to: String(data(to!)),
953
+ };
954
+ }
955
+ for (const [key, expr] of slots) {
956
+ if (["from", "to", "input", "summary"].includes(key)) continue;
957
+ if (key === "moves") {
958
+ const moves = expr.kind === "list" ? expr.items : [expr];
959
+ for (const [index, move] of moves.entries()) {
960
+ const parts = entries(asBlock(move));
961
+ const op = parts.has("operation")
962
+ ? String(data(parts.get("operation")!))
963
+ : "internal_transfer.create";
964
+ if (op === "internal_transfer.reserve" && !parts.has("capture"))
965
+ fail(
966
+ move,
967
+ "reserve needs a receipt field",
968
+ "add capture: receipt_name",
969
+ );
970
+ if (
971
+ (op === "internal_transfer.post" ||
972
+ op === "internal_transfer.void") &&
973
+ !parts.has("transfer")
974
+ )
975
+ fail(
976
+ move,
977
+ "post and void need a receipt",
978
+ "add transfer: receipt_name",
979
+ );
980
+ const key = parts.has("key")
981
+ ? String(data(parts.get("key")!))
982
+ : `move${index + 1}`;
983
+ if (
984
+ op === "internal_transfer.create" ||
985
+ op === "internal_transfer.reserve"
986
+ ) {
987
+ const amount = parts.get("amount"),
988
+ from = parts.get("from"),
989
+ to = parts.get("to");
990
+ if (parts.has("shares")) {
991
+ if (
992
+ !amount ||
993
+ !from ||
994
+ op !== "internal_transfer.create" ||
995
+ parts.has("fee")
996
+ )
997
+ fail(
998
+ move,
999
+ "split needs a posted amount and source",
1000
+ "declare amount, from and shares without a fee",
1001
+ );
1002
+ const shares = [
1003
+ ...entries(asBlock(resolve(parts.get("shares")!))),
1004
+ ];
1005
+ if (!shares.length || shares.length > 64)
1006
+ fail(
1007
+ move,
1008
+ "split needs 1 to 64 recipients",
1009
+ "name the receiving parties and percentages",
1010
+ );
1011
+ let total = 0;
1012
+ const pieces: UdlValue[] = [];
1013
+ for (const [index, [party, rate]] of shares.entries()) {
1014
+ const recipient = resolve({
1015
+ kind: "name",
1016
+ value: party,
1017
+ span: rate.span,
1018
+ });
1019
+ if (
1020
+ recipient.kind !== "name" ||
1021
+ !document.parties[recipient.value] ||
1022
+ rate.kind !== "percent"
1023
+ )
1024
+ fail(
1025
+ rate,
1026
+ "split needs party percentages",
1027
+ "write party_name: 70%",
1028
+ );
1029
+ total += Number(literal(rate));
1030
+ const target = `${name}_${key}_share${index + 1}`;
1031
+ if (fields.some((f) => f.name === target))
1032
+ fail(
1033
+ move,
1034
+ "split field name conflicts",
1035
+ "rename the move key",
1036
+ );
1037
+ fields.push({ name: target, type: "money" });
1038
+ calculations.push(
1039
+ index === shares.length - 1 && pieces.length
1040
+ ? {
1041
+ target,
1042
+ op: "subtract",
1043
+ base: val(amount),
1044
+ subtract: [...pieces],
1045
+ }
1046
+ : {
1047
+ target,
1048
+ op: "rate",
1049
+ base: val(amount),
1050
+ bps: { literal: literal(rate) },
1051
+ rounding: "floor",
1052
+ },
1053
+ );
1054
+ const piece: UdlValue = { field: `self.${target}` };
1055
+ pieces.push(piece);
1056
+ a.moves.push({
1057
+ key: `${key}_${index + 1}`,
1058
+ operation: "internal_transfer.create",
1059
+ amount: piece,
1060
+ from: path(from),
1061
+ to: `party.${recipient.value}`,
1062
+ });
1063
+ }
1064
+ if (total !== 10000)
1065
+ fail(
1066
+ move,
1067
+ "split percentages must sum to 100%",
1068
+ "give the recipients exactly 100% together",
1069
+ );
1070
+ continue;
1071
+ }
1072
+ if (!amount || !from || !to)
1073
+ fail(
1074
+ move,
1075
+ "move needs amount, from and to",
1076
+ "declare all three move operands",
1077
+ );
1078
+ const transfer = {
1079
+ key,
1080
+ operation: op,
1081
+ ...(parts.has("capture")
1082
+ ? { capture: String(data(parts.get("capture")!)) }
1083
+ : {}),
1084
+ amount: val(amount!),
1085
+ from: path(from!),
1086
+ to: path(to!),
1087
+ };
1088
+ const fee = parts.get("fee");
1089
+ if (!fee)
1090
+ a.moves.push(
1091
+ op === "internal_transfer.reserve"
1092
+ ? {
1093
+ ...transfer,
1094
+ operation: op,
1095
+ capture: String(data(parts.get("capture")!)),
1096
+ }
1097
+ : { ...transfer, operation: op },
1098
+ );
1099
+ else {
1100
+ if (op !== "internal_transfer.create")
1101
+ fail(
1102
+ move,
1103
+ "fees settle with a posted transfer",
1104
+ "declare a separate posted settlement after releasing the reservation",
1105
+ );
1106
+ const terms = entries(asBlock(resolve(fee)));
1107
+ for (const term of terms.keys())
1108
+ if (!["seller", "buyer", "tax"].includes(term))
1109
+ fail(
1110
+ fee,
1111
+ `unknown fee term ${term}`,
1112
+ "choose seller or buyer, and optional tax",
1113
+ );
1114
+ const paidBy = terms.has("seller") ? "seller" : "buyer";
1115
+ if (terms.has("seller") === terms.has("buyer"))
1116
+ fail(
1117
+ fee,
1118
+ "fee needs exactly one paying party",
1119
+ "write seller: 1% or buyer: 1%",
1120
+ );
1121
+ const quoted = terms.get(paidBy)!;
1122
+ const rate = quoted.kind === "capped" ? quoted.rate : quoted;
1123
+ if (rate.kind !== "percent")
1124
+ fail(
1125
+ rate,
1126
+ "fee rate needs a percentage",
1127
+ "write a percentage such as 1%",
1128
+ );
1129
+ const prefix = `${name}_${key}`;
1130
+ const derived = (
1131
+ suffix: string,
1132
+ calculation: Omit<UdlCalculation, "target">,
1133
+ ): UdlValue => {
1134
+ const target = prefix + "_" + suffix;
1135
+ if (fields.some((f) => f.name === target))
1136
+ fail(
1137
+ move,
1138
+ `generated fee field ${target} conflicts with a field`,
1139
+ "rename the authored field or move key",
1140
+ );
1141
+ fields.push({ name: target, type: "money" });
1142
+ calculations.push({
1143
+ ...calculation,
1144
+ target,
1145
+ } as UdlCalculation);
1146
+ return { field: `self.${target}` };
1147
+ };
1148
+ const gross = derived("feeGross", {
1149
+ op: "rate",
1150
+ base: transfer.amount,
1151
+ bps: { literal: literal(rate) },
1152
+ rounding: "floor",
1153
+ } as Omit<UdlCalculation, "target">);
1154
+ let charge = gross;
1155
+ if (quoted.kind === "capped") {
1156
+ if (quoted.cap.kind !== "money")
1157
+ fail(
1158
+ quoted.cap,
1159
+ "fee cap needs money",
1160
+ "write an amount such as 750 SAR",
1161
+ );
1162
+ charge = derived("fee", {
1163
+ op: "minimum",
1164
+ values: [gross, { literal: literal(quoted.cap) }],
1165
+ } as Omit<UdlCalculation, "target">);
1166
+ }
1167
+ const tax = terms.get("tax");
1168
+ if (tax && tax.kind !== "percent")
1169
+ fail(
1170
+ tax,
1171
+ "tax needs a percentage of the fee",
1172
+ "write tax: 15%",
1173
+ );
1174
+ const vat = tax
1175
+ ? derived("tax", {
1176
+ op: "rate",
1177
+ base: charge,
1178
+ bps: { literal: literal(tax) },
1179
+ rounding: "floor",
1180
+ } as Omit<UdlCalculation, "target">)
1181
+ : undefined;
1182
+ const charges = vat ? [charge, vat] : [charge];
1183
+ const net =
1184
+ paidBy === "seller"
1185
+ ? derived("net", {
1186
+ op: "subtract",
1187
+ base: transfer.amount,
1188
+ subtract: charges,
1189
+ } as Omit<UdlCalculation, "target">)
1190
+ : transfer.amount;
1191
+ for (const [party, role] of [
1192
+ ["programOperator", "program_operator"],
1193
+ ["programTax", "tax_payable"],
1194
+ ] as const) {
1195
+ if (
1196
+ document.parties[party] &&
1197
+ document.parties[party]!.role !== role
1198
+ )
1199
+ fail(
1200
+ move,
1201
+ `${party} is reserved for fees`,
1202
+ "rename this party",
1203
+ );
1204
+ document.parties[party] = { kind: "business", role };
1205
+ }
1206
+ a.moves.push({
1207
+ ...transfer,
1208
+ operation: "internal_transfer.create",
1209
+ amount: net,
1210
+ });
1211
+ a.moves.push({
1212
+ key: key + "Fee",
1213
+ operation: "internal_transfer.create",
1214
+ amount: charge,
1215
+ from: transfer.from,
1216
+ to: "party.programOperator",
1217
+ });
1218
+ if (vat)
1219
+ a.moves.push({
1220
+ key: key + "Tax",
1221
+ operation: "internal_transfer.create",
1222
+ amount: vat,
1223
+ from: transfer.from,
1224
+ to: "party.programTax",
1225
+ });
1226
+ }
1227
+ } else if (
1228
+ op === "internal_transfer.post" ||
1229
+ op === "internal_transfer.void"
1230
+ )
1231
+ a.moves.push({
1232
+ key,
1233
+ operation: op,
1234
+ transfer: path(parts.get("transfer")!),
1235
+ ...(parts.has("capture")
1236
+ ? { capture: String(data(parts.get("capture")!)) }
1237
+ : {}),
1238
+ });
1239
+ else
1240
+ fail(
1241
+ move,
1242
+ `unknown transfer instruction ${op}`,
1243
+ "choose create, reserve, post, or void from internal_transfer",
1244
+ );
1245
+ }
1246
+ } else if (key === "approval") {
1247
+ const approval = data(expr) as UdlAction["approval"];
1248
+ if (approval && (approval.input as unknown) === "material") {
1249
+ approval.input = {};
1250
+ materialApprovals.add(a);
1251
+ }
1252
+ a.approval = approval;
1253
+ } else (a as unknown as Record<string, unknown>)[key] = data(expr);
1254
+ }
1255
+ if (automatic(a.actor)) delete a.publicAction;
1256
+ for (const requirement of a.requires ?? []) {
1257
+ if (
1258
+ requirement.kind !== "approval" ||
1259
+ !approvers.has(requirement.party)
1260
+ )
1261
+ continue;
1262
+ const action = requirement.action ?? name;
1263
+ const key = `${id}_${action}_decision`;
1264
+ const previous = implicitDecisions.get(key);
1265
+ if (previous && previous.party !== requirement.party)
1266
+ fail(
1267
+ { span: origin },
1268
+ `action ${action} has multiple approval parties`,
1269
+ "use a separate decision action for each party",
1270
+ );
1271
+ implicitDecisions.set(key, {
1272
+ target: id,
1273
+ action,
1274
+ party: requirement.party,
1275
+ origin,
1276
+ });
1277
+ }
1278
+ inst.actions[name] = a;
1279
+ inst.actionOrder.push(name);
1280
+ }
1281
+ for (const key of ["invariants"] as const)
1282
+ if (body.has(key))
1283
+ (inst as unknown as Record<string, unknown>)[key] = data(
1284
+ body.get(key)!,
1285
+ );
1286
+ origins.push({
1287
+ path: `$.instruments[${document.instruments.length}]`,
1288
+ span: { ...origin, ...lineColAt(source, origin.start) },
173
1289
  });
1290
+ document.instruments.push(inst);
1291
+ for (const [key, definition] of records) {
1292
+ const child: InstrumentDecl = {
1293
+ kind: "instrument",
1294
+ name: key,
1295
+ parameters: [],
1296
+ body: asBlock(definition),
1297
+ span: origin,
1298
+ };
1299
+ addInstrument(
1300
+ child,
1301
+ `${id}_${key}`,
1302
+ emptyBlock,
1303
+ origin,
1304
+ new Map([
1305
+ ...environment,
1306
+ ["parent", { kind: "name", value: id, span: origin } as Expr],
1307
+ ]),
1308
+ approvers,
1309
+ enums,
1310
+ );
1311
+ }
1312
+ };
1313
+ for (const decl of program.decls) {
1314
+ if (decl.kind === "instrument") {
1315
+ if (decl.parameters.length)
1316
+ fail(
1317
+ decl,
1318
+ "program records cannot declare tunables",
1319
+ "put reusable instruments in a header",
1320
+ );
1321
+ addInstrument(decl, decl.name, emptyBlock, decl.span);
1322
+ }
1323
+ if (decl.kind === "object") {
1324
+ const template = templates.get(decl.object);
1325
+ if (!template)
1326
+ fail(
1327
+ decl,
1328
+ `unknown object ${decl.object}`,
1329
+ `add use ${decl.object.split(".")[0]} and choose a declared object`,
1330
+ );
1331
+ addInstrument(template, decl.name, decl.body, decl.span);
1332
+ }
174
1333
  }
175
- return { diagnostics, verdict: "invalid" };
176
- }
177
- const checked = checkGeneralProgram(
178
- resolved.program,
179
- options.publishedCatalog
180
- ? { publishedCatalog: options.publishedCatalog }
181
- : {},
182
- );
183
- for (const diagnostic of checked.diagnostics) {
184
- const moduleOrigin = resolved.origins.get(diagnostic.span);
185
- at(
186
- diagnostic.code === "HSX1001" ? "bind" : "typecheck",
187
- diagnostic.severity,
188
- diagnostic.message,
189
- diagnostic.span,
190
- {
191
- code: diagnostic.code,
192
- fix: diagnostic.fix,
193
- ...(diagnostic.path ? { path: diagnostic.path } : {}),
194
- ...(diagnostic.udlCode ? { udlCode: diagnostic.udlCode } : {}),
195
- },
196
- moduleOrigin,
197
- );
198
- }
199
- if (!checked.program) return { diagnostics, verdict: "invalid" };
200
- const cost = buildCostManifest(checked.program, options.costTable);
201
- if (!cost.ok) {
202
- for (const diagnostic of cost.diagnostics) {
203
- at(
204
- "typecheck",
205
- diagnostic.severity,
206
- diagnostic.message,
207
- diagnostic.span,
208
- {
209
- code: diagnostic.code,
210
- fix: diagnostic.fix,
211
- ...(diagnostic.path ? { path: diagnostic.path } : {}),
212
- ...(diagnostic.udlCode ? { udlCode: diagnostic.udlCode } : {}),
213
- },
1334
+ const eliminatedStates = new Map<string, Set<string>>();
1335
+ // Remove branches excluded by immutable tunables before checking reference states.
1336
+ for (const inst of document.instruments) {
1337
+ for (const [key, action] of Object.entries(inst.actions)) {
1338
+ const constant = (value: import("@hyperscale0/udl").UdlValue) => {
1339
+ if ("literal" in value) return value.literal;
1340
+ const f = inst.fields.find((f) => `self.${f.name}` === value.field);
1341
+ return f && "value" in f ? f.value : undefined;
1342
+ };
1343
+ if (
1344
+ action.requires.some((r) => {
1345
+ if (r.kind !== "compare") return false;
1346
+ const left = constant(r.left),
1347
+ right = constant(r.right);
1348
+ if (left === undefined || right === undefined) return false;
1349
+ return r.operator === "=="
1350
+ ? left !== right
1351
+ : r.operator === "!="
1352
+ ? left === right
1353
+ : false;
1354
+ })
1355
+ ) {
1356
+ delete inst.actions[key];
1357
+ delete inst.lifecycle.transitions[key];
1358
+ }
1359
+ }
1360
+ const reachable = new Set([inst.lifecycle.initial]);
1361
+ for (let n = 0; n < inst.lifecycle.states.length; n++)
1362
+ for (const edge of Object.values(inst.lifecycle.transitions))
1363
+ if (edge.from.some((state) => reachable.has(state)))
1364
+ reachable.add(edge.to);
1365
+ for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
1366
+ edge.from = edge.from.filter((state) => reachable.has(state));
1367
+ if (!edge.from.length) {
1368
+ delete inst.actions[key];
1369
+ delete inst.lifecycle.transitions[key];
1370
+ }
1371
+ }
1372
+ eliminatedStates.set(
1373
+ inst.id,
1374
+ new Set(inst.lifecycle.states.filter((state) => !reachable.has(state))),
214
1375
  );
1376
+ inst.lifecycle.states = inst.lifecycle.states.filter((state) =>
1377
+ reachable.has(state),
1378
+ );
1379
+ inst.actionOrder = inst.actionOrder.filter((key) => inst.actions[key]);
215
1380
  }
216
- return { diagnostics, verdict: "invalid" };
217
- }
218
- const lowered = lowerGeneralProgram(checked.program);
219
- if (!lowered.ok) {
220
- for (const issue of lowered.issues) {
221
- at("lower", "error", issue.message, issue.span, {
222
- code: issue.code,
223
- fix: issue.fix,
224
- path: issue.path,
225
- ...(issue.udlCode ? { udlCode: issue.udlCode } : {}),
226
- });
1381
+ for (const inst of document.instruments)
1382
+ for (const action of Object.values(inst.actions)) {
1383
+ for (const requirement of action.requires) {
1384
+ if (requirement.kind !== "state") continue;
1385
+ const field = inst.fields.find(
1386
+ (f) => `self.${f.name}` === requirement.reference,
1387
+ );
1388
+ if (field?.type === "ref")
1389
+ requirement.states = requirement.states.filter(
1390
+ (state) =>
1391
+ !(
1392
+ typeof field.target === "string"
1393
+ ? [field.target]
1394
+ : field.target
1395
+ ).some((id) => eliminatedStates.get(id)?.has(state)),
1396
+ );
1397
+ }
1398
+ }
1399
+ // Approval tunables instantiate the same library record as an explicit decision.
1400
+ if (implicitDecisions.size) {
1401
+ const content = (
1402
+ options.standardLibrary ?? bundledStandardLibrary
1403
+ ).source("approvals");
1404
+ const parsed = content ? parseProgram(content) : undefined;
1405
+ const template = parsed?.program.decls.find(
1406
+ (decl) => decl.kind === "instrument" && decl.name === "decision",
1407
+ );
1408
+ if (
1409
+ !template ||
1410
+ template.kind !== "instrument" ||
1411
+ parsed?.diagnostics.length
1412
+ )
1413
+ fail(
1414
+ program,
1415
+ "implicit decisions need the approvals header",
1416
+ "provide the standard approvals header",
1417
+ );
1418
+ for (const [id, decision] of implicitDecisions) {
1419
+ if (
1420
+ !document.instruments.find((inst) => inst.id === decision.target)
1421
+ ?.actions[decision.action]
1422
+ )
1423
+ continue;
1424
+ const existing = document.instruments.some((inst) =>
1425
+ Object.values(inst.actions).some(
1426
+ (action) =>
1427
+ action.approval?.action === decision.action &&
1428
+ action.approval.party === decision.party &&
1429
+ inst.fields.some(
1430
+ (field) =>
1431
+ field.type === "ref" &&
1432
+ field.target === decision.target &&
1433
+ `self.${field.name}` === action.approval?.target,
1434
+ ),
1435
+ ),
1436
+ );
1437
+ if (existing) continue;
1438
+ if (document.instruments.some((inst) => inst.id === id))
1439
+ fail(
1440
+ { span: decision.origin },
1441
+ `implicit decision name ${id} is already used`,
1442
+ "rename the conflicting object",
1443
+ );
1444
+ addInstrument(
1445
+ template,
1446
+ id,
1447
+ {
1448
+ kind: "block",
1449
+ span: decision.origin,
1450
+ entries: Object.entries({
1451
+ for: decision.target,
1452
+ approved_by: decision.party,
1453
+ action: decision.action,
1454
+ }).map(([key, value]) => ({
1455
+ key,
1456
+ value: {
1457
+ kind: key === "action" ? "text" : "name",
1458
+ value,
1459
+ span: decision.origin,
1460
+ },
1461
+ span: decision.origin,
1462
+ })),
1463
+ },
1464
+ decision.origin,
1465
+ );
1466
+ }
227
1467
  }
228
- return { diagnostics, verdict: "invalid" };
229
- }
230
- const compileOrigin = (entry: OriginMapEntry): CompileOriginMapEntry => {
231
- const position = lineColIn(lines, entry.span.start);
1468
+ // Material approval fields are inferred from the selected action's typed input.
1469
+ for (const inst of document.instruments)
1470
+ for (const action of Object.values(inst.actions)) {
1471
+ if (!materialApprovals.has(action) || !action.approval) continue;
1472
+ const targetField = inst.fields.find(
1473
+ (f) => `self.${f.name}` === action.approval!.target,
1474
+ );
1475
+ if (targetField?.type !== "ref") continue;
1476
+ const target = document.instruments.find(
1477
+ (i) => i.id === targetField.target,
1478
+ )?.actions[action.approval.action];
1479
+ if (!target) continue;
1480
+ for (const field of target.input) {
1481
+ const name = `material_${field.name}`;
1482
+ if (!inst.fields.some((f) => f.name === name))
1483
+ inst.fields.push({ ...field, name });
1484
+ action.approval.input[field.name] = { field: `self.${name}` };
1485
+ }
1486
+ }
1487
+ const changed = new Set<string>();
1488
+ for (const decl of program.decls)
1489
+ if (decl.kind === "hide" || decl.kind === "expose") {
1490
+ const parts = decl.target.split(".");
1491
+ const key = parts.pop();
1492
+ const id = parts.join("_");
1493
+ const action = document.instruments.find((i) => i.id === id)?.actions[
1494
+ key ?? ""
1495
+ ];
1496
+ if (!action || changed.has(decl.target))
1497
+ fail(
1498
+ decl,
1499
+ `unknown or repeated action ${decl.target}`,
1500
+ "name one declared object.action once",
1501
+ );
1502
+ changed.add(decl.target);
1503
+ if (decl.kind === "hide") delete action!.publicAction;
1504
+ else if (automatic(action!.actor))
1505
+ fail(
1506
+ decl,
1507
+ `${decl.target} runs on the clock or its parent, not a caller`,
1508
+ "expose only caller actions",
1509
+ );
1510
+ else action!.publicAction = decl.name!;
1511
+ }
1512
+ const usedParties = new Set<string>();
1513
+ const collectParties = (value: unknown): void => {
1514
+ if (typeof value === "string") {
1515
+ usedParties.add(value);
1516
+ if (value.startsWith("party.")) usedParties.add(value.split(".")[1]!);
1517
+ } else if (Array.isArray(value)) value.forEach(collectParties);
1518
+ else if (value && typeof value === "object")
1519
+ Object.values(value).forEach(collectParties);
1520
+ };
1521
+ collectParties(document.instruments);
1522
+ for (const party of ["programFines", "programCosts"])
1523
+ if (!usedParties.has(party) && !names.has(party))
1524
+ delete document.parties[party];
1525
+ const validated = validateUdl(document);
1526
+ if (!validated.ok)
1527
+ return {
1528
+ verdict: "invalid",
1529
+ diagnostics: validated.issues.map((i) => {
1530
+ const origin = [...origins]
1531
+ .reverse()
1532
+ .find((o) => i.path.startsWith(o.path));
1533
+ return diagnostic(
1534
+ {
1535
+ code: "HSX1601",
1536
+ message: `${i.path}: ${i.message}`,
1537
+ fix: i.fix,
1538
+ span: origin?.span ?? program.span,
1539
+ },
1540
+ "lower",
1541
+ );
1542
+ }),
1543
+ };
232
1544
  return {
233
- path: entry.path,
234
- span: {
235
- column: position.column,
236
- end:
237
- byteOffsets[entry.span.end] ?? byteOffsets[byteOffsets.length - 1]!,
238
- line: position.line,
239
- start: byteOffsets[entry.span.start] ?? 0,
1545
+ verdict: "valid",
1546
+ diagnostics: [],
1547
+ artifacts: {
1548
+ document: validated.value,
1549
+ costManifest: buildUdlCostManifest(validated.value),
1550
+ originMap: origins,
240
1551
  },
241
1552
  };
242
- };
243
- return {
244
- artifacts: {
245
- document: lowered.value.document,
246
- ...(lowered.value.actionPlans
247
- ? { actionPlans: lowered.value.actionPlans }
248
- : {}),
249
- costManifest: cost.manifest,
250
- originMap: lowered.value.originMap.map(compileOrigin),
251
- },
252
- diagnostics,
253
- verdict: diagnostics.length > 0 ? "warning" : "valid",
254
- };
255
- }
256
-
257
- function utf8OffsetIndex(source: string): Uint32Array {
258
- const offsets = new Uint32Array(source.length + 1);
259
- let bytes = 0;
260
- for (let index = 0; index < source.length; index += 1) {
261
- offsets[index] = bytes;
262
- const codePoint = source.codePointAt(index) as number;
263
- if (codePoint > 0xffff) {
264
- offsets[index + 1] = bytes;
265
- bytes += 4;
266
- index += 1;
267
- } else if (codePoint > 0x7ff) bytes += 3;
268
- else if (codePoint > 0x7f) bytes += 2;
269
- else bytes += 1;
1553
+ } catch (error) {
1554
+ if (error instanceof CompileFailure)
1555
+ return {
1556
+ verdict: "invalid",
1557
+ diagnostics: [diagnostic(error.diagnostic, "check")],
1558
+ };
1559
+ throw error;
270
1560
  }
271
- offsets[source.length] = bytes;
272
- return offsets;
273
1561
  }