@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
@@ -1,163 +1,1128 @@
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
- import { lineColIn, lineIndex } from "./ast.js";
14
- import { buildCostManifest, } from "./cost.js";
15
- import { lowerGeneralProgram } from "./emit.js";
16
- import { resolveProgramModules, } from "./modules.js";
1
+ import { buildUdlCostManifest } from "./cost.js";
2
+ import { validateUdl, } from "@hyperscale0/udl";
3
+ import { tunableBounds } from "./tunables.js";
17
4
  import { parseProgram } from "./parse.js";
18
- import { checkGeneralProgram } from "./typecheck.js";
19
- export function compile(source, options = {}) {
20
- const diagnostics = [];
21
- // Scanned once here rather than per diagnostic: a source can carry one
22
- // diagnostic per two bytes, and rescanning from offset zero for each one
23
- // made a 128 KB file cost 2.4 seconds of coordinate arithmetic.
24
- const lines = lineIndex(source);
25
- const byteOffsets = utf8OffsetIndex(source);
26
- const at = (stage, severity, message, span, details, origin) => {
27
- const position = origin ?? lineColIn(lines, span.start);
28
- diagnostics.push({
29
- column: position.column,
30
- ...(origin
31
- ? { file: origin.moduleName }
32
- : {
33
- byteSpan: {
34
- end: byteOffsets[span.end] ?? byteOffsets[byteOffsets.length - 1],
35
- start: byteOffsets[span.start] ?? 0,
36
- },
37
- }),
38
- line: position.line,
39
- message,
40
- span: { ...span },
41
- ...(details ? details : {}),
42
- severity,
43
- stage,
44
- });
45
- };
46
- const optionKeys = new Set([
47
- "costTable",
48
- "moduleName",
49
- "publishedCatalog",
50
- "resolveModule",
51
- "standardLibrary",
52
- ]);
53
- for (const key of Object.keys(options)) {
54
- if (optionKeys.has(key))
55
- continue;
56
- at("bind", "error", `unknown compile option ${key}`, { start: 0, end: 0 });
57
- }
58
- if (diagnostics.length > 0)
59
- return { diagnostics, verdict: "invalid" };
60
- const parsed = parseProgram(source);
61
- for (const diagnostic of parsed.diagnostics) {
62
- at("parse", "error", diagnostic.message, diagnostic.span, diagnostic.code && diagnostic.fix
63
- ? { code: diagnostic.code, fix: diagnostic.fix }
64
- : undefined);
65
- }
66
- if (parsed.diagnostics.length > 0) {
67
- return { diagnostics, verdict: "invalid" };
5
+ import { lineColAt, } from "./ast.js";
6
+ import { bundledStandardLibrary } from "./std-library.js";
7
+ class CompileFailure extends Error {
8
+ diagnostic;
9
+ constructor(diagnostic) {
10
+ super(diagnostic.message);
11
+ this.diagnostic = diagnostic;
68
12
  }
69
- const resolved = resolveProgramModules(parsed.program, options);
70
- if (!resolved.ok) {
71
- for (const issue of resolved.issues) {
72
- at("bind", "error", issue.message, issue.span, {
73
- code: issue.code,
74
- fix: issue.fix,
13
+ }
14
+ function fail(expr, message, fix) {
15
+ throw new CompileFailure({ code: "HSX1001", message, fix, span: expr.span });
16
+ }
17
+ const emptyBlock = {
18
+ kind: "block",
19
+ entries: [],
20
+ span: { start: 0, end: 0 },
21
+ };
22
+ const camel = (name) => name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
23
+ /** Clock and parent actors run without a caller, so they have no public name. */
24
+ const automatic = (actor) => actor === "clock" || (typeof actor === "object" && "parent" in actor);
25
+ const title = (name) => name[0].toUpperCase() + name.slice(1).replaceAll("_", " ");
26
+ function entries(block) {
27
+ const map = new Map();
28
+ for (const row of block.entries) {
29
+ const previous = map.get(row.key);
30
+ if (previous) {
31
+ if (!["requires", "moves", "invariants", "invoke", "calculate"].includes(row.key))
32
+ fail(row, `duplicate ${row.key}`, "keep one value for this name");
33
+ const items = (expr) => expr.kind === "list" ? expr.items : [expr];
34
+ map.set(row.key, {
35
+ kind: "list",
36
+ items: [...items(previous), ...items(row.value)],
37
+ span: row.span,
75
38
  });
76
39
  }
77
- return { diagnostics, verdict: "invalid" };
40
+ else
41
+ map.set(row.key, row.value);
78
42
  }
79
- const checked = checkGeneralProgram(resolved.program, options.publishedCatalog
80
- ? { publishedCatalog: options.publishedCatalog }
81
- : {});
82
- for (const diagnostic of checked.diagnostics) {
83
- const moduleOrigin = resolved.origins.get(diagnostic.span);
84
- at(diagnostic.code === "HSX1001" ? "bind" : "typecheck", diagnostic.severity, diagnostic.message, diagnostic.span, {
85
- code: diagnostic.code,
86
- fix: diagnostic.fix,
87
- ...(diagnostic.path ? { path: diagnostic.path } : {}),
88
- ...(diagnostic.udlCode ? { udlCode: diagnostic.udlCode } : {}),
89
- }, moduleOrigin);
43
+ return map;
44
+ }
45
+ function asBlock(expr) {
46
+ if (!expr)
47
+ return emptyBlock;
48
+ if (expr.kind !== "block")
49
+ fail(expr, "expected a block", "write { name: value }");
50
+ return expr;
51
+ }
52
+ function text(expr) {
53
+ if ("value" in expr && typeof expr.value === "string")
54
+ return expr.value;
55
+ return fail(expr, "expected a name or literal", "write a single name or literal");
56
+ }
57
+ function decimal(raw, scale, expr) {
58
+ const [whole = "", fraction = ""] = raw.split(".");
59
+ if (!/^\d+$/.test(whole) || fraction.length > scale)
60
+ fail(expr, `literal has more than ${scale} decimal places`, `write at most ${scale} decimal places`);
61
+ return (BigInt(whole) * 10n ** BigInt(scale) +
62
+ BigInt(fraction.padEnd(scale, "0") || "0")).toString();
63
+ }
64
+ function literal(expr) {
65
+ if (expr.kind === "money") {
66
+ const [raw, currency] = expr.value.split(" ");
67
+ if (currency !== "SAR")
68
+ fail(expr, `currency ${currency} differs from SAR`, "write this amount in SAR");
69
+ const result = decimal(raw, 2, expr);
70
+ if (result.length > 18)
71
+ fail(expr, "amount exceeds 18 minor-unit digits", "write a smaller amount");
72
+ return result;
90
73
  }
91
- if (!checked.program)
92
- return { diagnostics, verdict: "invalid" };
93
- const cost = buildCostManifest(checked.program, options.costTable);
94
- if (!cost.ok) {
95
- for (const diagnostic of cost.diagnostics) {
96
- at("typecheck", diagnostic.severity, diagnostic.message, diagnostic.span, {
97
- code: diagnostic.code,
98
- fix: diagnostic.fix,
99
- ...(diagnostic.path ? { path: diagnostic.path } : {}),
100
- ...(diagnostic.udlCode ? { udlCode: diagnostic.udlCode } : {}),
101
- });
102
- }
103
- return { diagnostics, verdict: "invalid" };
74
+ if (expr.kind === "percent") {
75
+ const n = Number(decimal(expr.value, 2, expr));
76
+ if (n > 10000)
77
+ fail(expr, "percentage exceeds 100%", "write a percentage from 0% to 100%");
78
+ return n;
104
79
  }
105
- const lowered = lowerGeneralProgram(checked.program);
106
- if (!lowered.ok) {
107
- for (const issue of lowered.issues) {
108
- at("lower", "error", issue.message, issue.span, {
109
- code: issue.code,
110
- fix: issue.fix,
111
- path: issue.path,
112
- ...(issue.udlCode ? { udlCode: issue.udlCode } : {}),
113
- });
114
- }
115
- return { diagnostics, verdict: "invalid" };
80
+ if (expr.kind === "number") {
81
+ const n = Number(expr.value);
82
+ if (!Number.isSafeInteger(n))
83
+ fail(expr, "expected a safe integer", "write an integer, a percentage, or an amount with SAR");
84
+ return n;
85
+ }
86
+ if (expr.kind === "duration" ||
87
+ (expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)) ||
88
+ (expr.kind === "text" && /^P(?:\d|T)/.test(expr.value))) {
89
+ const short = /^(\d+)(ms|s|m|h|d|w)$/.exec(expr.value);
90
+ const units = {
91
+ ms: 1,
92
+ s: 1000,
93
+ m: 60000,
94
+ h: 3600000,
95
+ d: 86400000,
96
+ w: 604800000,
97
+ };
98
+ const iso = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(expr.value);
99
+ const n = short
100
+ ? Number(short[1]) * units[short[2]]
101
+ : iso
102
+ ? Number(iso[1] ?? 0) * units.w +
103
+ Number(iso[2] ?? 0) * units.d +
104
+ Number(iso[3] ?? 0) * units.h +
105
+ Number(iso[4] ?? 0) * units.m +
106
+ Number(iso[5] ?? 0) * units.s
107
+ : NaN;
108
+ if (!Number.isSafeInteger(n) || n <= 0)
109
+ fail(expr, "invalid duration", "write a positive fixed duration such as 48h or 3d");
110
+ return n;
111
+ }
112
+ if (expr.kind === "date") {
113
+ const raw = expr.value.length === 10 ? expr.value + "T00:00:00Z" : expr.value;
114
+ const n = Date.parse(raw);
115
+ if (!Number.isFinite(n) ||
116
+ new Date(raw.slice(0, 10) + "T00:00:00Z").toISOString().slice(0, 10) !==
117
+ raw.slice(0, 10))
118
+ fail(expr, "invalid date", "write a calendar date or a timestamp with an explicit offset");
119
+ return new Date(n).toISOString();
116
120
  }
117
- const compileOrigin = (entry) => {
118
- const position = lineColIn(lines, entry.span.start);
121
+ if (expr.kind === "text")
122
+ return expr.value;
123
+ if (expr.kind === "name" && ["true", "false"].includes(expr.value))
124
+ return expr.value === "true";
125
+ return fail(expr, "expected a literal", "write a typed constant");
126
+ }
127
+ export function compile(source, options = {}) {
128
+ const parsed = parseProgram(source);
129
+ const diagnostic = (d, stage) => ({
130
+ ...d,
131
+ ...lineColAt(source, d.span.start),
132
+ severity: "error",
133
+ stage,
134
+ });
135
+ if (parsed.diagnostics.length)
119
136
  return {
120
- path: entry.path,
121
- span: {
122
- column: position.column,
123
- end: byteOffsets[entry.span.end] ?? byteOffsets[byteOffsets.length - 1],
124
- line: position.line,
125
- start: byteOffsets[entry.span.start] ?? 0,
137
+ verdict: "invalid",
138
+ diagnostics: parsed.diagnostics.map((d) => diagnostic(d, "parse")),
139
+ };
140
+ const program = parsed.program;
141
+ try {
142
+ if (program.header)
143
+ fail(program, "compile a program, not a header", 'start with program product_name "Title"');
144
+ if (program.currency !== "SAR")
145
+ fail(program, "this release supports SAR", "write currency SAR or omit currency");
146
+ const templates = new Map();
147
+ const used = new Set();
148
+ for (const use of program.decls.filter((d) => d.kind === "use")) {
149
+ if (used.has(use.name))
150
+ fail(use, `header ${use.name} is used twice`, "remove the duplicate use line");
151
+ used.add(use.name);
152
+ const content = (options.standardLibrary ?? bundledStandardLibrary).source(use.name);
153
+ if (!content)
154
+ fail(use, `unknown header ${use.name}`, "choose a published header");
155
+ const header = parseProgram(content);
156
+ if (header.diagnostics.length ||
157
+ !header.program.header ||
158
+ header.program.name !== use.name)
159
+ fail(use, `header ${use.name} is malformed`, "repair the header source before compiling");
160
+ for (const decl of header.program.decls)
161
+ if (decl.kind === "instrument")
162
+ templates.set(`${use.name}.${decl.name}`, decl);
163
+ }
164
+ const document = {
165
+ udl: 3,
166
+ version: 1,
167
+ product: program.name,
168
+ title: program.title,
169
+ currency: "SAR",
170
+ parties: {
171
+ programOperator: { kind: "business", role: "program_operator" },
172
+ programTax: { kind: "business", role: "tax_payable" },
173
+ programFines: { kind: "business", role: "fine_payable" },
174
+ programCosts: { kind: "business", role: "cost_recovery" },
126
175
  },
176
+ instruments: [],
127
177
  };
128
- };
129
- return {
130
- artifacts: {
131
- document: lowered.value.document,
132
- ...(lowered.value.actionPlans
133
- ? { actionPlans: lowered.value.actionPlans }
134
- : {}),
135
- costManifest: cost.manifest,
136
- originMap: lowered.value.originMap.map(compileOrigin),
137
- },
138
- diagnostics,
139
- verdict: diagnostics.length > 0 ? "warning" : "valid",
140
- };
141
- }
142
- function utf8OffsetIndex(source) {
143
- const offsets = new Uint32Array(source.length + 1);
144
- let bytes = 0;
145
- for (let index = 0; index < source.length; index += 1) {
146
- offsets[index] = bytes;
147
- const codePoint = source.codePointAt(index);
148
- if (codePoint > 0xffff) {
149
- offsets[index + 1] = bytes;
150
- bytes += 4;
151
- index += 1;
178
+ const objects = new Map();
179
+ const names = new Set();
180
+ for (const decl of program.decls) {
181
+ if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
182
+ continue;
183
+ if (names.has(decl.name))
184
+ fail(decl, `duplicate declaration ${decl.name}`, "give this declaration a distinct name");
185
+ names.add(decl.name);
186
+ if (decl.kind === "object")
187
+ objects.set(decl.name, decl);
188
+ if (decl.kind === "party") {
189
+ if (!["person", "business", "staff"].includes(decl.partyKind))
190
+ fail(decl, `unknown party kind ${decl.partyKind}`, "choose person, business, or staff");
191
+ document.parties[decl.name] = {
192
+ kind: decl.partyKind,
193
+ ...(decl.role ? { role: decl.role } : {}),
194
+ };
195
+ }
152
196
  }
153
- else if (codePoint > 0x7ff)
154
- bytes += 3;
155
- else if (codePoint > 0x7f)
156
- bytes += 2;
157
- else
158
- bytes += 1;
197
+ const origins = [];
198
+ const materialApprovals = new Set();
199
+ const implicitDecisions = new Map();
200
+ const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedApprovers = new Set(), inheritedEnums = new Map()) => {
201
+ const enums = new Map(inheritedEnums);
202
+ for (const parameter of decl.parameters) {
203
+ const type = parameter.value.kind === "default"
204
+ ? parameter.value.type
205
+ : parameter.value;
206
+ if (type.kind === "call" && type.name === "enum")
207
+ enums.set(parameter.key, type.args.map(text));
208
+ }
209
+ const approvers = new Set(inheritedApprovers);
210
+ const supplied = entries(arguments_);
211
+ const environment = new Map(inherited);
212
+ for (const param of decl.parameters) {
213
+ const type = param.value.kind === "default" ? param.value.type : param.value;
214
+ const fallback = param.value.kind === "default" ? param.value.value : undefined;
215
+ const actual = supplied.get(param.key) ?? fallback;
216
+ if (!actual) {
217
+ if (type.kind === "type" && type.optional)
218
+ continue;
219
+ fail({ span: origin }, `${id} needs ${param.key}`, `add ${param.key}: value inside ${id}`);
220
+ }
221
+ environment.set(param.key, actual);
222
+ }
223
+ for (const key of supplied.keys())
224
+ if (!decl.parameters.some((p) => p.key === key))
225
+ fail(supplied.get(key), `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
226
+ const resolve = (expr, seen = new Set()) => {
227
+ if (expr.kind === "call" &&
228
+ ["object", "all", "party"].includes(expr.name)) {
229
+ if (expr.args.length !== 1)
230
+ fail(expr, `${expr.name} needs one type`, "supply one declared object or party kind");
231
+ const type = text(expr.args[0]);
232
+ const matches = expr.name === "party"
233
+ ? Object.entries(document.parties)
234
+ .filter(([, party]) => party.kind === type)
235
+ .map(([name]) => name)
236
+ : [...objects.values()]
237
+ .filter((object) => object.object === type ||
238
+ type.startsWith(`${object.object}.`))
239
+ .map((object) => object.name +
240
+ type.slice(object.object.length).replaceAll(".", "_"));
241
+ if (expr.name !== "all" && matches.length !== 1)
242
+ fail({ span: origin }, `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`, "declare the required object or supply this tunable explicitly");
243
+ const items = matches.map((value) => ({
244
+ kind: "name",
245
+ value,
246
+ span: origin,
247
+ }));
248
+ return expr.name === "all"
249
+ ? { kind: "list", items, span: origin }
250
+ : items[0];
251
+ }
252
+ if (expr.kind !== "name")
253
+ return expr;
254
+ if (expr.value.startsWith("party.")) {
255
+ const binding = environment.get(expr.value.slice(6));
256
+ if (binding?.kind === "name" && document.parties[binding.value])
257
+ return { ...expr, value: `party.${binding.value}` };
258
+ }
259
+ const [root, ...tail] = expr.value.split(".");
260
+ const bound = environment.get(root);
261
+ if (!bound || (bound.kind === "name" && bound.value === root))
262
+ return expr;
263
+ if (seen.has(root))
264
+ return fail(expr, `cyclic tunable ${root}`, "replace the cycle with a literal or declared reference");
265
+ let resolved = supplied.has(root) || inherited.has(root) || enums.has(root)
266
+ ? bound
267
+ : resolve(bound, new Set([...seen, root]));
268
+ for (const key of tail) {
269
+ if (resolved.kind !== "block")
270
+ return expr;
271
+ const child = entries(resolved).get(key);
272
+ if (!child)
273
+ return fail(expr, `missing tunable ${expr.value}`, `declare ${key} in ${root}`);
274
+ resolved = resolve(child, new Set([...seen, root]));
275
+ }
276
+ return resolved;
277
+ };
278
+ for (const param of decl.parameters) {
279
+ const actual = environment.get(param.key);
280
+ if (!actual)
281
+ continue;
282
+ const t = param.value.kind === "default" ? param.value.type : param.value;
283
+ const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
284
+ const v = supplied.has(param.key) || type === "enum" ? actual : resolve(actual);
285
+ environment.set(param.key, v);
286
+ if (type === "enum" && t.kind === "call") {
287
+ if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
288
+ fail(v, `invalid ${param.key}`, `choose ${t.args.map(text).join(", ")}`);
289
+ }
290
+ else if (type === "list") {
291
+ if (v.kind !== "list")
292
+ fail(v, `${param.key} needs a list`, "write [value, value]");
293
+ }
294
+ else if (type === "party" || type === "approval") {
295
+ if (v.kind !== "name" || !document.parties[v.value])
296
+ fail(v, `${param.key} needs a declared party`, "declare a party and use its name here");
297
+ if (type === "approval")
298
+ approvers.add(text(v));
299
+ }
300
+ else if (type === "ref") {
301
+ const values = v.kind === "list" ? v.items : [v];
302
+ if (v.kind === "list" && (t.kind !== "type" || !t.many))
303
+ fail(v, `${param.key} accepts one reference`, "use one object name");
304
+ if (!values.length || values.length > 16)
305
+ fail(v, "reference union needs 1 to 16 objects", "use at most 16 distinct object names");
306
+ const seen = new Set();
307
+ for (const value of values) {
308
+ if (value.kind !== "name")
309
+ fail(value, "reference needs an object name", "name a declared object");
310
+ const [root, ...tail] = value.value.split(".");
311
+ const obj = objects.get(root);
312
+ if ((!obj &&
313
+ !document.instruments.some((inst) => inst.id === value.value)) ||
314
+ (t.kind === "type" &&
315
+ t.target &&
316
+ [obj?.object, ...tail].join(".") !== t.target))
317
+ fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
318
+ if (seen.has(value.value))
319
+ fail(value, "duplicate reference", "list each object once");
320
+ seen.add(value.value);
321
+ }
322
+ }
323
+ else if (type === "fee" || type === "split" || type === "policy") {
324
+ if (v.kind !== "block")
325
+ fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
326
+ }
327
+ else if (type === "money" ||
328
+ type === "percent" ||
329
+ type === "date" ||
330
+ type === "duration" ||
331
+ type === "integer" ||
332
+ type === "text") {
333
+ if (v.kind === "name" && v.value === "runtime")
334
+ continue;
335
+ const expected = type === "integer" ? "number" : type;
336
+ if (v.kind !== expected &&
337
+ !(type === "duration" &&
338
+ (v.kind === "text" || v.kind === "name") &&
339
+ /^P/.test(v.value)))
340
+ fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
341
+ try {
342
+ const value = literal(v);
343
+ const bounds = tunableBounds(t);
344
+ if (bounds &&
345
+ (BigInt(String(value)) < BigInt(bounds.minimum) ||
346
+ BigInt(String(value)) > BigInt(bounds.maximum)))
347
+ fail(v, `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`, "choose a value inside the tunable's bounds");
348
+ }
349
+ catch (error) {
350
+ if (error instanceof CompileFailure)
351
+ fail(v, `${param.key}: ${error.diagnostic.message}`, error.diagnostic.fix);
352
+ throw error;
353
+ }
354
+ }
355
+ }
356
+ const body = entries(decl.body);
357
+ for (const constraint of asBlock(body.get("constraints")).entries) {
358
+ const rule = constraint.value;
359
+ if (rule.kind !== "call" ||
360
+ rule.args.length !== 1 ||
361
+ !["less_than", "at_most", "greater_than"].includes(rule.name))
362
+ fail(rule, "invalid tunable constraint", "use less_than(name), at_most(name), or greater_than(name)");
363
+ const left = environment.get(constraint.key), right = environment.get(text(rule.args[0]));
364
+ if (!left || !right)
365
+ fail(rule, "constraint needs two declared tunables", "name the compared tunables");
366
+ const a = BigInt(String(literal(left))), b = BigInt(String(literal(right)));
367
+ if (!(rule.name === "less_than"
368
+ ? a < b
369
+ : rule.name === "at_most"
370
+ ? a <= b
371
+ : a > b))
372
+ fail(left, `${constraint.key} must be ${rule.name} ${text(rule.args[0])}`, "choose values satisfying the header constraint");
373
+ }
374
+ for (const key of body.keys())
375
+ if (![
376
+ "fields",
377
+ "lifecycle",
378
+ "records",
379
+ "summary",
380
+ "invariants",
381
+ "constraints",
382
+ ].includes(key) &&
383
+ !key.startsWith("action "))
384
+ fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
385
+ const records = entries(asBlock(body.get("records")));
386
+ if (!inherited.size)
387
+ environment.set("parent", { kind: "name", value: id, span: origin });
388
+ for (const key of records.keys())
389
+ if (!decl.parameters.some((parameter) => parameter.key === key))
390
+ environment.set(key, {
391
+ kind: "name",
392
+ value: `${id}_${key}`,
393
+ span: origin,
394
+ });
395
+ const fields = [];
396
+ const calculations = [];
397
+ const path = (expr) => {
398
+ const value = resolve(expr);
399
+ const name = text(value);
400
+ if (document.parties[name])
401
+ return `party.${name}`;
402
+ return /^(self|input|party)\./.test(name) ? name : `self.${name}`;
403
+ };
404
+ const val = (expr) => {
405
+ const v = resolve(expr);
406
+ return v.kind === "name" && !["true", "false"].includes(v.value)
407
+ ? { field: path(v) }
408
+ : { literal: literal(v) };
409
+ };
410
+ const data = (expr) => {
411
+ if (expr.kind === "call" &&
412
+ expr.name === "current" &&
413
+ !expr.args.length)
414
+ return id;
415
+ const value = resolve(expr);
416
+ if (value.kind === "block") {
417
+ const result = Object.fromEntries([...entries(value)].map(([key, value]) => [key, data(value)]));
418
+ const selection = result.selection;
419
+ if (Array.isArray(selection?.instrument) &&
420
+ selection.instrument.length === 0) {
421
+ if (result.op === "aggregate")
422
+ return {
423
+ target: result.target,
424
+ op: "sum",
425
+ values: [{ literal: result.measure === "count" ? 0 : "0" }],
426
+ };
427
+ if (result.kind === "aggregate")
428
+ return {
429
+ kind: "compare",
430
+ left: { literal: result.measure === "count" ? 0 : "0" },
431
+ operator: result.operator,
432
+ right: result.value,
433
+ };
434
+ return null;
435
+ }
436
+ return result;
437
+ }
438
+ if (value.kind === "list")
439
+ return value.items.map(data).filter((item) => item !== null);
440
+ if (value.kind === "call" &&
441
+ value.name === "child" &&
442
+ value.args.length === 2) {
443
+ const parent = resolve(value.args[0]);
444
+ const child = (item) => `${text(item).replaceAll(".", "_")}_${text(value.args[1])}`;
445
+ return parent.kind === "list"
446
+ ? parent.items.map(child)
447
+ : child(parent);
448
+ }
449
+ if (value.kind === "name")
450
+ return value.value === "true" || value.value === "false"
451
+ ? value.value === "true"
452
+ : value.value;
453
+ return literal(value);
454
+ };
455
+ const lowerFields = (block) => {
456
+ const result = [];
457
+ for (const row of block.entries) {
458
+ const t = row.value.kind === "default" ? row.value.type : row.value;
459
+ const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
460
+ const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
461
+ const f = {
462
+ name: row.key,
463
+ type,
464
+ ...(t.kind === "type" && t.optional ? { optional: true } : {}),
465
+ };
466
+ if (type === "enum" && t.kind === "call")
467
+ f.values = t.args.map(text);
468
+ if (["integer", "money"].includes(type) && t.kind === "call") {
469
+ if (t.args.length !== 2)
470
+ fail(t, "bounded fields need a minimum and maximum", "write integer(1, 12) or money(0 SAR, 100 SAR)");
471
+ f.minimum = literal(resolve(t.args[0]));
472
+ f.maximum = literal(resolve(t.args[1]));
473
+ }
474
+ if (type === "list" && t.kind === "call") {
475
+ f.item = text(t.args[0]);
476
+ f.maxItems = t.args[1] ? literal(resolve(t.args[1])) : 366;
477
+ }
478
+ if (type === "list" && t.kind === "type") {
479
+ f.item = t.target;
480
+ f.maxItems = 366;
481
+ }
482
+ if (type === "ref") {
483
+ const target = t.kind === "type" ? t.target : undefined;
484
+ if (!target)
485
+ fail(row, "reference needs a target", "write ref<object>");
486
+ const [root, ...tail] = target.split(".");
487
+ const resolved = environment.get(root);
488
+ const resolvedTargets = resolved?.kind === "list"
489
+ ? resolved.items
490
+ : resolved
491
+ ? [resolved]
492
+ : [];
493
+ const targets = resolvedTargets.map((value) => [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"));
494
+ f.target =
495
+ target === "self"
496
+ ? id
497
+ : targets.length === 1
498
+ ? targets[0]
499
+ : targets.length
500
+ ? targets
501
+ : target;
502
+ }
503
+ else if (type === "account") {
504
+ if (t.kind === "call") {
505
+ if (t.args.length < 1 || t.args.length > 4)
506
+ fail(t, "account needs an owner, optional book, mode and key", "write account(buyer, claim, contra)");
507
+ f.owner = text(resolve(t.args[0]));
508
+ f.book = t.args[1] ? text(t.args[1]) : "cash";
509
+ if (t.args[2]) {
510
+ const mode = text(t.args[2]);
511
+ if (["contra", "external"].includes(mode))
512
+ f[mode] = true;
513
+ else
514
+ f.key = mode;
515
+ }
516
+ if (t.args[3])
517
+ f.key = text(t.args[3]);
518
+ }
519
+ else if (t.kind === "type" && t.owner) {
520
+ f.owner =
521
+ t.owner === "self"
522
+ ? "self"
523
+ : text(resolve({ kind: "name", value: t.owner, span: row.span }));
524
+ f.book = "cash";
525
+ }
526
+ else
527
+ fail(row, "account needs an owner", "write account of buyer or account of self");
528
+ }
529
+ if (constant &&
530
+ !(constant.kind === "name" && constant.value === "runtime")) {
531
+ if (constant.kind === "call") {
532
+ const args = constant.name === "aggregate" ? [] : constant.args.map(val);
533
+ const target = row.key;
534
+ if (constant.name === "aggregate" && constant.args.length === 2)
535
+ calculations.push({
536
+ target,
537
+ op: "aggregate",
538
+ selection: data(constant.args[0]),
539
+ measure: text(constant.args[1]) === "count"
540
+ ? "count"
541
+ : { sum: text(constant.args[1]) },
542
+ });
543
+ else if (constant.name === "at" && args.length === 2)
544
+ calculations.push({
545
+ target,
546
+ op: "at",
547
+ list: path(constant.args[0]),
548
+ position: args[1],
549
+ });
550
+ else if (constant.name === "ratio" && args.length === 3)
551
+ calculations.push({
552
+ target,
553
+ op: "ratio",
554
+ amount: args[0],
555
+ numerator: args[1],
556
+ denominator: args[2],
557
+ rounding: "floor",
558
+ });
559
+ else if (constant.name === "rate" && args.length === 2)
560
+ calculations.push({
561
+ target,
562
+ op: "rate",
563
+ base: args[0],
564
+ bps: args[1],
565
+ rounding: "floor",
566
+ });
567
+ else if (constant.name === "sum" || constant.name === "minimum")
568
+ calculations.push({ target, op: constant.name, values: args });
569
+ else if (constant.name === "subtract" && args.length >= 2)
570
+ calculations.push({
571
+ target,
572
+ op: "subtract",
573
+ base: args[0],
574
+ subtract: args.slice(1),
575
+ });
576
+ else if (constant.name === "multiply" && args.length === 2)
577
+ calculations.push({
578
+ target,
579
+ op: "multiply",
580
+ amount: args[0],
581
+ units: args[1],
582
+ });
583
+ else if (constant.name === "divide" && args.length === 2)
584
+ calculations.push({
585
+ target,
586
+ op: "divide",
587
+ amount: args[0],
588
+ divisor: args[1],
589
+ rounding: "floor",
590
+ });
591
+ else if (["before", "after"].includes(constant.name) &&
592
+ args.length === 2)
593
+ calculations.push({
594
+ target,
595
+ op: "shift",
596
+ date: args[0],
597
+ milliseconds: args[1],
598
+ direction: constant.name,
599
+ });
600
+ else
601
+ fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
602
+ }
603
+ else if (type === "enum" && constant.kind === "name")
604
+ f.value = constant.value;
605
+ else if (type === "money" && constant.kind === "name")
606
+ calculations.push({
607
+ target: row.key,
608
+ op: "sum",
609
+ values: [val(constant)],
610
+ });
611
+ else
612
+ f.value = literal(constant);
613
+ }
614
+ result.push(f);
615
+ }
616
+ return result;
617
+ };
618
+ fields.push(...lowerFields(asBlock(body.get("fields"))));
619
+ const lifecycle = data(body.get("lifecycle") ?? emptyBlock);
620
+ lifecycle.transitions = {};
621
+ const inst = {
622
+ id,
623
+ title: title(id),
624
+ summary: body.has("summary")
625
+ ? String(data(body.get("summary")))
626
+ : title(id),
627
+ fields,
628
+ calculate: calculations,
629
+ lifecycle,
630
+ actions: {},
631
+ actionOrder: [],
632
+ };
633
+ for (const row of decl.body.entries.filter((e) => e.key.startsWith("action "))) {
634
+ const name = row.key.slice(7);
635
+ const selected = (block) => ({
636
+ ...block,
637
+ entries: block.entries.flatMap((entry) => {
638
+ if (!entry.key.startsWith("when "))
639
+ return [entry];
640
+ const [, tunable, , choice] = entry.key.split(" ");
641
+ const binding = environment.get(tunable);
642
+ if (!binding)
643
+ fail(entry, `unknown branch tunable ${tunable}`, "name an enum tunable declared by this header");
644
+ if (!enums.get(tunable)?.includes(choice))
645
+ fail(entry, `unknown enum branch ${choice}`, "use a declared enum value");
646
+ const body = asBlock(entry.value);
647
+ for (const clause of body.entries)
648
+ if (!["requires", "moves", "invoke", "calculate"].includes(clause.key) &&
649
+ !clause.key.startsWith("when "))
650
+ fail(clause, "when permits requirements, calculations, moves and invocations", "keep lifecycle and actor clauses outside the branch");
651
+ return text(binding) === choice ? selected(body).entries : [];
652
+ }),
653
+ });
654
+ const slots = entries(selected(asBlock(row.value)));
655
+ const a = {
656
+ summary: slots.has("summary")
657
+ ? String(data(slots.get("summary")))
658
+ : `${title(name)} ${id}`,
659
+ publicAction: camel(`${name}_${id}`),
660
+ event: `${id}.${name}`,
661
+ actor: "caller",
662
+ input: lowerFields(asBlock(slots.get("input"))),
663
+ requires: [],
664
+ moves: [],
665
+ };
666
+ if (name !== "create") {
667
+ const from = slots.get("from");
668
+ const to = slots.get("to");
669
+ if (!from || !to)
670
+ fail(row, `action ${name} needs from and to`, "declare its source and destination states");
671
+ lifecycle.transitions[name] = {
672
+ from: (from.kind === "list" ? from.items : [from]).map((e) => String(data(e))),
673
+ to: String(data(to)),
674
+ };
675
+ }
676
+ for (const [key, expr] of slots) {
677
+ if (["from", "to", "input", "summary"].includes(key))
678
+ continue;
679
+ if (key === "moves") {
680
+ const moves = expr.kind === "list" ? expr.items : [expr];
681
+ for (const [index, move] of moves.entries()) {
682
+ const parts = entries(asBlock(move));
683
+ const op = parts.has("operation")
684
+ ? String(data(parts.get("operation")))
685
+ : "internal_transfer.create";
686
+ if (op === "internal_transfer.reserve" && !parts.has("capture"))
687
+ fail(move, "reserve needs a receipt field", "add capture: receipt_name");
688
+ if ((op === "internal_transfer.post" ||
689
+ op === "internal_transfer.void") &&
690
+ !parts.has("transfer"))
691
+ fail(move, "post and void need a receipt", "add transfer: receipt_name");
692
+ const key = parts.has("key")
693
+ ? String(data(parts.get("key")))
694
+ : `move${index + 1}`;
695
+ if (op === "internal_transfer.create" ||
696
+ op === "internal_transfer.reserve") {
697
+ const amount = parts.get("amount"), from = parts.get("from"), to = parts.get("to");
698
+ if (parts.has("shares")) {
699
+ if (!amount ||
700
+ !from ||
701
+ op !== "internal_transfer.create" ||
702
+ parts.has("fee"))
703
+ fail(move, "split needs a posted amount and source", "declare amount, from and shares without a fee");
704
+ const shares = [
705
+ ...entries(asBlock(resolve(parts.get("shares")))),
706
+ ];
707
+ if (!shares.length || shares.length > 64)
708
+ fail(move, "split needs 1 to 64 recipients", "name the receiving parties and percentages");
709
+ let total = 0;
710
+ const pieces = [];
711
+ for (const [index, [party, rate]] of shares.entries()) {
712
+ const recipient = resolve({
713
+ kind: "name",
714
+ value: party,
715
+ span: rate.span,
716
+ });
717
+ if (recipient.kind !== "name" ||
718
+ !document.parties[recipient.value] ||
719
+ rate.kind !== "percent")
720
+ fail(rate, "split needs party percentages", "write party_name: 70%");
721
+ total += Number(literal(rate));
722
+ const target = `${name}_${key}_share${index + 1}`;
723
+ if (fields.some((f) => f.name === target))
724
+ fail(move, "split field name conflicts", "rename the move key");
725
+ fields.push({ name: target, type: "money" });
726
+ calculations.push(index === shares.length - 1 && pieces.length
727
+ ? {
728
+ target,
729
+ op: "subtract",
730
+ base: val(amount),
731
+ subtract: [...pieces],
732
+ }
733
+ : {
734
+ target,
735
+ op: "rate",
736
+ base: val(amount),
737
+ bps: { literal: literal(rate) },
738
+ rounding: "floor",
739
+ });
740
+ const piece = { field: `self.${target}` };
741
+ pieces.push(piece);
742
+ a.moves.push({
743
+ key: `${key}_${index + 1}`,
744
+ operation: "internal_transfer.create",
745
+ amount: piece,
746
+ from: path(from),
747
+ to: `party.${recipient.value}`,
748
+ });
749
+ }
750
+ if (total !== 10000)
751
+ fail(move, "split percentages must sum to 100%", "give the recipients exactly 100% together");
752
+ continue;
753
+ }
754
+ if (!amount || !from || !to)
755
+ fail(move, "move needs amount, from and to", "declare all three move operands");
756
+ const transfer = {
757
+ key,
758
+ operation: op,
759
+ ...(parts.has("capture")
760
+ ? { capture: String(data(parts.get("capture"))) }
761
+ : {}),
762
+ amount: val(amount),
763
+ from: path(from),
764
+ to: path(to),
765
+ };
766
+ const fee = parts.get("fee");
767
+ if (!fee)
768
+ a.moves.push(op === "internal_transfer.reserve"
769
+ ? {
770
+ ...transfer,
771
+ operation: op,
772
+ capture: String(data(parts.get("capture"))),
773
+ }
774
+ : { ...transfer, operation: op });
775
+ else {
776
+ if (op !== "internal_transfer.create")
777
+ fail(move, "fees settle with a posted transfer", "declare a separate posted settlement after releasing the reservation");
778
+ const terms = entries(asBlock(resolve(fee)));
779
+ for (const term of terms.keys())
780
+ if (!["seller", "buyer", "tax"].includes(term))
781
+ fail(fee, `unknown fee term ${term}`, "choose seller or buyer, and optional tax");
782
+ const paidBy = terms.has("seller") ? "seller" : "buyer";
783
+ if (terms.has("seller") === terms.has("buyer"))
784
+ fail(fee, "fee needs exactly one paying party", "write seller: 1% or buyer: 1%");
785
+ const quoted = terms.get(paidBy);
786
+ const rate = quoted.kind === "capped" ? quoted.rate : quoted;
787
+ if (rate.kind !== "percent")
788
+ fail(rate, "fee rate needs a percentage", "write a percentage such as 1%");
789
+ const prefix = `${name}_${key}`;
790
+ const derived = (suffix, calculation) => {
791
+ const target = prefix + "_" + suffix;
792
+ if (fields.some((f) => f.name === target))
793
+ fail(move, `generated fee field ${target} conflicts with a field`, "rename the authored field or move key");
794
+ fields.push({ name: target, type: "money" });
795
+ calculations.push({
796
+ ...calculation,
797
+ target,
798
+ });
799
+ return { field: `self.${target}` };
800
+ };
801
+ const gross = derived("feeGross", {
802
+ op: "rate",
803
+ base: transfer.amount,
804
+ bps: { literal: literal(rate) },
805
+ rounding: "floor",
806
+ });
807
+ let charge = gross;
808
+ if (quoted.kind === "capped") {
809
+ if (quoted.cap.kind !== "money")
810
+ fail(quoted.cap, "fee cap needs money", "write an amount such as 750 SAR");
811
+ charge = derived("fee", {
812
+ op: "minimum",
813
+ values: [gross, { literal: literal(quoted.cap) }],
814
+ });
815
+ }
816
+ const tax = terms.get("tax");
817
+ if (tax && tax.kind !== "percent")
818
+ fail(tax, "tax needs a percentage of the fee", "write tax: 15%");
819
+ const vat = tax
820
+ ? derived("tax", {
821
+ op: "rate",
822
+ base: charge,
823
+ bps: { literal: literal(tax) },
824
+ rounding: "floor",
825
+ })
826
+ : undefined;
827
+ const charges = vat ? [charge, vat] : [charge];
828
+ const net = paidBy === "seller"
829
+ ? derived("net", {
830
+ op: "subtract",
831
+ base: transfer.amount,
832
+ subtract: charges,
833
+ })
834
+ : transfer.amount;
835
+ for (const [party, role] of [
836
+ ["programOperator", "program_operator"],
837
+ ["programTax", "tax_payable"],
838
+ ]) {
839
+ if (document.parties[party] &&
840
+ document.parties[party].role !== role)
841
+ fail(move, `${party} is reserved for fees`, "rename this party");
842
+ document.parties[party] = { kind: "business", role };
843
+ }
844
+ a.moves.push({
845
+ ...transfer,
846
+ operation: "internal_transfer.create",
847
+ amount: net,
848
+ });
849
+ a.moves.push({
850
+ key: key + "Fee",
851
+ operation: "internal_transfer.create",
852
+ amount: charge,
853
+ from: transfer.from,
854
+ to: "party.programOperator",
855
+ });
856
+ if (vat)
857
+ a.moves.push({
858
+ key: key + "Tax",
859
+ operation: "internal_transfer.create",
860
+ amount: vat,
861
+ from: transfer.from,
862
+ to: "party.programTax",
863
+ });
864
+ }
865
+ }
866
+ else if (op === "internal_transfer.post" ||
867
+ op === "internal_transfer.void")
868
+ a.moves.push({
869
+ key,
870
+ operation: op,
871
+ transfer: path(parts.get("transfer")),
872
+ ...(parts.has("capture")
873
+ ? { capture: String(data(parts.get("capture"))) }
874
+ : {}),
875
+ });
876
+ else
877
+ fail(move, `unknown transfer instruction ${op}`, "choose create, reserve, post, or void from internal_transfer");
878
+ }
879
+ }
880
+ else if (key === "approval") {
881
+ const approval = data(expr);
882
+ if (approval && approval.input === "material") {
883
+ approval.input = {};
884
+ materialApprovals.add(a);
885
+ }
886
+ a.approval = approval;
887
+ }
888
+ else
889
+ a[key] = data(expr);
890
+ }
891
+ if (automatic(a.actor))
892
+ delete a.publicAction;
893
+ for (const requirement of a.requires ?? []) {
894
+ if (requirement.kind !== "approval" ||
895
+ !approvers.has(requirement.party))
896
+ continue;
897
+ const action = requirement.action ?? name;
898
+ const key = `${id}_${action}_decision`;
899
+ const previous = implicitDecisions.get(key);
900
+ if (previous && previous.party !== requirement.party)
901
+ fail({ span: origin }, `action ${action} has multiple approval parties`, "use a separate decision action for each party");
902
+ implicitDecisions.set(key, {
903
+ target: id,
904
+ action,
905
+ party: requirement.party,
906
+ origin,
907
+ });
908
+ }
909
+ inst.actions[name] = a;
910
+ inst.actionOrder.push(name);
911
+ }
912
+ for (const key of ["invariants"])
913
+ if (body.has(key))
914
+ inst[key] = data(body.get(key));
915
+ origins.push({
916
+ path: `$.instruments[${document.instruments.length}]`,
917
+ span: { ...origin, ...lineColAt(source, origin.start) },
918
+ });
919
+ document.instruments.push(inst);
920
+ for (const [key, definition] of records) {
921
+ const child = {
922
+ kind: "instrument",
923
+ name: key,
924
+ parameters: [],
925
+ body: asBlock(definition),
926
+ span: origin,
927
+ };
928
+ addInstrument(child, `${id}_${key}`, emptyBlock, origin, new Map([
929
+ ...environment,
930
+ ["parent", { kind: "name", value: id, span: origin }],
931
+ ]), approvers, enums);
932
+ }
933
+ };
934
+ for (const decl of program.decls) {
935
+ if (decl.kind === "instrument") {
936
+ if (decl.parameters.length)
937
+ fail(decl, "program records cannot declare tunables", "put reusable instruments in a header");
938
+ addInstrument(decl, decl.name, emptyBlock, decl.span);
939
+ }
940
+ if (decl.kind === "object") {
941
+ const template = templates.get(decl.object);
942
+ if (!template)
943
+ fail(decl, `unknown object ${decl.object}`, `add use ${decl.object.split(".")[0]} and choose a declared object`);
944
+ addInstrument(template, decl.name, decl.body, decl.span);
945
+ }
946
+ }
947
+ const eliminatedStates = new Map();
948
+ // Remove branches excluded by immutable tunables before checking reference states.
949
+ for (const inst of document.instruments) {
950
+ for (const [key, action] of Object.entries(inst.actions)) {
951
+ const constant = (value) => {
952
+ if ("literal" in value)
953
+ return value.literal;
954
+ const f = inst.fields.find((f) => `self.${f.name}` === value.field);
955
+ return f && "value" in f ? f.value : undefined;
956
+ };
957
+ if (action.requires.some((r) => {
958
+ if (r.kind !== "compare")
959
+ return false;
960
+ const left = constant(r.left), right = constant(r.right);
961
+ if (left === undefined || right === undefined)
962
+ return false;
963
+ return r.operator === "=="
964
+ ? left !== right
965
+ : r.operator === "!="
966
+ ? left === right
967
+ : false;
968
+ })) {
969
+ delete inst.actions[key];
970
+ delete inst.lifecycle.transitions[key];
971
+ }
972
+ }
973
+ const reachable = new Set([inst.lifecycle.initial]);
974
+ for (let n = 0; n < inst.lifecycle.states.length; n++)
975
+ for (const edge of Object.values(inst.lifecycle.transitions))
976
+ if (edge.from.some((state) => reachable.has(state)))
977
+ reachable.add(edge.to);
978
+ for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
979
+ edge.from = edge.from.filter((state) => reachable.has(state));
980
+ if (!edge.from.length) {
981
+ delete inst.actions[key];
982
+ delete inst.lifecycle.transitions[key];
983
+ }
984
+ }
985
+ eliminatedStates.set(inst.id, new Set(inst.lifecycle.states.filter((state) => !reachable.has(state))));
986
+ inst.lifecycle.states = inst.lifecycle.states.filter((state) => reachable.has(state));
987
+ inst.actionOrder = inst.actionOrder.filter((key) => inst.actions[key]);
988
+ }
989
+ for (const inst of document.instruments)
990
+ for (const action of Object.values(inst.actions)) {
991
+ for (const requirement of action.requires) {
992
+ if (requirement.kind !== "state")
993
+ continue;
994
+ const field = inst.fields.find((f) => `self.${f.name}` === requirement.reference);
995
+ if (field?.type === "ref")
996
+ requirement.states = requirement.states.filter((state) => !(typeof field.target === "string"
997
+ ? [field.target]
998
+ : field.target).some((id) => eliminatedStates.get(id)?.has(state)));
999
+ }
1000
+ }
1001
+ // Approval tunables instantiate the same library record as an explicit decision.
1002
+ if (implicitDecisions.size) {
1003
+ const content = (options.standardLibrary ?? bundledStandardLibrary).source("approvals");
1004
+ const parsed = content ? parseProgram(content) : undefined;
1005
+ const template = parsed?.program.decls.find((decl) => decl.kind === "instrument" && decl.name === "decision");
1006
+ if (!template ||
1007
+ template.kind !== "instrument" ||
1008
+ parsed?.diagnostics.length)
1009
+ fail(program, "implicit decisions need the approvals header", "provide the standard approvals header");
1010
+ for (const [id, decision] of implicitDecisions) {
1011
+ if (!document.instruments.find((inst) => inst.id === decision.target)
1012
+ ?.actions[decision.action])
1013
+ continue;
1014
+ const existing = document.instruments.some((inst) => Object.values(inst.actions).some((action) => action.approval?.action === decision.action &&
1015
+ action.approval.party === decision.party &&
1016
+ inst.fields.some((field) => field.type === "ref" &&
1017
+ field.target === decision.target &&
1018
+ `self.${field.name}` === action.approval?.target)));
1019
+ if (existing)
1020
+ continue;
1021
+ if (document.instruments.some((inst) => inst.id === id))
1022
+ fail({ span: decision.origin }, `implicit decision name ${id} is already used`, "rename the conflicting object");
1023
+ addInstrument(template, id, {
1024
+ kind: "block",
1025
+ span: decision.origin,
1026
+ entries: Object.entries({
1027
+ for: decision.target,
1028
+ approved_by: decision.party,
1029
+ action: decision.action,
1030
+ }).map(([key, value]) => ({
1031
+ key,
1032
+ value: {
1033
+ kind: key === "action" ? "text" : "name",
1034
+ value,
1035
+ span: decision.origin,
1036
+ },
1037
+ span: decision.origin,
1038
+ })),
1039
+ }, decision.origin);
1040
+ }
1041
+ }
1042
+ // Material approval fields are inferred from the selected action's typed input.
1043
+ for (const inst of document.instruments)
1044
+ for (const action of Object.values(inst.actions)) {
1045
+ if (!materialApprovals.has(action) || !action.approval)
1046
+ continue;
1047
+ const targetField = inst.fields.find((f) => `self.${f.name}` === action.approval.target);
1048
+ if (targetField?.type !== "ref")
1049
+ continue;
1050
+ const target = document.instruments.find((i) => i.id === targetField.target)?.actions[action.approval.action];
1051
+ if (!target)
1052
+ continue;
1053
+ for (const field of target.input) {
1054
+ const name = `material_${field.name}`;
1055
+ if (!inst.fields.some((f) => f.name === name))
1056
+ inst.fields.push({ ...field, name });
1057
+ action.approval.input[field.name] = { field: `self.${name}` };
1058
+ }
1059
+ }
1060
+ const changed = new Set();
1061
+ for (const decl of program.decls)
1062
+ if (decl.kind === "hide" || decl.kind === "expose") {
1063
+ const parts = decl.target.split(".");
1064
+ const key = parts.pop();
1065
+ const id = parts.join("_");
1066
+ const action = document.instruments.find((i) => i.id === id)?.actions[key ?? ""];
1067
+ if (!action || changed.has(decl.target))
1068
+ fail(decl, `unknown or repeated action ${decl.target}`, "name one declared object.action once");
1069
+ changed.add(decl.target);
1070
+ if (decl.kind === "hide")
1071
+ delete action.publicAction;
1072
+ else if (automatic(action.actor))
1073
+ fail(decl, `${decl.target} runs on the clock or its parent, not a caller`, "expose only caller actions");
1074
+ else
1075
+ action.publicAction = decl.name;
1076
+ }
1077
+ const usedParties = new Set();
1078
+ const collectParties = (value) => {
1079
+ if (typeof value === "string") {
1080
+ usedParties.add(value);
1081
+ if (value.startsWith("party."))
1082
+ usedParties.add(value.split(".")[1]);
1083
+ }
1084
+ else if (Array.isArray(value))
1085
+ value.forEach(collectParties);
1086
+ else if (value && typeof value === "object")
1087
+ Object.values(value).forEach(collectParties);
1088
+ };
1089
+ collectParties(document.instruments);
1090
+ for (const party of ["programFines", "programCosts"])
1091
+ if (!usedParties.has(party) && !names.has(party))
1092
+ delete document.parties[party];
1093
+ const validated = validateUdl(document);
1094
+ if (!validated.ok)
1095
+ return {
1096
+ verdict: "invalid",
1097
+ diagnostics: validated.issues.map((i) => {
1098
+ const origin = [...origins]
1099
+ .reverse()
1100
+ .find((o) => i.path.startsWith(o.path));
1101
+ return diagnostic({
1102
+ code: "HSX1601",
1103
+ message: `${i.path}: ${i.message}`,
1104
+ fix: i.fix,
1105
+ span: origin?.span ?? program.span,
1106
+ }, "lower");
1107
+ }),
1108
+ };
1109
+ return {
1110
+ verdict: "valid",
1111
+ diagnostics: [],
1112
+ artifacts: {
1113
+ document: validated.value,
1114
+ costManifest: buildUdlCostManifest(validated.value),
1115
+ originMap: origins,
1116
+ },
1117
+ };
1118
+ }
1119
+ catch (error) {
1120
+ if (error instanceof CompileFailure)
1121
+ return {
1122
+ verdict: "invalid",
1123
+ diagnostics: [diagnostic(error.diagnostic, "check")],
1124
+ };
1125
+ throw error;
159
1126
  }
160
- offsets[source.length] = bytes;
161
- return offsets;
162
1127
  }
163
1128
  //# sourceMappingURL=compile.js.map