@hyperscale0/hsx 1.0.0 → 1.0.123

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 (316) hide show
  1. package/CHANGELOG.md +5 -256
  2. package/LICENSE.md +356 -0
  3. package/NOTICE.md +8 -0
  4. package/README.md +44 -44
  5. package/bin/hsx.ts +0 -7
  6. package/dist/bin/hsx.d.ts +0 -4
  7. package/dist/bin/hsx.d.ts.map +1 -1
  8. package/dist/bin/hsx.js +0 -6
  9. package/dist/bin/hsx.js.map +1 -1
  10. package/dist/src/ast.d.ts +113 -308
  11. package/dist/src/ast.d.ts.map +1 -1
  12. package/dist/src/ast.js +7 -71
  13. package/dist/src/ast.js.map +1 -1
  14. package/dist/src/attachment-economics.d.ts +5 -0
  15. package/dist/src/attachment-economics.d.ts.map +1 -0
  16. package/dist/src/attachment-economics.js +55 -0
  17. package/dist/src/attachment-economics.js.map +1 -0
  18. package/dist/src/binding-contract.d.ts +22 -0
  19. package/dist/src/binding-contract.d.ts.map +1 -0
  20. package/dist/src/binding-contract.js +74 -0
  21. package/dist/src/binding-contract.js.map +1 -0
  22. package/dist/src/cli.d.ts +4 -19
  23. package/dist/src/cli.d.ts.map +1 -1
  24. package/dist/src/cli.js +89 -276
  25. package/dist/src/cli.js.map +1 -1
  26. package/dist/src/compile.d.ts +34 -66
  27. package/dist/src/compile.d.ts.map +1 -1
  28. package/dist/src/compile.js +2949 -117
  29. package/dist/src/compile.js.map +1 -1
  30. package/dist/src/cost.d.ts +8 -75
  31. package/dist/src/cost.d.ts.map +1 -1
  32. package/dist/src/cost.js +54 -364
  33. package/dist/src/cost.js.map +1 -1
  34. package/dist/src/diagnostics.d.ts +14 -9
  35. package/dist/src/diagnostics.d.ts.map +1 -1
  36. package/dist/src/diagnostics.js +39 -416
  37. package/dist/src/diagnostics.js.map +1 -1
  38. package/dist/src/examples-bundle.d.ts +3 -0
  39. package/dist/src/examples-bundle.d.ts.map +1 -0
  40. package/dist/src/examples-bundle.js +18 -0
  41. package/dist/src/examples-bundle.js.map +1 -0
  42. package/dist/src/examples.d.ts +10 -0
  43. package/dist/src/examples.d.ts.map +1 -0
  44. package/dist/src/examples.js +2 -0
  45. package/dist/src/examples.js.map +1 -0
  46. package/dist/src/format.d.ts +8 -7
  47. package/dist/src/format.d.ts.map +1 -1
  48. package/dist/src/format.js +9 -175
  49. package/dist/src/format.js.map +1 -1
  50. package/dist/src/header-source.d.ts +3 -0
  51. package/dist/src/header-source.d.ts.map +1 -0
  52. package/dist/src/header-source.js +41 -0
  53. package/dist/src/header-source.js.map +1 -0
  54. package/dist/src/headers.d.ts +64 -0
  55. package/dist/src/headers.d.ts.map +1 -0
  56. package/dist/src/headers.js +167 -0
  57. package/dist/src/headers.js.map +1 -0
  58. package/dist/src/index.d.ts +7 -11
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/index.js +4 -9
  61. package/dist/src/index.js.map +1 -1
  62. package/dist/src/keywords.d.ts +56 -0
  63. package/dist/src/keywords.d.ts.map +1 -0
  64. package/dist/src/keywords.js +133 -0
  65. package/dist/src/keywords.js.map +1 -0
  66. package/dist/src/language.d.ts +26 -0
  67. package/dist/src/language.d.ts.map +1 -0
  68. package/dist/src/language.js +440 -0
  69. package/dist/src/language.js.map +1 -0
  70. package/dist/src/lex.d.ts +13 -28
  71. package/dist/src/lex.d.ts.map +1 -1
  72. package/dist/src/lex.js +119 -145
  73. package/dist/src/lex.js.map +1 -1
  74. package/dist/src/parse.d.ts +4 -17
  75. package/dist/src/parse.d.ts.map +1 -1
  76. package/dist/src/parse.js +484 -1036
  77. package/dist/src/parse.js.map +1 -1
  78. package/dist/src/std-bundle.d.ts.map +1 -1
  79. package/dist/src/std-bundle.js +11 -84
  80. package/dist/src/std-bundle.js.map +1 -1
  81. package/dist/src/std-library.d.ts +1 -1
  82. package/dist/src/std-library.d.ts.map +1 -1
  83. package/dist/src/std-library.js +1 -5
  84. package/dist/src/std-library.js.map +1 -1
  85. package/dist/src/tunables.d.ts +7 -0
  86. package/dist/src/tunables.d.ts.map +1 -0
  87. package/dist/src/tunables.js +27 -0
  88. package/dist/src/tunables.js.map +1 -0
  89. package/docs/README.md +528 -26
  90. package/docs/assets/brand/hsx-horizontal-white.svg +1 -0
  91. package/docs/assets/brand/hsx-horizontal.svg +1 -0
  92. package/docs/assets/brand/hsx-stacked-white.svg +1 -0
  93. package/docs/assets/brand/hsx-stacked.svg +1 -0
  94. package/docs/assets/brand/manifest.json +30 -0
  95. package/docs/assets/hsx.svg +11 -0
  96. package/examples/cards.hsx +34 -0
  97. package/examples/collections.hsx +37 -0
  98. package/examples/cost-table.json +633 -385
  99. package/examples/escrow.hsx +24 -0
  100. package/examples/financing.hsx +32 -0
  101. package/examples/insurance.hsx +30 -0
  102. package/examples/lending.hsx +57 -0
  103. package/examples/library.hsx +15 -0
  104. package/examples/marketplace.hsx +32 -0
  105. package/examples/money.hsx +19 -0
  106. package/examples/rental-deposit.hsx +60 -0
  107. package/examples/repair-approval.hsx +44 -0
  108. package/examples/reporting.hsx +28 -0
  109. package/examples/savings.hsx +21 -0
  110. package/examples/serviced.hsx +25 -0
  111. package/examples/wallet.hsx +24 -0
  112. package/package.json +25 -23
  113. package/src/ast.ts +83 -440
  114. package/src/attachment-economics.ts +103 -0
  115. package/src/binding-contract.ts +104 -0
  116. package/src/cli.ts +95 -325
  117. package/src/compile.ts +4063 -209
  118. package/src/cost.ts +71 -574
  119. package/src/diagnostics.ts +57 -437
  120. package/src/examples-bundle.ts +19 -0
  121. package/src/examples.ts +9 -0
  122. package/src/format.ts +12 -256
  123. package/src/header-source.ts +54 -0
  124. package/src/headers.ts +195 -0
  125. package/src/index.ts +18 -33
  126. package/src/keywords.ts +134 -0
  127. package/src/language.ts +629 -0
  128. package/src/lex.ts +131 -192
  129. package/src/parse.ts +489 -1195
  130. package/src/std-bundle.ts +12 -88
  131. package/src/std-library.ts +2 -7
  132. package/src/tunables.ts +36 -0
  133. package/std/cards.hsx +85 -0
  134. package/std/collections.hsx +90 -0
  135. package/std/escrow.hsx +73 -0
  136. package/std/financing.hsx +683 -0
  137. package/std/insurance.hsx +89 -0
  138. package/std/lending.hsx +138 -0
  139. package/std/marketplace.hsx +123 -0
  140. package/std/money.hsx +173 -0
  141. package/std/reporting.hsx +753 -0
  142. package/std/savings.hsx +68 -0
  143. package/std/wallet.hsx +57 -0
  144. package/LICENSE +0 -661
  145. package/LICENSING.md +0 -52
  146. package/TRADEMARKS.md +0 -35
  147. package/dist/src/emit.d.ts +0 -34
  148. package/dist/src/emit.d.ts.map +0 -1
  149. package/dist/src/emit.js +0 -137
  150. package/dist/src/emit.js.map +0 -1
  151. package/dist/src/entry-overrides.d.ts +0 -58
  152. package/dist/src/entry-overrides.d.ts.map +0 -1
  153. package/dist/src/entry-overrides.js +0 -284
  154. package/dist/src/entry-overrides.js.map +0 -1
  155. package/dist/src/ir.d.ts +0 -66
  156. package/dist/src/ir.d.ts.map +0 -1
  157. package/dist/src/ir.js +0 -15
  158. package/dist/src/ir.js.map +0 -1
  159. package/dist/src/limits.d.ts +0 -21
  160. package/dist/src/limits.d.ts.map +0 -1
  161. package/dist/src/limits.js +0 -21
  162. package/dist/src/limits.js.map +0 -1
  163. package/dist/src/lsp/server.d.ts +0 -32
  164. package/dist/src/lsp/server.d.ts.map +0 -1
  165. package/dist/src/lsp/server.js +0 -391
  166. package/dist/src/lsp/server.js.map +0 -1
  167. package/dist/src/modules.d.ts +0 -38
  168. package/dist/src/modules.d.ts.map +0 -1
  169. package/dist/src/modules.js +0 -368
  170. package/dist/src/modules.js.map +0 -1
  171. package/dist/src/typecheck.d.ts +0 -8
  172. package/dist/src/typecheck.d.ts.map +0 -1
  173. package/dist/src/typecheck.js +0 -2958
  174. package/dist/src/typecheck.js.map +0 -1
  175. package/dist/src/version.d.ts +0 -5
  176. package/dist/src/version.d.ts.map +0 -1
  177. package/dist/src/version.js +0 -5
  178. package/dist/src/version.js.map +0 -1
  179. package/docs/guide/01-first-program.md +0 -24
  180. package/docs/guide/02-money.md +0 -25
  181. package/docs/guide/03-instruments.md +0 -22
  182. package/docs/guide/04-lifecycles.md +0 -25
  183. package/docs/guide/05-fees-and-splits.md +0 -45
  184. package/docs/guide/06-schedules.md +0 -22
  185. package/docs/guide/07-composition.md +0 -46
  186. package/docs/guide/08-writing-a-module.md +0 -26
  187. package/docs/guide/09-cost.md +0 -24
  188. package/docs/guide/10-diagnostics.md +0 -27
  189. package/docs/llms-full.txt +0 -1739
  190. package/docs/llms.txt +0 -45
  191. package/docs/playground.md +0 -54
  192. package/docs/reference/cli.md +0 -35
  193. package/docs/reference/diagnostics.md +0 -619
  194. package/docs/reference/grammar.md +0 -39
  195. package/docs/reference/std/advance.md +0 -35
  196. package/docs/reference/std/cancellable_booking.md +0 -35
  197. package/docs/reference/std/captured_payment.md +0 -43
  198. package/docs/reference/std/conditional_disbursement.md +0 -33
  199. package/docs/reference/std/credit_facility.md +0 -35
  200. package/docs/reference/std/held_payment.md +0 -64
  201. package/docs/reference/std/instant_transfer.md +0 -30
  202. package/docs/reference/std/metered.md +0 -27
  203. package/docs/reference/std/pooled_split.md +0 -26
  204. package/docs/reference/std/premium_forward.md +0 -39
  205. package/docs/reference/std/reconciled_payout.md +0 -33
  206. package/docs/reference/std/recurring_collection.md +0 -25
  207. package/docs/reference/std/rotating_pool.md +0 -42
  208. package/docs/reference/std/scheduled.md +0 -51
  209. package/docs/reference/std/security_deposit.md +0 -46
  210. package/docs/reference/std/settlement_batch.md +0 -42
  211. package/docs/reference/std/swap.md +0 -46
  212. package/docs/reference/std/threshold_pool.md +0 -47
  213. package/docs/reference/std/weighted_distribution.md +0 -39
  214. package/docs/reference/types.md +0 -20
  215. package/docs/reference/udl-output.md +0 -13
  216. package/docs/sessions/2026-09-02-two-instruments.hsx +0 -21
  217. package/docs/sessions/2026-09-02-two-instruments.md +0 -41
  218. package/examples/01-first-program/README.md +0 -45
  219. package/examples/01-first-program/tip-jar.hsx +0 -17
  220. package/examples/02-imports-and-archetypes/README.md +0 -96
  221. package/examples/02-imports-and-archetypes/photo-booth.hsx +0 -37
  222. package/examples/03-diagnostics/README.md +0 -68
  223. package/examples/03-diagnostics/corner-shop-fixed.hsx +0 -22
  224. package/examples/03-diagnostics/corner-shop.hsx +0 -19
  225. package/examples/04-complete-product/README.md +0 -84
  226. package/examples/04-complete-product/study-hall.hsx +0 -67
  227. package/examples/05-watch-club/README.md +0 -9
  228. package/examples/05-watch-club/watch-club.hsx +0 -124
  229. package/examples/README.md +0 -18
  230. package/examples/advance/README.md +0 -3
  231. package/examples/advance/advance.hsx +0 -13
  232. package/examples/advance/advance.udl +0 -339
  233. package/examples/cancellable_booking/README.md +0 -3
  234. package/examples/cancellable_booking/cancellable_booking.hsx +0 -14
  235. package/examples/cancellable_booking/cancellable_booking.udl +0 -391
  236. package/examples/captured_payment/README.md +0 -3
  237. package/examples/captured_payment/captured_payment.hsx +0 -21
  238. package/examples/captured_payment/captured_payment.udl +0 -677
  239. package/examples/conditional_disbursement/README.md +0 -3
  240. package/examples/conditional_disbursement/conditional_disbursement.hsx +0 -17
  241. package/examples/conditional_disbursement/conditional_disbursement.udl +0 -412
  242. package/examples/credit_facility/README.md +0 -3
  243. package/examples/credit_facility/credit_facility.hsx +0 -27
  244. package/examples/credit_facility/credit_facility.udl +0 -1373
  245. package/examples/held_payment/README.md +0 -3
  246. package/examples/held_payment/held_payment.hsx +0 -13
  247. package/examples/held_payment/held_payment.udl +0 -507
  248. package/examples/instant_transfer/README.md +0 -3
  249. package/examples/instant_transfer/instant_transfer.hsx +0 -16
  250. package/examples/instant_transfer/instant_transfer.udl +0 -364
  251. package/examples/metered/README.md +0 -3
  252. package/examples/metered/metered.hsx +0 -13
  253. package/examples/metered/metered.udl +0 -247
  254. package/examples/pooled_split/README.md +0 -3
  255. package/examples/pooled_split/pooled_split.hsx +0 -15
  256. package/examples/pooled_split/pooled_split.udl +0 -425
  257. package/examples/premium_forward/README.md +0 -3
  258. package/examples/premium_forward/premium_forward.hsx +0 -19
  259. package/examples/premium_forward/premium_forward.udl +0 -594
  260. package/examples/reconciled_payout/README.md +0 -3
  261. package/examples/reconciled_payout/reconciled_payout.hsx +0 -13
  262. package/examples/reconciled_payout/reconciled_payout.udl +0 -303
  263. package/examples/recurring_collection/README.md +0 -3
  264. package/examples/recurring_collection/recurring_collection.hsx +0 -21
  265. package/examples/recurring_collection/recurring_collection.udl +0 -1130
  266. package/examples/rotating_pool/README.md +0 -3
  267. package/examples/rotating_pool/rotating_pool.hsx +0 -18
  268. package/examples/rotating_pool/rotating_pool.udl +0 -4352
  269. package/examples/scheduled/README.md +0 -3
  270. package/examples/scheduled/scheduled.hsx +0 -12
  271. package/examples/scheduled/scheduled.udl +0 -335
  272. package/examples/security_deposit/README.md +0 -3
  273. package/examples/security_deposit/security_deposit.hsx +0 -21
  274. package/examples/security_deposit/security_deposit.udl +0 -293
  275. package/examples/settlement_batch/README.md +0 -3
  276. package/examples/settlement_batch/settlement_batch.hsx +0 -19
  277. package/examples/settlement_batch/settlement_batch.udl +0 -753
  278. package/examples/swap/README.md +0 -3
  279. package/examples/swap/swap.hsx +0 -19
  280. package/examples/swap/swap.udl +0 -876
  281. package/examples/threshold_pool/README.md +0 -3
  282. package/examples/threshold_pool/threshold_pool.hsx +0 -16
  283. package/examples/threshold_pool/threshold_pool.udl +0 -667
  284. package/examples/weighted_distribution/README.md +0 -3
  285. package/examples/weighted_distribution/weighted_distribution.hsx +0 -20
  286. package/examples/weighted_distribution/weighted_distribution.udl +0 -337
  287. package/skills/hsx/SKILL.md +0 -483
  288. package/src/emit.ts +0 -201
  289. package/src/entry-overrides.ts +0 -428
  290. package/src/ir.ts +0 -99
  291. package/src/limits.ts +0 -20
  292. package/src/lsp/server.ts +0 -460
  293. package/src/modules.ts +0 -446
  294. package/src/typecheck.ts +0 -3876
  295. package/src/version.ts +0 -5
  296. package/std/SEMANTICS.md +0 -130
  297. package/std/settlements/advance.hsx +0 -177
  298. package/std/settlements/cancellable_booking.hsx +0 -87
  299. package/std/settlements/captured_payment.hsx +0 -144
  300. package/std/settlements/conditional_disbursement.hsx +0 -185
  301. package/std/settlements/credit_facility.hsx +0 -167
  302. package/std/settlements/held_payment.hsx +0 -578
  303. package/std/settlements/index.hsx +0 -3
  304. package/std/settlements/instant_transfer.hsx +0 -190
  305. package/std/settlements/metered.hsx +0 -67
  306. package/std/settlements/pooled_split.hsx +0 -68
  307. package/std/settlements/premium_forward.hsx +0 -257
  308. package/std/settlements/reconciled_payout.hsx +0 -77
  309. package/std/settlements/recurring_collection.hsx +0 -27
  310. package/std/settlements/rotating_pool.hsx +0 -252
  311. package/std/settlements/scheduled.hsx +0 -641
  312. package/std/settlements/security_deposit.hsx +0 -166
  313. package/std/settlements/settlement_batch.hsx +0 -122
  314. package/std/settlements/swap.hsx +0 -1114
  315. package/std/settlements/threshold_pool.hsx +0 -221
  316. package/std/settlements/weighted_distribution.hsx +0 -141
@@ -1,129 +1,2961 @@
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 { CompileFailure, didYouMean, fail, failWithCode, } from "./diagnostics.js";
2
+ import { hash as sha256 } from "fast-sha256";
3
+ import { buildUdlCostManifest } from "./cost.js";
4
+ import { validateUdl, resolveField, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, UDL_FORMAT_VERSION, udlObjectFieldSchema, udlInstrumentSchema, } from "@hyperscale0/udl";
5
+ import { bindingDependencies, parameterDiagnostics, BindingContractError, } from "./binding-contract.js";
6
+ import { tunableBounds } from "./tunables.js";
17
7
  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 ? { file: origin.moduleName } : {}),
31
- line: position.line,
32
- message,
33
- span: { ...span },
34
- ...(details ? details : {}),
35
- severity,
36
- stage,
37
- });
8
+ import { applyAttachmentEconomics } from "./attachment-economics.js";
9
+ import { parseHeader } from "./header-source.js";
10
+ import { lineColAt, } from "./ast.js";
11
+ import { bundledStandardLibrary } from "./std-library.js";
12
+ function fieldType(row) {
13
+ const type = row.value.kind === "default" ? row.value.type : row.value;
14
+ if (type.kind !== "call" || type.name !== "sensitive")
15
+ return { type, sensitive: false };
16
+ if (type.args.length !== 1)
17
+ fail(type, "sensitive needs one field type", "write sensitive(text)");
18
+ return { type: type.args[0], sensitive: true };
19
+ }
20
+ function lowerFieldShape(row, resolveExpr = (e) => e) {
21
+ const { type: t, sensitive: isSensitive } = fieldType(row);
22
+ if (t.kind === "block") {
23
+ const b = entries(t);
24
+ if (b.has("family") || b.has("target") || b.has("instrument")) {
25
+ const tgt = b.get("instrument") ?? b.get("target");
26
+ let target;
27
+ if (tgt) {
28
+ const resolved = resolveExpr(tgt);
29
+ if (resolved.kind === "list") {
30
+ const items = resolved.items.map((i) => text(resolveExpr(i)));
31
+ target = items.length === 1 ? items[0] : items;
32
+ }
33
+ else {
34
+ target = text(resolved);
35
+ }
36
+ }
37
+ return {
38
+ name: row.key,
39
+ type: "ref",
40
+ targetKind: "instrument",
41
+ ...(target !== undefined ? { target } : {}),
42
+ ...(isSensitive ? { sensitive: true } : {}),
43
+ };
44
+ }
45
+ }
46
+ const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
47
+ const f = {
48
+ name: row.key,
49
+ type,
50
+ ...(t.kind === "type" && t.optional ? { optional: true } : {}),
51
+ ...(isSensitive ? { sensitive: true } : {}),
38
52
  };
39
- const parsed = parseProgram(source);
40
- for (const diagnostic of parsed.diagnostics) {
41
- at("parse", "error", diagnostic.message, diagnostic.span, diagnostic.code && diagnostic.fix
42
- ? { code: diagnostic.code, fix: diagnostic.fix }
43
- : undefined);
44
- }
45
- if (parsed.diagnostics.length > 0) {
46
- return { diagnostics, verdict: "invalid" };
47
- }
48
- const resolved = resolveProgramModules(parsed.program, options);
49
- if (!resolved.ok) {
50
- for (const issue of resolved.issues) {
51
- at("bind", "error", issue.message, issue.span, {
52
- code: issue.code,
53
- fix: issue.fix,
54
- });
53
+ if (type === "enum" && t.kind === "call") {
54
+ f.values = t.args.map(text);
55
+ }
56
+ if (type === "text" && t.kind === "call") {
57
+ if (t.args.length < 2 || t.args.length > 3) {
58
+ fail(t, "bounded text needs length bounds and an optional pattern", "write text(1, 80)");
59
+ }
60
+ f.minLength = literal(resolveExpr(t.args[0]));
61
+ f.maxLength = literal(resolveExpr(t.args[1]));
62
+ if (t.args[2])
63
+ f.pattern = literal(resolveExpr(t.args[2]));
64
+ }
65
+ if (["integer", "money"].includes(type) && t.kind === "call") {
66
+ if (t.args.length !== 2) {
67
+ fail(t, "bounded fields need a minimum and maximum", "write integer(1, 12) or money(0 SAR, 100 SAR)");
68
+ }
69
+ f.minimum = literal(resolveExpr(t.args[0]));
70
+ f.maximum = literal(resolveExpr(t.args[1]));
71
+ }
72
+ if (t.kind === "call" &&
73
+ !["enum", "text", "integer", "money", "list", "account"].includes(type))
74
+ fail(t, `unknown field constructor ${type}`, "use a field type without arguments");
75
+ if (type === "list" && t.kind === "call") {
76
+ if (t.args.length < 1 || t.args.length > 2)
77
+ fail(t, "list needs an item type and optional bound", "write list(text, 12)");
78
+ const item = t.args[0];
79
+ f.item = item.kind === "type" ? item.name : text(item);
80
+ if (item.kind === "type" && item.name === "ref") {
81
+ f.target = item.target;
82
+ f.targetKind = "object";
55
83
  }
56
- return { diagnostics, verdict: "invalid" };
57
- }
58
- const checked = checkGeneralProgram(resolved.program, options.publishedCatalog
59
- ? { publishedCatalog: options.publishedCatalog }
60
- : {});
61
- for (const diagnostic of checked.diagnostics) {
62
- const moduleOrigin = resolved.origins.get(diagnostic.span);
63
- at(diagnostic.code === "HSX1001" ? "bind" : "typecheck", diagnostic.severity, diagnostic.message, diagnostic.span, { code: diagnostic.code, fix: diagnostic.fix }, moduleOrigin);
64
- }
65
- if (!checked.program)
66
- return { diagnostics, verdict: "invalid" };
67
- const cost = buildCostManifest(checked.program, options.costTable, options.composesCatalogBlueprint ?? false);
68
- if (!cost.ok) {
69
- for (const diagnostic of cost.diagnostics) {
70
- at("typecheck", diagnostic.severity, diagnostic.message, diagnostic.span, { code: diagnostic.code, fix: diagnostic.fix });
71
- }
72
- return { diagnostics, verdict: "invalid" };
73
- }
74
- const lowered = lowerGeneralProgram(checked.program);
75
- if (!lowered.ok) {
76
- for (const issue of lowered.issues) {
77
- at("lower", "error", issue.message, issue.span, {
78
- code: issue.code,
79
- fix: issue.fix,
80
- path: issue.path,
81
- ...(issue.udlCode ? { udlCode: issue.udlCode } : {}),
84
+ f.maxItems = t.args[1] ? literal(resolveExpr(t.args[1])) : 366;
85
+ }
86
+ if (type === "list" && t.kind === "type") {
87
+ f.item = t.target;
88
+ f.maxItems = 366;
89
+ }
90
+ if (type === "ref") {
91
+ const target = t.kind === "type" ? t.target : f.target;
92
+ if (!target && t.kind !== "block") {
93
+ fail(row, "reference needs a target", "write ref<object>");
94
+ }
95
+ f.targetKind = f.targetKind ?? "object";
96
+ if (target)
97
+ f.target = target;
98
+ }
99
+ return f;
100
+ }
101
+ function lowerObjectField(row, resolveExpr = (e) => e) {
102
+ const f = lowerFieldShape(row, resolveExpr);
103
+ const constant = row.value.kind === "default" ? resolveExpr(row.value.value) : undefined;
104
+ if (constant && !(constant.kind === "name" && constant.value === "runtime")) {
105
+ f.value =
106
+ f.type === "enum" && constant.kind === "name"
107
+ ? constant.value
108
+ : literal(constant, String(f.type));
109
+ }
110
+ const result = udlObjectFieldSchema.safeParse(f);
111
+ if (!result.success)
112
+ fail(row, result.error.message, "use a UDL object field type and its constraints");
113
+ return result.data;
114
+ }
115
+ function canonicalJson(value) {
116
+ if (value === null || typeof value !== "object") {
117
+ return JSON.stringify(value);
118
+ }
119
+ if (Array.isArray(value)) {
120
+ return `[${value.map((item) => (item === undefined ? "null" : canonicalJson(item))).join(",")}]`;
121
+ }
122
+ const record = value;
123
+ const keys = Object.keys(record)
124
+ .filter((k) => record[k] !== undefined)
125
+ .sort();
126
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
127
+ }
128
+ const emptyBlock = {
129
+ kind: "block",
130
+ entries: [],
131
+ span: { start: 0, end: 0 },
132
+ };
133
+ const camel = (name) => name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
134
+ /** Clock and parent actors run without a caller, so they have no public name. */
135
+ const automatic = (actor) => actor === "clock" ||
136
+ (actor !== null && typeof actor === "object" && "parent" in actor);
137
+ const title = (name) => name[0].toUpperCase() + name.slice(1).replaceAll("_", " ");
138
+ function entries(block) {
139
+ const map = new Map();
140
+ for (const row of block.entries) {
141
+ const previous = map.get(row.key);
142
+ if (previous) {
143
+ if (![
144
+ "requires",
145
+ "moves",
146
+ "invariants",
147
+ "invoke",
148
+ "calculate",
149
+ "expose",
150
+ ].includes(row.key))
151
+ fail(row, `duplicate ${row.key}`, "keep one value for this name");
152
+ const items = (expr) => expr.kind === "list" ? expr.items : [expr];
153
+ map.set(row.key, {
154
+ kind: "list",
155
+ items: [...items(previous), ...items(row.value)],
156
+ span: row.span,
82
157
  });
83
158
  }
84
- return { diagnostics, verdict: "invalid" };
159
+ else
160
+ map.set(row.key, row.value);
161
+ }
162
+ return map;
163
+ }
164
+ function asBlock(expr) {
165
+ if (!expr)
166
+ return emptyBlock;
167
+ if (expr.kind !== "block")
168
+ fail(expr, "expected a block", "write { name: value }");
169
+ return expr;
170
+ }
171
+ function text(expr) {
172
+ if ("value" in expr && typeof expr.value === "string")
173
+ return expr.value;
174
+ return fail(expr, "expected a name or literal", "write a single name or literal");
175
+ }
176
+ function decimal(raw, scale, expr) {
177
+ const [whole = "", fraction = ""] = raw.split(".");
178
+ if (!/^\d+$/.test(whole) || fraction.length > scale)
179
+ fail(expr, `literal has more than ${scale} decimal places`, `write at most ${scale} decimal places`);
180
+ return (BigInt(whole) * 10n ** BigInt(scale) +
181
+ BigInt(fraction.padEnd(scale, "0") || "0")).toString();
182
+ }
183
+ function literal(expr, expectedType) {
184
+ if (expr.kind === "text" && expectedType === "duration")
185
+ return literal({ ...expr, kind: "duration" });
186
+ if (expr.kind === "money") {
187
+ const [raw, currency] = expr.value.split(" ");
188
+ if (currency !== "SAR")
189
+ fail(expr, `currency ${currency} differs from SAR`, "write this amount in SAR");
190
+ const result = decimal(raw, 2, expr);
191
+ if (result.length > 18)
192
+ fail(expr, "amount exceeds 18 minor-unit digits", "write a smaller amount");
193
+ return result;
194
+ }
195
+ if (expr.kind === "percent") {
196
+ const n = Number(decimal(expr.value, 2, expr));
197
+ if (n > 10000)
198
+ fail(expr, "percentage exceeds 100%", "write a percentage from 0% to 100%");
199
+ return n;
200
+ }
201
+ if (expr.kind === "number") {
202
+ const n = Number(expr.value);
203
+ if (!Number.isSafeInteger(n))
204
+ fail(expr, "expected a safe integer", "write an integer, a percentage, or an amount with SAR");
205
+ return n;
206
+ }
207
+ if (expr.kind === "duration" ||
208
+ (expr.kind === "name" && /^P(?:\d|T)/.test(expr.value))) {
209
+ const short = /^(\d+)(ms|s|m|h|d|w)$/.exec(expr.value);
210
+ const units = {
211
+ ms: 1,
212
+ s: 1000,
213
+ m: 60000,
214
+ h: 3600000,
215
+ d: 86400000,
216
+ w: 604800000,
217
+ };
218
+ const iso = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(expr.value);
219
+ const n = short
220
+ ? Number(short[1]) * units[short[2]]
221
+ : iso
222
+ ? Number(iso[1] ?? 0) * units.w +
223
+ Number(iso[2] ?? 0) * units.d +
224
+ Number(iso[3] ?? 0) * units.h +
225
+ Number(iso[4] ?? 0) * units.m +
226
+ Number(iso[5] ?? 0) * units.s
227
+ : NaN;
228
+ if (!Number.isSafeInteger(n) || n <= 0)
229
+ fail(expr, "invalid duration", "write a positive fixed duration such as 48h or 3d");
230
+ return n;
85
231
  }
86
- const compileOrigin = (entry) => {
87
- const position = lineColIn(lines, entry.span.start);
232
+ if (expr.kind === "date") {
233
+ const raw = expr.value.length === 10 ? expr.value + "T00:00:00Z" : expr.value;
234
+ const n = Date.parse(raw);
235
+ const day = Date.parse(raw.slice(0, 10) + "T00:00:00Z");
236
+ if (!Number.isFinite(n) ||
237
+ !Number.isFinite(day) ||
238
+ new Date(day).toISOString().slice(0, 10) !== raw.slice(0, 10))
239
+ fail(expr, "invalid date", "write a calendar date or a timestamp with an explicit offset");
240
+ return new Date(n).toISOString();
241
+ }
242
+ if (expr.kind === "text")
243
+ return expr.value;
244
+ if (expr.kind === "name" && ["true", "false"].includes(expr.value))
245
+ return expr.value === "true";
246
+ return fail(expr, "expected a literal", "write a typed constant");
247
+ }
248
+ export function compile(source, options = {}) {
249
+ const parsed = parseProgram(source);
250
+ const sources = new Map([["program", source]]);
251
+ const diagnostic = (d, stage) => ({
252
+ ...d,
253
+ ...lineColAt(sources.get(d.source ?? "program") ?? source, d.span.start),
254
+ severity: "error",
255
+ stage,
256
+ });
257
+ // A program instrument lowers once standalone and once per attachment, so
258
+ // one authored mistake can fail each copy at the same place.
259
+ const distinct = (list) => list.filter((d, index) => list.findIndex((other) => other.source === d.source &&
260
+ other.span.start === d.span.start &&
261
+ other.message === d.message) === index);
262
+ if (parsed.diagnostics.length)
88
263
  return {
89
- path: entry.path,
90
- span: {
91
- column: position.column,
92
- end: byteOffsets[entry.span.end] ?? byteOffsets[byteOffsets.length - 1],
93
- line: position.line,
94
- start: byteOffsets[entry.span.start] ?? 0,
95
- },
264
+ verdict: "invalid",
265
+ diagnostics: parsed.diagnostics.map((d) => diagnostic(d, "parse")),
96
266
  };
267
+ const program = parsed.program;
268
+ const bindingDiagnostics = [];
269
+ const adapterTarget = (binding) => {
270
+ const registry = options.adapterRegistry;
271
+ if (!registry || !Object.hasOwn(registry, binding))
272
+ return;
273
+ const target = registry[binding];
274
+ return target &&
275
+ Object.hasOwn(target.adapter.operationMap, target.operation)
276
+ ? target
277
+ : undefined;
97
278
  };
98
- return {
99
- artifacts: {
100
- document: lowered.value.document,
101
- costManifest: cost.manifest,
102
- originMap: lowered.value.originMap.map(compileOrigin),
103
- },
104
- diagnostics,
105
- verdict: diagnostics.length > 0 ? "warning" : "valid",
106
- };
107
- }
108
- function utf8OffsetIndex(source) {
109
- const offsets = new Uint32Array(source.length + 1);
110
- let bytes = 0;
111
- for (let index = 0; index < source.length; index += 1) {
112
- offsets[index] = bytes;
113
- const codePoint = source.codePointAt(index);
114
- if (codePoint > 0xffff) {
115
- offsets[index + 1] = bytes;
116
- bytes += 4;
117
- index += 1;
118
- }
119
- else if (codePoint > 0x7ff)
120
- bytes += 3;
121
- else if (codePoint > 0x7f)
122
- bytes += 2;
123
- else
124
- bytes += 1;
279
+ try {
280
+ if (program.header)
281
+ fail(program, "compile a program, not a header", 'start with program product_name "Title"');
282
+ if (program.currency !== "SAR")
283
+ fail(program, "this release supports SAR", "write currency SAR or omit currency");
284
+ const templates = new Map();
285
+ const declarationSources = new Map();
286
+ const standardOrigins = new Map();
287
+ const requirementOrigins = new Map();
288
+ const used = new Set();
289
+ for (const use of program.decls.filter((d) => d.kind === "use")) {
290
+ if (used.has(use.name))
291
+ fail(use, `header ${use.name} is used twice`, "remove the duplicate use line");
292
+ used.add(use.name);
293
+ const content = (options.standardLibrary ?? bundledStandardLibrary).source(use.name);
294
+ if (!content)
295
+ fail(use, `unknown header ${use.name}`, "choose a published header");
296
+ sources.set(use.name, content);
297
+ const bundled = bundledStandardLibrary.source(use.name);
298
+ const headerDigest = bundled === content
299
+ ? Array.from(sha256(new TextEncoder().encode(content)), (byte) => byte.toString(16).padStart(2, "0")).join("")
300
+ : undefined;
301
+ let header;
302
+ try {
303
+ header = parseHeader(content, use.name);
304
+ }
305
+ catch (error) {
306
+ if (!(error instanceof CompileFailure))
307
+ throw error;
308
+ throw new CompileFailure({
309
+ ...error.diagnostic,
310
+ related: [
311
+ {
312
+ source: "program",
313
+ span: use.span,
314
+ message: `Imported header ${use.name}`,
315
+ },
316
+ ],
317
+ });
318
+ }
319
+ const registerTemplates = (parentDecl, prefix) => {
320
+ if (templates.has(prefix))
321
+ fail(parentDecl, `duplicate instrument ${prefix}`, "give each instrument a distinct name");
322
+ templates.set(prefix, parentDecl);
323
+ declarationSources.set(parentDecl, use.name);
324
+ if (headerDigest) {
325
+ const [, root, ...record] = prefix.split(".");
326
+ standardOrigins.set(parentDecl, {
327
+ key: `${use.name}.${root}`,
328
+ recordPath: record.join("."),
329
+ headerDigest,
330
+ });
331
+ }
332
+ const recs = entries(asBlock(entries(parentDecl.body).get("records")));
333
+ for (const [recName, recBlock] of recs) {
334
+ const recDecl = {
335
+ kind: "instrument",
336
+ name: recName,
337
+ parameters: [],
338
+ body: asBlock(recBlock),
339
+ span: parentDecl.span,
340
+ };
341
+ registerTemplates(recDecl, `${prefix}.${recName}`);
342
+ }
343
+ };
344
+ for (const decl of header.decls)
345
+ if (decl.kind === "instrument") {
346
+ registerTemplates(decl, `${use.name}.${decl.name}`);
347
+ }
348
+ }
349
+ for (const decl of program.decls)
350
+ if (decl.kind === "instrument")
351
+ templates.set(decl.name, decl);
352
+ for (const decl of program.decls) {
353
+ if (decl.kind === "instrument" && decl.parameters.length)
354
+ fail(decl, "This compiler cannot instantiate a parameterized instrument declared inside a program.", "For this version, specialize the instrument with fixed bindings and remove its parameters. Custom reusable headers require a host-supplied library.");
355
+ }
356
+ const document = {
357
+ udl: UDL_FORMAT_VERSION,
358
+ version: 1,
359
+ product: program.name,
360
+ title: program.title,
361
+ currency: "SAR",
362
+ parties: Object.assign(Object.create(null), {
363
+ programOperator: { kind: "business", role: "program_operator" },
364
+ programTax: { kind: "business", role: "tax_payable" },
365
+ }),
366
+ objects: [],
367
+ instruments: [],
368
+ };
369
+ const objects = new Map();
370
+ const assignments = new Map();
371
+ const attachmentSubjects = new Map();
372
+ const names = new Set();
373
+ for (const decl of program.decls) {
374
+ if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
375
+ continue;
376
+ if (names.has(decl.name))
377
+ fail(decl, `duplicate declaration ${decl.name}`, "give this declaration a distinct name");
378
+ names.add(decl.name);
379
+ if (decl.kind === "object") {
380
+ objects.set(decl.name, decl);
381
+ for (const entry of decl.body.entries) {
382
+ const match = /^attach\s+(\w+)\s*=\s*(.+)$/.exec(entry.key);
383
+ if (!match)
384
+ continue;
385
+ const name = `${decl.name}_${match[1]}`;
386
+ assignments.set(name, {
387
+ kind: "assignment",
388
+ name,
389
+ target: match[2],
390
+ body: asBlock(entry.value),
391
+ span: entry.span,
392
+ });
393
+ attachmentSubjects.set(name, decl.name);
394
+ }
395
+ }
396
+ if (decl.kind === "assignment")
397
+ assignments.set(decl.name, decl);
398
+ if (decl.kind === "party") {
399
+ if (subjectPartyRoles.includes(decl.name))
400
+ failWithCode(decl, "party_name_reserved", `${decl.name} is a reserved subject role`, "choose a party name other than owner, actor or operator");
401
+ if (!["person", "business", "staff"].includes(decl.partyKind))
402
+ fail(decl, `unknown party kind ${decl.partyKind}`, "choose person, business, or staff");
403
+ document.parties[decl.name] = {
404
+ kind: decl.partyKind,
405
+ ...(decl.role ? { role: decl.role } : {}),
406
+ };
407
+ }
408
+ }
409
+ const origins = [];
410
+ // Lowered objects lose their spans, so each keeps the expression that
411
+ // produced it for the name check after lowering.
412
+ const authored = new WeakMap();
413
+ const declarations = new WeakMap();
414
+ const exposures = [];
415
+ const refundBindings = new Map();
416
+ const resolveFamily = (rawPath, expr, required = true) => {
417
+ const parts = rawPath.split(".");
418
+ if (parts.length < 2) {
419
+ if (!required)
420
+ return undefined;
421
+ failWithCode(expr, "HSX1001", `invalid family ${rawPath}`, "use module.instrument or module.instrument.record");
422
+ }
423
+ const moduleName = parts[0];
424
+ const exportPath = parts.slice(1).join(".");
425
+ const targetTemplate = templates.get(rawPath);
426
+ if (!targetTemplate) {
427
+ if (!required)
428
+ return undefined;
429
+ failWithCode(expr, "HSX1001", `unknown family declaration ${rawPath}`, "choose a declared standard instrument");
430
+ }
431
+ const topTemplate = templates.get(`${moduleName}.${parts[1]}`);
432
+ if (!topTemplate) {
433
+ if (!required)
434
+ return undefined;
435
+ failWithCode(expr, "HSX1001", `unknown family declaration ${moduleName}.${parts[1]}`, "choose a declared standard instrument");
436
+ }
437
+ const topBody = entries(topTemplate.body);
438
+ let revision;
439
+ if (topBody.has("familyRevision")) {
440
+ const val = literal(topBody.get("familyRevision"));
441
+ if (typeof val === "number")
442
+ revision = val;
443
+ }
444
+ let currentBody = topTemplate.body;
445
+ for (const recName of parts.slice(2)) {
446
+ const recs = entries(asBlock(entries(currentBody).get("records")));
447
+ const child = recs.get(recName);
448
+ if (child) {
449
+ currentBody = asBlock(child);
450
+ const cBody = entries(currentBody);
451
+ if (cBody.has("familyRevision")) {
452
+ const val = literal(cBody.get("familyRevision"));
453
+ if (typeof val === "number")
454
+ revision = val;
455
+ }
456
+ }
457
+ }
458
+ if (revision === undefined) {
459
+ if (!required)
460
+ return undefined;
461
+ failWithCode(expr, "HSX1001", `declaration ${rawPath} has no familyRevision declared`, "declare familyRevision on the standard instrument");
462
+ }
463
+ return { module: moduleName, exportPath, revision };
464
+ };
465
+ const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedEnums = new Map(), attachmentInfo, familyDeclaration, standardOrigin) => {
466
+ const resolveFamilyInstruments = (family, currentInstId, expr) => {
467
+ const found = new Set();
468
+ if (currentInstId && familyDeclaration) {
469
+ if (family.module === familyDeclaration.module &&
470
+ family.exportPath.startsWith(`${familyDeclaration.exportPath}.`)) {
471
+ const sub = family.exportPath
472
+ .slice(familyDeclaration.exportPath.length + 1)
473
+ .replaceAll(".", "_");
474
+ found.add(`${currentInstId}_${sub}`);
475
+ }
476
+ }
477
+ const topDeclName = family.exportPath.split(".")[0];
478
+ const subRecordPath = family.exportPath.includes(".")
479
+ ? family.exportPath.slice(topDeclName.length + 1).replaceAll(".", "_")
480
+ : undefined;
481
+ for (const asgn of assignments.values()) {
482
+ if (asgn.target === `${family.module}.${topDeclName}`) {
483
+ if (subRecordPath) {
484
+ found.add(`${asgn.name}_${subRecordPath}`);
485
+ }
486
+ else {
487
+ found.add(asgn.name);
488
+ }
489
+ }
490
+ }
491
+ for (const inst of document.instruments) {
492
+ if (inst.family &&
493
+ inst.family.module === family.module &&
494
+ inst.family.exportPath === family.exportPath &&
495
+ inst.family.revision === family.revision) {
496
+ found.add(inst.id);
497
+ }
498
+ }
499
+ const result = [...found];
500
+ if (result.length === 0 && expr) {
501
+ failWithCode(expr, "HSX1001", `no instruments found for family ${family.module}.${family.exportPath}`, "declare an attachment matching this family");
502
+ }
503
+ return result;
504
+ };
505
+ const collectChildExportPaths = (parentDecl, suffix) => {
506
+ const recs = entries(asBlock(entries(parentDecl.body).get("records")));
507
+ const matches = [];
508
+ for (const [recName] of recs) {
509
+ if (suffix === recName)
510
+ matches.push(recName);
511
+ }
512
+ for (const [recName, recBlock] of recs) {
513
+ if (suffix.startsWith(`${recName}_`)) {
514
+ const nested = collectChildExportPaths({
515
+ kind: "instrument",
516
+ name: recName,
517
+ parameters: [],
518
+ body: asBlock(recBlock),
519
+ span: parentDecl.span,
520
+ }, suffix.slice(recName.length + 1));
521
+ for (const rest of nested) {
522
+ matches.push(`${recName}.${rest}`);
523
+ }
524
+ }
525
+ }
526
+ return matches;
527
+ };
528
+ const resolveChildExportPath = (parentDecl, suffix) => {
529
+ const matches = collectChildExportPaths(parentDecl, suffix);
530
+ if (matches.length > 1) {
531
+ failWithCode(parentDecl, "HSX1001", `ambiguous child export path suffix '${suffix}': multiple candidates (${matches.join(", ")})`, "rename conflicting records to remove duplicate export path suffixes");
532
+ }
533
+ return matches[0];
534
+ };
535
+ const getInstrumentFamily = (targetId) => {
536
+ const existing = document.instruments.find((i) => i.id === targetId);
537
+ if (existing?.family)
538
+ return existing.family;
539
+ if (targetId.startsWith(`${id}_`) && familyDeclaration) {
540
+ const sub = resolveChildExportPath(decl, targetId.slice(id.length + 1));
541
+ if (sub) {
542
+ const fullExport = `${familyDeclaration.exportPath}.${sub}`;
543
+ return resolveFamily(`${familyDeclaration.module}.${fullExport}`, { span: origin }, false);
544
+ }
545
+ }
546
+ for (const [asgnName, asgn] of assignments) {
547
+ if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
548
+ const tmpl = templates.get(asgn.target);
549
+ if (!tmpl)
550
+ continue;
551
+ const mod = declarationSources.get(tmpl);
552
+ if (!mod || mod === "program")
553
+ continue;
554
+ if (targetId === asgnName) {
555
+ return resolveFamily(asgn.target, asgn, false);
556
+ }
557
+ else {
558
+ const sub = resolveChildExportPath(tmpl, targetId.slice(asgnName.length + 1));
559
+ if (sub) {
560
+ const fullTarget = `${asgn.target}.${sub}`;
561
+ return resolveFamily(fullTarget, asgn, false);
562
+ }
563
+ }
564
+ }
565
+ }
566
+ return undefined;
567
+ };
568
+ const checkTargetFamily = (targetIds, expectedFamily, expr) => {
569
+ const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
570
+ for (const tid of ids) {
571
+ if (typeof tid !== "string")
572
+ fail(expr, "instrument target needs a name", "name a declared instrument or a list of instruments");
573
+ const fam = getInstrumentFamily(tid);
574
+ if (!fam ||
575
+ fam.module !== expectedFamily.module ||
576
+ fam.exportPath !== expectedFamily.exportPath ||
577
+ fam.revision !== expectedFamily.revision) {
578
+ failWithCode(expr, "HSX1001", `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`, "ensure target instrument matches the declared family");
579
+ }
580
+ }
581
+ };
582
+ const enums = new Map(inheritedEnums);
583
+ for (const parameter of decl.parameters) {
584
+ const type = parameter.value.kind === "default"
585
+ ? parameter.value.type
586
+ : parameter.value;
587
+ if (type.kind === "call" && type.name === "enum")
588
+ enums.set(parameter.key, type.args.map(text));
589
+ }
590
+ const supplied = new Map();
591
+ for (const entry of arguments_.entries) {
592
+ if (supplied.has(entry.key))
593
+ fail(entry, `duplicate tunable ${entry.key}`, "supply each parameter once");
594
+ supplied.set(entry.key, entry.value);
595
+ }
596
+ const environment = new Map(inherited);
597
+ const parameterErrors = [];
598
+ for (const param of decl.parameters) {
599
+ const type = param.value.kind === "default" ? param.value.type : param.value;
600
+ const fallback = param.value.kind === "default" ? param.value.value : undefined;
601
+ const typeName = type.kind === "type" || type.kind === "call" ? type.name : text(type);
602
+ const partyParameter = attachmentInfo && typeName === "party";
603
+ const byName = partyParameter &&
604
+ (subjectPartyRoles.includes(param.key) ||
605
+ document.parties[param.key])
606
+ ? { kind: "name", value: param.key, span: origin }
607
+ : undefined;
608
+ const actual = supplied.get(param.key) ??
609
+ byName ??
610
+ (fallback && {
611
+ ...fallback,
612
+ source: declarationSources.get(decl) ?? "program",
613
+ });
614
+ if (!actual) {
615
+ if (type.kind === "type" && type.optional)
616
+ continue;
617
+ parameterErrors.push({
618
+ span: origin,
619
+ code: partyParameter ? "subject_party_unbound" : "HSX1001",
620
+ message: partyParameter
621
+ ? `\`${param.key}\` has no binding. An attached ${param.key} must resolve to a subject role or an eligible declared party.`
622
+ : `${id} needs ${param.key}`,
623
+ fix: partyParameter
624
+ ? `Use \`${param.key}: actor\` for the initiating customer or \`${param.key}: owner\` for the object owner. Declare a business for a fixed company counterparty.`
625
+ : `add ${param.key}: value inside ${id}`,
626
+ related: [
627
+ {
628
+ source: declarationSources.get(decl) ?? "program",
629
+ span: param.span,
630
+ message: `Parameter ${param.key}`,
631
+ },
632
+ ],
633
+ });
634
+ continue;
635
+ }
636
+ environment.set(param.key, actual);
637
+ }
638
+ let policyDiagnostics;
639
+ let dependencies;
640
+ try {
641
+ policyDiagnostics = parameterDiagnostics(decl);
642
+ dependencies = bindingDependencies(decl);
643
+ }
644
+ catch (error) {
645
+ if (!(error instanceof BindingContractError))
646
+ throw error;
647
+ throw new CompileFailure({
648
+ code: "HSX1001",
649
+ message: error.message,
650
+ fix: "Repair the header binding contract.",
651
+ span: error.entry.span,
652
+ source: declarationSources.get(decl) ?? "program",
653
+ });
654
+ }
655
+ for (const dependency of dependencies) {
656
+ const selected = environment.get(dependency.when);
657
+ if (selected?.kind === "name" &&
658
+ selected.value === dependency.is &&
659
+ !environment.has(dependency.binding)) {
660
+ throw new CompileFailure({
661
+ code: "HSX1001",
662
+ message: dependency.message.replaceAll("{attachment}", attachmentInfo?.attachmentName ?? id),
663
+ fix: dependency.fix,
664
+ span: origin,
665
+ related: [
666
+ {
667
+ source: declarationSources.get(decl) ?? "program",
668
+ span: dependency.span,
669
+ message: "Conditional binding requirement",
670
+ },
671
+ ],
672
+ });
673
+ }
674
+ }
675
+ for (const key of supplied.keys()) {
676
+ if (decl.parameters.some((p) => p.key === key))
677
+ continue;
678
+ const suppliedValue = supplied.get(key);
679
+ const requirements = decl.body.entries
680
+ .filter((entry) => entry.key.startsWith("action "))
681
+ .flatMap((entry) => asBlock(entries(asBlock(entry.value)).get("subject")).entries);
682
+ const moneyFields = [
683
+ ...new Set(requirements
684
+ .filter((entry) => {
685
+ const type = entry.value;
686
+ return type.kind === "name"
687
+ ? type.value === "money"
688
+ : (type.kind === "type" || type.kind === "call") &&
689
+ type.name === "money";
690
+ })
691
+ .map((entry) => entry.key)),
692
+ ];
693
+ if (attachmentInfo &&
694
+ (suppliedValue.kind === "money" || suppliedValue.kind === "name") &&
695
+ moneyFields.length === 1) {
696
+ const field = moneyFields[0];
697
+ const object = objects.get(attachmentInfo.subjectKindId);
698
+ const candidates = asBlock(entries(object.body).get("fields")).entries.filter((entry) => entry.value.kind === "name"
699
+ ? entry.value.value === "money"
700
+ : (entry.value.kind === "type" || entry.value.kind === "call") &&
701
+ entry.value.name === "money");
702
+ const target = candidates.length === 1 ? candidates[0].key : "yourMoneyField";
703
+ fail(suppliedValue, `\`${assignments.get(id)?.target ?? decl.name}\` has no \`${key}\` tunable. Its actions require the object's \`${field}\` field.`, `Remove \`${key}\`. To use your object's money field, add \`rename { ${field}: ${target} }\`.`);
704
+ }
705
+ fail(suppliedValue, `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
706
+ }
707
+ const isParty = (name) => !!document.parties[name] ||
708
+ (!!attachmentInfo &&
709
+ subjectPartyRoles.includes(name));
710
+ const resolvedParties = new Set();
711
+ const resolve = (expr, seen = new Set(), partyBinding = false) => {
712
+ if (expr.kind === "call" &&
713
+ ["object", "all", "party"].includes(expr.name)) {
714
+ if (expr.args.length !== 1)
715
+ fail(expr, `${expr.name} needs one type`, "supply one declared object or party kind");
716
+ const type = text(expr.args[0]);
717
+ const matches = expr.name === "party"
718
+ ? Object.entries(document.parties)
719
+ .filter(([name, party]) => party.kind === type && (!partyBinding || names.has(name)))
720
+ .map(([name]) => name)
721
+ : [...assignments.values()]
722
+ .filter((assignment) => expr.name === "all" ||
723
+ !attachmentSubjects.has(assignment.name) ||
724
+ attachmentSubjects.get(assignment.name) ===
725
+ attachmentInfo?.subjectKindId)
726
+ .filter((assignment) => assignment.target === type ||
727
+ type.startsWith(`${assignment.target}.`))
728
+ .map((assignment) => assignment.name +
729
+ type.slice(assignment.target.length).replaceAll(".", "_"));
730
+ if (expr.name === "object" &&
731
+ matches.length !== 1 &&
732
+ attachmentInfo) {
733
+ const parameter = [...environment].find(([, value]) => value === expr)?.[0] ??
734
+ decl.parameters.find((p) => p.value.kind === "default" &&
735
+ p.value.value.span.start === expr.span.start)?.key ??
736
+ "reference";
737
+ const local = (name) => attachmentSubjects.has(name)
738
+ ? name.slice(attachmentInfo.subjectKindId.length + 1)
739
+ : `${name} (program)`;
740
+ const target = templates.get(type);
741
+ const required = target?.parameters
742
+ .filter((p) => p.value.kind !== "default" &&
743
+ !(p.value.kind === "type" && p.value.optional))
744
+ .map((p) => p.key) ?? [];
745
+ fail({ span: origin }, `Attachment \`${attachmentInfo.attachmentName}\` needs a \`${parameter}\` binding. ${matches.length === 0
746
+ ? `No \`${type}\` attachment exists on \`${attachmentInfo.subjectKindId}\`.`
747
+ : `Several \`${type}\` attachments match on \`${attachmentInfo.subjectKindId}\`: ${matches.map(local).join(", ")}.`}`, matches.length
748
+ ? `Bind \`${parameter}\` explicitly to one of: ${matches.map(local).join(", ")}.`
749
+ : `Declare a ${parameter} attachment and bind \`${parameter}: allowance\`. Choose ${required.map((name) => (name.startsWith("per_") ? `its ${name.slice(4).replaceAll("_", " ")} limit` : `\`${name}\``)).join(", ") || "its required bindings"} explicitly.`);
750
+ }
751
+ if (expr.name !== "all" && matches.length !== 1)
752
+ failWithCode(expr, partyBinding ? "subject_party_unbound" : "HSX1001", `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`, "declare the required object or supply this tunable explicitly");
753
+ const items = matches.map((value) => ({
754
+ kind: "name",
755
+ value,
756
+ span: origin,
757
+ }));
758
+ return expr.name === "all"
759
+ ? { kind: "list", items, span: origin }
760
+ : items[0];
761
+ }
762
+ if (expr.kind !== "name")
763
+ return expr;
764
+ const binding = environment.get(expr.value);
765
+ if (attachmentInfo &&
766
+ subjectPartyRoles.includes(expr.value) &&
767
+ (!binding ||
768
+ (binding.kind === "name" && binding.value === expr.value)))
769
+ return expr;
770
+ // A resolved party parameter or an own parameter bound to a value takes
771
+ // precedence over a same-named sibling attachment; `limits: limits`
772
+ // binds by identity and still names the sibling.
773
+ if (attachmentInfo && !resolvedParties.has(expr.value)) {
774
+ const [local, ...tail] = expr.value.split(".");
775
+ const own = environment.get(local);
776
+ const ownValue = own !== undefined && !(own.kind === "name" && own.value === local);
777
+ const target = `${attachmentInfo.subjectKindId}_${local}`;
778
+ if (!ownValue && attachmentSubjects.has(target))
779
+ return { ...expr, value: [target, ...tail].join("_") };
780
+ }
781
+ if (expr.value.startsWith("party.")) {
782
+ const [, party, ...members] = expr.value.split(".");
783
+ const binding = environment.get(party);
784
+ if (binding?.kind === "name" && isParty(binding.value))
785
+ return {
786
+ ...expr,
787
+ value: ["party", binding.value, ...members].join("."),
788
+ };
789
+ }
790
+ const [root, ...tail] = expr.value.split(".");
791
+ const bound = environment.get(root);
792
+ if (!bound || (bound.kind === "name" && bound.value === root))
793
+ return expr;
794
+ if (seen.has(root))
795
+ return failWithCode(expr, partyBinding ? "subject_party_unbound" : "HSX1001", `cyclic tunable ${root}`, "replace the cycle with a literal or declared reference");
796
+ let resolved = resolvedParties.has(root) ||
797
+ (!partyBinding &&
798
+ (supplied.has(root) || inherited.has(root) || enums.has(root)))
799
+ ? bound
800
+ : resolve(bound, new Set([...seen, root]), partyBinding);
801
+ for (const key of tail) {
802
+ if (resolved.kind !== "block")
803
+ return expr;
804
+ const child = entries(resolved).get(key);
805
+ if (!child)
806
+ return fail(expr, `missing tunable ${expr.value}`, `declare ${key} in ${root}`);
807
+ resolved = resolve(child, new Set([...seen, root]), partyBinding);
808
+ }
809
+ return resolved;
810
+ };
811
+ for (const param of decl.parameters) {
812
+ try {
813
+ const actual = environment.get(param.key);
814
+ if (!actual)
815
+ continue;
816
+ const t = param.value.kind === "default" ? param.value.type : param.value;
817
+ const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
818
+ const asWritten = (supplied.has(param.key) && !attachmentInfo) || type === "enum";
819
+ let v = asWritten
820
+ ? actual
821
+ : resolve(actual, new Set(), !!attachmentInfo && type === "party");
822
+ if (type === "duration" &&
823
+ (v.kind === "text" || v.kind === "name") &&
824
+ /^P/.test(v.value))
825
+ v = { ...v, kind: "duration" };
826
+ environment.set(param.key, v);
827
+ if (type === "enum" && t.kind === "call") {
828
+ if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
829
+ fail(v, `invalid ${param.key}`, `choose ${t.args.map(text).join(", ")}`);
830
+ }
831
+ else if (type === "list") {
832
+ if (v.kind !== "list")
833
+ fail(v, `${param.key} needs a list`, "write [value, value]");
834
+ }
835
+ else if (type === "party") {
836
+ if (v.kind !== "name" || !isParty(v.value))
837
+ failWithCode(actual, attachmentInfo ? "subject_party_unbound" : "HSX1001", attachmentInfo
838
+ ? `\`${param.key}: ${"value" in actual && typeof actual.value === "string" ? actual.value : param.key}\` has no binding. An attached ${param.key} must resolve to a subject role or an eligible declared party.`
839
+ : `${param.key} needs a declared party`, attachmentInfo
840
+ ? `Use \`${param.key}: actor\` for the initiating customer or \`${param.key}: owner\` for the object owner. Declare a business for a fixed company counterparty.`
841
+ : "declare a party and use its name here");
842
+ const party = document.parties[v.value];
843
+ if ((party?.kind === "staff" && !party.role) ||
844
+ (attachmentInfo && party?.kind === "person"))
845
+ failWithCode(actual, "party_kind_mismatch", party?.kind === "person"
846
+ ? `\`${v.value}\` is a declared person. Attachments resolve customer identity through \`owner\` or \`actor\`, rather than a fixed person declaration.`
847
+ : `\`${v.value}\` is declared staff without a permission role.`, "Replace this binding with the appropriate subject role. Use declared businesses for fixed counterparties and permission-bearing staff roles for authorized actions.");
848
+ resolvedParties.add(param.key);
849
+ if (attachmentInfo)
850
+ attachmentInfo.parties[param.key] = subjectPartyRoles.includes(v.value)
851
+ ? { role: v.value }
852
+ : { party: v.value };
853
+ }
854
+ else if (type === "ref") {
855
+ // Listed objects resolve like a single binding, so `on: [plan]`
856
+ // names the sibling attachment that `on: plan` names.
857
+ const values = v.kind !== "list"
858
+ ? [v]
859
+ : asWritten
860
+ ? v.items
861
+ : v.items.map((item) => resolve(item));
862
+ if (v.kind === "list")
863
+ environment.set(param.key, { ...v, items: values });
864
+ if (v.kind === "list" && (t.kind !== "type" || !t.many))
865
+ fail(v, `${param.key} accepts one reference`, "use one object name");
866
+ if (!values.length || values.length > 16)
867
+ fail(v, "reference union needs 1 to 16 objects", "use at most 16 distinct object names");
868
+ const seen = new Set();
869
+ for (const value of values) {
870
+ if (value.kind !== "name")
871
+ fail(value, "reference needs an object name", "name a declared object");
872
+ let targetType = objects.get(value.value)?.name;
873
+ const candidates = [
874
+ ...[...assignments.values()].map((assignment) => [assignment.name, assignment.target]),
875
+ ...program.decls
876
+ .filter((decl) => decl.kind === "instrument")
877
+ .map((decl) => [decl.name, decl.name]),
878
+ ];
879
+ for (const [instance, target] of candidates) {
880
+ if (value.value === instance) {
881
+ targetType = target;
882
+ break;
883
+ }
884
+ if (!value.value.startsWith(`${instance}.`) &&
885
+ !value.value.startsWith(`${instance}_`))
886
+ continue;
887
+ const template = templates.get(target);
888
+ if (!template)
889
+ continue;
890
+ const suffix = value.value
891
+ .slice(instance.length + 1)
892
+ .replaceAll(".", "_");
893
+ const child = resolveChildExportPath(template, suffix);
894
+ if (child)
895
+ targetType = `${target}.${child}`;
896
+ }
897
+ if (!targetType ||
898
+ (t.kind === "type" && t.target && targetType !== t.target))
899
+ fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
900
+ if (seen.has(value.value))
901
+ fail(value, "duplicate reference", "list each object once");
902
+ seen.add(value.value);
903
+ }
904
+ // A many-reference tunable is a list even when one object is bound,
905
+ // so report datasets and other value positions never see a bare name.
906
+ if (t.kind === "type" && t.many && v.kind !== "list")
907
+ environment.set(param.key, {
908
+ kind: "list",
909
+ items: [v],
910
+ span: v.span,
911
+ });
912
+ }
913
+ else if (type === "fee" || type === "split" || type === "policy") {
914
+ if (v.kind !== "block")
915
+ fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
916
+ }
917
+ else if (type === "money" ||
918
+ type === "percent" ||
919
+ type === "date" ||
920
+ type === "duration" ||
921
+ type === "integer" ||
922
+ type === "text" ||
923
+ type === "boolean") {
924
+ const bounds = tunableBounds(t);
925
+ if (v.kind === "name" && v.value === "runtime")
926
+ continue;
927
+ const expected = type === "integer"
928
+ ? "number"
929
+ : type === "boolean"
930
+ ? "name"
931
+ : type;
932
+ if (v.kind !== expected ||
933
+ (type === "boolean" &&
934
+ v.kind === "name" &&
935
+ !["true", "false"].includes(v.value))) {
936
+ if (type === "money" && v.kind === "name" && attachmentInfo) {
937
+ const target = v.value.replace(/^subject\./, "");
938
+ const fallback = param.value.kind === "default"
939
+ ? param.value.value
940
+ : undefined;
941
+ const advice = fallback?.kind === "name" && fallback.value === "runtime"
942
+ ? `omit \`${param.key}\``
943
+ : `set \`${param.key}: runtime\``;
944
+ fail(v, `\`${param.key}\` cannot read \`${v.value}\` as a tunable. This position accepts a fixed money amount or a runtime amount.`, `For a varying deposit, ${advice} and add \`rename { ${param.key}: ${target} }\`. Use a literal only for a fixed charge.`);
945
+ }
946
+ if (type === "money" && v.kind === "percent") {
947
+ const policy = policyDiagnostics.find((policy) => policy.parameter === param.key);
948
+ fail(v, `\`${assignments.get(id)?.target ?? decl.name}.${param.key}\` accepts ${policy?.accepts ?? "a fixed amount"}. It cannot express ${policy?.percentage ?? "a percentage-based charge"}.`, policy?.fix ??
949
+ "Use a fixed amount only if that is the intended policy. A percentage charge needs an authored rate calculation; do not approximate it with a cash amount.");
950
+ }
951
+ fail(v, `${param.key} needs ${type}`, `write a ${type} literal`);
952
+ }
953
+ try {
954
+ const value = literal(v);
955
+ if (bounds &&
956
+ (BigInt(String(value)) < BigInt(bounds.minimum) ||
957
+ BigInt(String(value)) > BigInt(bounds.maximum)))
958
+ fail(v, `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`, "choose a value inside the tunable's bounds");
959
+ }
960
+ catch (error) {
961
+ if (error instanceof CompileFailure)
962
+ fail(v, `${param.key}: ${error.diagnostic.message}`, error.diagnostic.fix);
963
+ throw error;
964
+ }
965
+ }
966
+ else {
967
+ fail(t, `unknown tunable type ${type}`, "use a declared HSX tunable type");
968
+ }
969
+ }
970
+ catch (error) {
971
+ if (!(error instanceof CompileFailure))
972
+ throw error;
973
+ const related = [
974
+ {
975
+ source: declarationSources.get(decl) ?? "program",
976
+ span: param.span,
977
+ message: `Parameter ${param.key}`,
978
+ },
979
+ ];
980
+ const actual = supplied.get(param.key);
981
+ const party = actual?.kind === "name"
982
+ ? program.decls.find((entry) => entry.kind === "party" && entry.name === actual.value)
983
+ : undefined;
984
+ if (party?.kind === "party")
985
+ related.push({
986
+ source: "program",
987
+ span: party.span,
988
+ message: `Declared party ${party.name}`,
989
+ });
990
+ parameterErrors.push({ ...error.diagnostic, related });
991
+ }
992
+ }
993
+ if (parameterErrors.length) {
994
+ bindingDiagnostics.push(...parameterErrors.slice(1));
995
+ throw new CompileFailure(parameterErrors[0]);
996
+ }
997
+ const body = entries(decl.body);
998
+ for (const constraint of asBlock(body.get("constraints")).entries) {
999
+ const rule = constraint.value;
1000
+ if (rule.kind !== "call" ||
1001
+ rule.args.length !== 1 ||
1002
+ !["less_than", "at_most", "greater_than"].includes(rule.name))
1003
+ fail(rule, "invalid tunable constraint", "use less_than(name), at_most(name), or greater_than(name)");
1004
+ const left = environment.get(constraint.key), right = environment.get(text(rule.args[0]));
1005
+ if (!left || !right)
1006
+ fail(rule, "constraint needs two declared tunables", "name the compared tunables");
1007
+ const numericKind = (expr) => expr.kind === "name" && /^P/.test(expr.value)
1008
+ ? "duration"
1009
+ : expr.kind;
1010
+ if (!["money", "number", "percent", "duration"].includes(numericKind(left)) ||
1011
+ numericKind(left) !== numericKind(right))
1012
+ fail(rule, "constraint needs numeric tunables of the same type", "compare two amounts, integers, percentages or durations");
1013
+ const a = BigInt(String(literal(left))), b = BigInt(String(literal(right)));
1014
+ if (!(rule.name === "less_than"
1015
+ ? a < b
1016
+ : rule.name === "at_most"
1017
+ ? a <= b
1018
+ : a > b))
1019
+ fail(left, `${constraint.key} must be ${rule.name} ${text(rule.args[0])}`, "choose values satisfying the header constraint");
1020
+ }
1021
+ for (const key of body.keys())
1022
+ if (![
1023
+ "familyRevision",
1024
+ "fields",
1025
+ "lifecycle",
1026
+ "records",
1027
+ "summary",
1028
+ "invariants",
1029
+ "constraints",
1030
+ "dependencies",
1031
+ "parameterDiagnostics",
1032
+ "reports",
1033
+ "revisioned",
1034
+ "scope",
1035
+ ].includes(key) &&
1036
+ !key.startsWith("action "))
1037
+ fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
1038
+ if (body.has("familyRevision")) {
1039
+ const revExpr = body.get("familyRevision");
1040
+ const revVal = literal(revExpr);
1041
+ if (typeof revVal !== "number" ||
1042
+ !Number.isInteger(revVal) ||
1043
+ revVal <= 0) {
1044
+ fail(revExpr, "familyRevision must be a positive integer", "use a positive integer revision");
1045
+ }
1046
+ if (familyDeclaration) {
1047
+ familyDeclaration.revision = revVal;
1048
+ }
1049
+ }
1050
+ const records = entries(asBlock(body.get("records")));
1051
+ if (!inherited.size)
1052
+ environment.set("parent", { kind: "name", value: id, span: origin });
1053
+ for (const key of records.keys())
1054
+ if (!decl.parameters.some((parameter) => parameter.key === key))
1055
+ environment.set(key, {
1056
+ kind: "name",
1057
+ value: `${id}_${key}`,
1058
+ span: origin,
1059
+ });
1060
+ const fields = [];
1061
+ const calculations = [];
1062
+ let currentAction;
1063
+ let currentActionName;
1064
+ const path = (expr) => {
1065
+ const value = resolve(expr);
1066
+ const name = text(value);
1067
+ if (isParty(name))
1068
+ return `party.${name}`;
1069
+ if (name.startsWith("subject.")) {
1070
+ const subField = name.split(".")[1];
1071
+ if (currentAction) {
1072
+ const req = currentAction.subject?.requirements.find((r) => r.field.name === subField);
1073
+ if (!req) {
1074
+ failWithCode(expr, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${currentActionName}`, `declare ${subField} in subject { ... }`);
1075
+ }
1076
+ }
1077
+ else {
1078
+ const declaredInAction = decl.body.entries.some((e) => {
1079
+ if (!e.key.startsWith("action "))
1080
+ return false;
1081
+ const subBlock = entries(asBlock(e.value)).get("subject");
1082
+ if (!subBlock)
1083
+ return false;
1084
+ return asBlock(subBlock).entries.some((se) => {
1085
+ if (se.key === subField)
1086
+ return true;
1087
+ if (se.key === "adapter") {
1088
+ const names = se.value.kind === "list"
1089
+ ? se.value.items.map(text)
1090
+ : [text(se.value)];
1091
+ return names.some((n) => {
1092
+ const reg = adapterTarget(n);
1093
+ if (!reg)
1094
+ return false;
1095
+ const op = reg.adapter.operationMap[reg.operation];
1096
+ return op?.subjectRequirements?.some((sr) => sr.name === subField);
1097
+ });
1098
+ }
1099
+ return false;
1100
+ });
1101
+ });
1102
+ if (!declaredInAction) {
1103
+ failWithCode(expr, "subject_field_unknown", `subject.${subField} names no declared subject requirement in instrument ${id}`, `declare ${subField} in an action subject { ... }`);
1104
+ }
1105
+ }
1106
+ }
1107
+ return /^(self|input|party|subject)\./.test(name)
1108
+ ? name
1109
+ : `self.${name}`;
1110
+ };
1111
+ const val = (expr) => {
1112
+ const v = resolve(expr);
1113
+ if (v.kind !== "name" || ["true", "false"].includes(v.value))
1114
+ return { literal: literal(v) };
1115
+ const value = { field: path(v) };
1116
+ authored.set(value, expr);
1117
+ return value;
1118
+ };
1119
+ const data = (expr) => {
1120
+ if (expr.kind === "call" &&
1121
+ expr.name === "current" &&
1122
+ !expr.args.length)
1123
+ return id;
1124
+ const value = resolve(expr);
1125
+ if (value.kind === "block") {
1126
+ const rawEntries = entries(value);
1127
+ if ((rawEntries.has("family") && !rawEntries.has("kind")) ||
1128
+ ((rawEntries.has("states") ||
1129
+ rawEntries.has("reference") ||
1130
+ rawEntries.has("anchor")) &&
1131
+ rawEntries.has("instrument"))) {
1132
+ let famTuple;
1133
+ if (rawEntries.has("family")) {
1134
+ const famExpr = rawEntries.get("family");
1135
+ const famStr = famExpr.kind === "name" ? famExpr.value : text(famExpr);
1136
+ famTuple = resolveFamily(famStr, famExpr);
1137
+ }
1138
+ let instrumentVal;
1139
+ if (rawEntries.has("instrument")) {
1140
+ instrumentVal = data(rawEntries.get("instrument"));
1141
+ if (famTuple) {
1142
+ checkTargetFamily(instrumentVal, famTuple, rawEntries.get("instrument"));
1143
+ }
1144
+ }
1145
+ else if (famTuple) {
1146
+ const matched = resolveFamilyInstruments(famTuple, id, rawEntries.get("family"));
1147
+ instrumentVal = matched.length === 1 ? matched[0] : matched;
1148
+ }
1149
+ const result = {};
1150
+ if (famTuple) {
1151
+ result.family = famTuple;
1152
+ }
1153
+ if (instrumentVal !== undefined) {
1154
+ result.instrument = instrumentVal;
1155
+ }
1156
+ for (const [k, v] of rawEntries) {
1157
+ if (k === "family" || k === "instrument")
1158
+ continue;
1159
+ result[k] = data(v);
1160
+ }
1161
+ authored.set(result, value);
1162
+ return result;
1163
+ }
1164
+ const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
1165
+ const selection = result.selection;
1166
+ if (Array.isArray(selection?.instrument) &&
1167
+ selection.instrument.length === 0) {
1168
+ if (result.op === "aggregate")
1169
+ return {
1170
+ target: result.target,
1171
+ op: "sum",
1172
+ values: [{ literal: result.measure === "count" ? 0 : "0" }],
1173
+ };
1174
+ if (result.kind === "aggregate")
1175
+ return {
1176
+ kind: "compare",
1177
+ left: { literal: result.measure === "count" ? 0 : "0" },
1178
+ operator: result.operator,
1179
+ right: result.value,
1180
+ };
1181
+ return null;
1182
+ }
1183
+ authored.set(result, value);
1184
+ return result;
1185
+ }
1186
+ if (value.kind === "list") {
1187
+ const items = value.items.map(data).filter((item) => item !== null);
1188
+ authored.set(items, value);
1189
+ return items;
1190
+ }
1191
+ if (value.kind === "call" &&
1192
+ value.name === "child" &&
1193
+ value.args.length === 2) {
1194
+ const parent = resolve(value.args[0]);
1195
+ const child = (item) => `${text(item).replaceAll(".", "_")}_${text(value.args[1])}`;
1196
+ return parent.kind === "list"
1197
+ ? parent.items.map(child)
1198
+ : child(parent);
1199
+ }
1200
+ if (value.kind === "name")
1201
+ return value.value === "true" || value.value === "false"
1202
+ ? value.value === "true"
1203
+ : value.value;
1204
+ return literal(value);
1205
+ };
1206
+ const lowerFields = (block) => {
1207
+ const result = [];
1208
+ for (const row of block.entries) {
1209
+ const { type: t } = fieldType(row);
1210
+ const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
1211
+ const f = lowerFieldShape(row, resolve);
1212
+ const type = f.type;
1213
+ if (type === "list" && t.kind === "call") {
1214
+ const item = t.args[0];
1215
+ if (item.kind === "type" && item.name === "ref" && item.target) {
1216
+ f.target = text(resolve({ kind: "name", value: item.target, span: item.span })).replaceAll(".", "_");
1217
+ f.targetKind = objects.has(String(f.target))
1218
+ ? "object"
1219
+ : "instrument";
1220
+ }
1221
+ }
1222
+ if (type === "ref") {
1223
+ if (t.kind === "block") {
1224
+ const b = entries(t);
1225
+ const famNode = b.get("targetFamily") ?? b.get("family");
1226
+ let targetFamTuple;
1227
+ if (famNode) {
1228
+ const famStr = famNode.kind === "name" ? famNode.value : text(famNode);
1229
+ targetFamTuple = resolveFamily(famStr, famNode);
1230
+ f.targetFamily = targetFamTuple;
1231
+ }
1232
+ if (b.has("target") || b.has("instrument")) {
1233
+ const tgtExpr = (b.get("target") ?? b.get("instrument"));
1234
+ const resolvedTgt = resolve(tgtExpr);
1235
+ let tgtVal;
1236
+ if (resolvedTgt.kind === "list") {
1237
+ const items = resolvedTgt.items.map((it) => text(resolve(it)).replaceAll(".", "_"));
1238
+ tgtVal = items.length === 1 ? items[0] : items;
1239
+ }
1240
+ else {
1241
+ tgtVal = text(resolvedTgt).replaceAll(".", "_");
1242
+ }
1243
+ f.target = tgtVal;
1244
+ if (targetFamTuple) {
1245
+ checkTargetFamily(tgtVal, targetFamTuple, tgtExpr);
1246
+ }
1247
+ }
1248
+ else if (targetFamTuple) {
1249
+ const matched = resolveFamilyInstruments(targetFamTuple, id, famNode);
1250
+ f.target = matched.length === 1 ? matched[0] : matched;
1251
+ }
1252
+ f.targetKind =
1253
+ typeof f.target === "string" && objects.has(f.target)
1254
+ ? "object"
1255
+ : "instrument";
1256
+ }
1257
+ else {
1258
+ const target = t.kind === "type" ? t.target : undefined;
1259
+ if (!target)
1260
+ fail(row, "reference needs a target", "write ref<object>");
1261
+ const [root, ...tail] = target.split(".");
1262
+ const resolved = environment.get(root);
1263
+ // An optional field pointing at an unsupplied optional tunable
1264
+ // has nothing to point at, so the instrument drops it.
1265
+ if (!resolved &&
1266
+ f.optional &&
1267
+ decl.parameters.some((parameter) => parameter.key === root))
1268
+ continue;
1269
+ const resolvedTargets = resolved?.kind === "list"
1270
+ ? resolved.items
1271
+ : resolved
1272
+ ? [resolved]
1273
+ : [];
1274
+ const targets = resolvedTargets.map((value) => [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"));
1275
+ f.target =
1276
+ target === "self"
1277
+ ? id
1278
+ : targets.length === 1
1279
+ ? targets[0]
1280
+ : targets.length
1281
+ ? targets
1282
+ : target;
1283
+ f.targetKind =
1284
+ typeof f.target === "string" && objects.has(f.target)
1285
+ ? "object"
1286
+ : "instrument";
1287
+ }
1288
+ }
1289
+ else if (type === "account") {
1290
+ if (t.kind === "call") {
1291
+ if (t.args.length < 1 || t.args.length > 4)
1292
+ fail(t, "account needs an owner, optional book, mode and key", "write account(buyer, claim, contra)");
1293
+ const owner = t.args[0];
1294
+ if (owner.kind === "call" && owner.name === "adapter") {
1295
+ if (owner.args.length !== 1)
1296
+ fail(owner, "adapter needs one binding", 'write account(adapter(binding), cash, "premium")');
1297
+ f.owner = { adapter: text(resolve(owner.args[0])) };
1298
+ }
1299
+ else
1300
+ f.owner = text(resolve(owner));
1301
+ f.book = t.args[1] ? text(t.args[1]) : "cash";
1302
+ if (t.args[2]) {
1303
+ const mode = text(t.args[2]);
1304
+ if (mode === "contra")
1305
+ f.contra = true;
1306
+ else
1307
+ f.key = mode;
1308
+ }
1309
+ if (t.args[3])
1310
+ f.key = text(t.args[3]);
1311
+ }
1312
+ else if (t.kind === "type" && t.owner) {
1313
+ f.owner =
1314
+ t.owner === "self"
1315
+ ? "self"
1316
+ : text(resolve({ kind: "name", value: t.owner, span: row.span }));
1317
+ f.book = "cash";
1318
+ }
1319
+ else
1320
+ fail(row, "account needs an owner", "write account of buyer or account of self");
1321
+ }
1322
+ if (constant &&
1323
+ !(constant.kind === "name" && constant.value === "runtime")) {
1324
+ if (constant.kind === "call") {
1325
+ const args = constant.name === "aggregate" ? [] : constant.args.map(val);
1326
+ const target = row.key;
1327
+ if (constant.name === "aggregate" && constant.args.length === 2)
1328
+ calculations.push({
1329
+ target,
1330
+ op: "aggregate",
1331
+ selection: data(constant.args[0]),
1332
+ measure: text(constant.args[1]) === "count"
1333
+ ? "count"
1334
+ : { sum: text(constant.args[1]) },
1335
+ });
1336
+ else if (constant.name === "at" && args.length === 2)
1337
+ calculations.push({
1338
+ target,
1339
+ op: "at",
1340
+ list: path(constant.args[0]),
1341
+ position: args[1],
1342
+ });
1343
+ else if (constant.name === "ratio" && args.length === 3)
1344
+ calculations.push({
1345
+ target,
1346
+ op: "ratio",
1347
+ amount: args[0],
1348
+ numerator: args[1],
1349
+ denominator: args[2],
1350
+ rounding: "floor",
1351
+ });
1352
+ else if (constant.name === "rate" && args.length === 2)
1353
+ calculations.push({
1354
+ target,
1355
+ op: "rate",
1356
+ base: args[0],
1357
+ bps: args[1],
1358
+ rounding: "floor",
1359
+ });
1360
+ else if (constant.name === "sum" || constant.name === "minimum")
1361
+ calculations.push({ target, op: constant.name, values: args });
1362
+ else if (constant.name === "subtract" && args.length >= 2)
1363
+ calculations.push({
1364
+ target,
1365
+ op: "subtract",
1366
+ base: args[0],
1367
+ subtract: args.slice(1),
1368
+ });
1369
+ else if (constant.name === "multiply" && args.length === 2)
1370
+ calculations.push({
1371
+ target,
1372
+ op: "multiply",
1373
+ amount: args[0],
1374
+ units: args[1],
1375
+ });
1376
+ else if (constant.name === "divide" && args.length === 2)
1377
+ calculations.push({
1378
+ target,
1379
+ op: "divide",
1380
+ amount: args[0],
1381
+ divisor: args[1],
1382
+ rounding: "floor",
1383
+ });
1384
+ else if (["before", "after"].includes(constant.name) &&
1385
+ args.length === 2)
1386
+ calculations.push({
1387
+ target,
1388
+ op: "shift",
1389
+ date: args[0],
1390
+ milliseconds: args[1],
1391
+ direction: constant.name,
1392
+ });
1393
+ else
1394
+ fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
1395
+ authored.set(calculations.at(-1), constant);
1396
+ }
1397
+ else if (type === "enum" && constant.kind === "name")
1398
+ f.value = constant.value;
1399
+ else if (type === "money" && constant.kind === "name")
1400
+ calculations.push({
1401
+ target: row.key,
1402
+ op: "sum",
1403
+ values: [val(constant)],
1404
+ });
1405
+ else
1406
+ f.value = literal(constant, String(type));
1407
+ }
1408
+ authored.set(f, row.value);
1409
+ result.push(f);
1410
+ }
1411
+ return result;
1412
+ };
1413
+ fields.push(...lowerFields(asBlock(body.get("fields"))));
1414
+ const lifecycleBlock = asBlock(body.get("lifecycle"));
1415
+ const lifecycle = data(lifecycleBlock);
1416
+ const states = Array.isArray(lifecycle.states) ? lifecycle.states : [];
1417
+ const checkState = (expr, state) => {
1418
+ if (typeof state !== "string" || !states.length)
1419
+ return;
1420
+ if (states.includes(state))
1421
+ return;
1422
+ fail(expr, `\`${state}\` is not a state of \`${decl.name}\`.${didYouMean(state, states)}`, `Use one of ${states.join(", ")}, or add \`${state}\` to \`lifecycle.states\`.`);
1423
+ };
1424
+ const initialExpr = entries(lifecycleBlock).get("initial");
1425
+ if (initialExpr)
1426
+ checkState(initialExpr, lifecycle.initial);
1427
+ lifecycle.transitions = Object.create(null);
1428
+ const inst = {
1429
+ id,
1430
+ ...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
1431
+ title: title(id),
1432
+ summary: body.has("summary")
1433
+ ? String(data(body.get("summary")))
1434
+ : title(id),
1435
+ fields,
1436
+ calculate: calculations,
1437
+ lifecycle,
1438
+ actions: Object.create(null),
1439
+ actionOrder: [],
1440
+ };
1441
+ for (const row of decl.body.entries.filter((e) => e.key.startsWith("action "))) {
1442
+ const name = row.key.slice(7);
1443
+ const selected = (block) => ({
1444
+ ...block,
1445
+ entries: block.entries.flatMap((entry) => {
1446
+ if (!entry.key.startsWith("when "))
1447
+ return [entry];
1448
+ const [, tunable, relation, choice] = entry.key.split(" ");
1449
+ const binding = environment.get(tunable);
1450
+ if (!binding)
1451
+ fail(entry, `unknown branch tunable ${tunable}`, "name an enum or reference tunable declared by this header");
1452
+ let matches;
1453
+ if (relation === "has") {
1454
+ // Inspect declarations, not lowering order: a bound object may
1455
+ // appear after the instrument that asks about its fields.
1456
+ const bound = resolve(binding);
1457
+ const [root, ...children] = text(bound).split(".");
1458
+ const assignment = assignments.get(root);
1459
+ let target = assignment
1460
+ ? templates.get(assignment.target)?.body
1461
+ : program.decls
1462
+ .filter((decl) => decl.kind === "instrument")
1463
+ .find((decl) => decl.name === root)?.body;
1464
+ for (const child of children) {
1465
+ const record = target
1466
+ ? entries(asBlock(entries(target).get("records"))).get(child)
1467
+ : undefined;
1468
+ target = record ? asBlock(record) : undefined;
1469
+ }
1470
+ if (!target)
1471
+ fail(entry, "field branch needs a declared object", "bind a reference to a declared object");
1472
+ matches = entries(asBlock(entries(target).get("fields"))).has(choice);
1473
+ }
1474
+ else {
1475
+ if (!enums.get(tunable)?.includes(choice))
1476
+ fail(entry, `unknown enum branch ${choice}`, "use a declared enum value");
1477
+ matches = text(binding) === choice;
1478
+ }
1479
+ const body = asBlock(entry.value);
1480
+ for (const clause of body.entries)
1481
+ if (!["requires", "moves", "invoke", "calculate"].includes(clause.key) &&
1482
+ !clause.key.startsWith("when "))
1483
+ fail(clause, "when permits requirements, calculations, moves and invocations", "keep lifecycle and actor clauses outside the branch");
1484
+ return matches ? selected(body).entries : [];
1485
+ }),
1486
+ });
1487
+ const slots = entries(selected(asBlock(row.value)));
1488
+ let actionSubject;
1489
+ let subjectExpr = slots.get("subject");
1490
+ const boundaryBindings = new Set();
1491
+ const authoredMoves = slots.get("moves");
1492
+ for (const move of authoredMoves?.kind === "list"
1493
+ ? authoredMoves.items
1494
+ : authoredMoves
1495
+ ? [authoredMoves]
1496
+ : []) {
1497
+ const parts = entries(asBlock(move));
1498
+ const boundary = parts.get("boundary");
1499
+ if (!boundary)
1500
+ continue;
1501
+ if ((parts.has("operation")
1502
+ ? String(data(parts.get("operation")))
1503
+ : "internal_transfer.create") !== "internal_transfer.reserve" ||
1504
+ parts.has("shares") ||
1505
+ parts.has("fee"))
1506
+ fail(move, "boundary dispatch requires a reservation", "reserve the exact amount before dispatch");
1507
+ const adapterExpr = entries(asBlock(boundary)).get("adapter");
1508
+ if (!adapterExpr)
1509
+ fail(boundary, "boundary needs an adapter", "name a bound ADL adapter");
1510
+ const binding = text(resolve(adapterExpr));
1511
+ const target = adapterTarget(binding);
1512
+ if (!target)
1513
+ fail(boundary, `unknown boundary adapter ${binding}`, "bind the named ADL adapter before compilation");
1514
+ boundaryBindings.add(binding);
1515
+ }
1516
+ if (boundaryBindings.size) {
1517
+ const block = asBlock(subjectExpr);
1518
+ const declared = new Set(block.entries
1519
+ .filter((entry) => entry.key === "adapter")
1520
+ .flatMap((entry) => entry.value.kind === "list"
1521
+ ? entry.value.items.map(text)
1522
+ : [text(entry.value)]));
1523
+ subjectExpr = {
1524
+ ...block,
1525
+ entries: [
1526
+ ...block.entries,
1527
+ ...[...boundaryBindings]
1528
+ .filter((binding) => !declared.has(binding))
1529
+ .map((binding) => ({
1530
+ key: "adapter",
1531
+ value: {
1532
+ kind: "name",
1533
+ value: binding,
1534
+ span: row.span,
1535
+ },
1536
+ span: row.span,
1537
+ })),
1538
+ ],
1539
+ };
1540
+ }
1541
+ if (subjectExpr) {
1542
+ const subjectBlock = asBlock(subjectExpr);
1543
+ const directRequirements = [];
1544
+ const adapterList = [];
1545
+ for (const entry of subjectBlock.entries) {
1546
+ if (entry.key === "adapter") {
1547
+ // A tunable names the adapter; anything else is the binding itself.
1548
+ const adapterName = (item) => item.kind === "name" &&
1549
+ decl.parameters.some((parameter) => parameter.key === item.value)
1550
+ ? text(resolve(item))
1551
+ : text(item);
1552
+ const bindingNames = entry.value.kind === "list"
1553
+ ? entry.value.items.map(adapterName)
1554
+ : [adapterName(entry.value)];
1555
+ for (const bindingName of bindingNames) {
1556
+ const target = adapterTarget(bindingName);
1557
+ if (target) {
1558
+ const { adapter, operation } = target;
1559
+ const opBinding = adapter.operationMap[operation];
1560
+ if (opBinding &&
1561
+ opBinding.subjectRequirements !== undefined) {
1562
+ const validatedRequirements = [];
1563
+ for (const req of opBinding.subjectRequirements) {
1564
+ const result = udlObjectFieldSchema.safeParse(req);
1565
+ if (!result.success || result.data.optional) {
1566
+ fail(entry, `adapter requirement ${req.name} is invalid or optional: ${result.success ? "requirements cannot be optional" : result.error.message}`, "ensure adapter subject requirements conform to UDL schema");
1567
+ }
1568
+ validatedRequirements.push(result.data);
1569
+ }
1570
+ const declaration = {
1571
+ provider: adapter.provider,
1572
+ capability: adapter.capability,
1573
+ operation,
1574
+ requirements: validatedRequirements,
1575
+ };
1576
+ const digest = sha256(new TextEncoder().encode(canonicalJson(declaration)));
1577
+ const snapshot = {
1578
+ ...declaration,
1579
+ declarationDigest: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
1580
+ };
1581
+ const adapterRenames = {};
1582
+ if (attachmentInfo?.renames) {
1583
+ for (const req of snapshot.requirements) {
1584
+ if (attachmentInfo.renames.has(req.name)) {
1585
+ adapterRenames[req.name] = attachmentInfo.renames.get(req.name);
1586
+ }
1587
+ }
1588
+ }
1589
+ adapterList.push({
1590
+ binding: bindingName,
1591
+ snapshot,
1592
+ ...(Object.keys(adapterRenames).length > 0
1593
+ ? { renames: adapterRenames }
1594
+ : {}),
1595
+ });
1596
+ for (const reqField of snapshot.requirements) {
1597
+ const objectField = attachmentInfo?.renames.get(reqField.name);
1598
+ const targetName = objectField ?? reqField.name;
1599
+ const existing = directRequirements.find((r) => (r.objectField ?? r.field.name) === targetName);
1600
+ if (existing) {
1601
+ if (!sameObjectField(existing.field, reqField)) {
1602
+ failWithCode(entry, "subject_field_conflict", `conflicting requirement ${reqField.name} in action ${name}`, "rename or unify the requirement");
1603
+ }
1604
+ }
1605
+ else {
1606
+ directRequirements.push({
1607
+ field: { ...reqField },
1608
+ ...(objectField ? { objectField } : {}),
1609
+ });
1610
+ }
1611
+ }
1612
+ }
1613
+ else {
1614
+ adapterList.push({
1615
+ binding: bindingName,
1616
+ snapshot: null,
1617
+ });
1618
+ }
1619
+ }
1620
+ else {
1621
+ adapterList.push({
1622
+ binding: bindingName,
1623
+ snapshot: null,
1624
+ });
1625
+ }
1626
+ }
1627
+ }
1628
+ else {
1629
+ const fieldDef = lowerObjectField(entry, resolve);
1630
+ if (fieldDef.optional) {
1631
+ fail(entry, `subject requirement ${entry.key} cannot be optional`, "remove ? from requirement");
1632
+ }
1633
+ const objectField = attachmentInfo?.renames.get(entry.key);
1634
+ const targetName = objectField ?? entry.key;
1635
+ const existing = directRequirements.find((r) => (r.objectField ?? r.field.name) === targetName);
1636
+ if (existing) {
1637
+ if (!sameObjectField(existing.field, fieldDef)) {
1638
+ failWithCode(entry, "subject_field_conflict", `conflicting requirement ${entry.key} in action ${name}`, "rename or unify the requirement");
1639
+ }
1640
+ }
1641
+ else {
1642
+ directRequirements.push({
1643
+ field: fieldDef,
1644
+ ...(objectField ? { objectField } : {}),
1645
+ });
1646
+ }
1647
+ }
1648
+ }
1649
+ if (directRequirements.length > 0 || adapterList.length > 0) {
1650
+ actionSubject = {
1651
+ requirements: directRequirements,
1652
+ adapters: adapterList,
1653
+ };
1654
+ }
1655
+ }
1656
+ const fromBinding = slots.get("from");
1657
+ if (fromBinding?.kind === "name" &&
1658
+ fromBinding.value.includes(".") &&
1659
+ slots.has("moves")) {
1660
+ const [parameter, member] = fromBinding.value.split(".");
1661
+ const suppliedPolicy = supplied.get(parameter);
1662
+ if (suppliedPolicy?.kind === "block") {
1663
+ const selected = entries(suppliedPolicy).get(member);
1664
+ const parameterDecl = decl.parameters.find((entry) => entry.key === parameter);
1665
+ const fallback = parameterDecl?.value.kind === "default"
1666
+ ? parameterDecl.value.value
1667
+ : undefined;
1668
+ const defaultState = fallback?.kind === "block"
1669
+ ? entries(fallback).get(member)
1670
+ : undefined;
1671
+ if (selected && defaultState?.kind === "name") {
1672
+ const bindings = refundBindings.get(id) ?? [];
1673
+ bindings.push({
1674
+ parameter: member,
1675
+ span: selected.span,
1676
+ defaultState: defaultState.value,
1677
+ action: name,
1678
+ });
1679
+ refundBindings.set(id, bindings);
1680
+ }
1681
+ }
1682
+ }
1683
+ const a = {
1684
+ summary: slots.has("summary")
1685
+ ? String(data(slots.get("summary")))
1686
+ : title(name),
1687
+ publicAction: camel(`${name}_${id}`),
1688
+ event: `${id}.${name}`,
1689
+ actor: "caller",
1690
+ input: lowerFields(asBlock(slots.get("input"))),
1691
+ requires: [],
1692
+ moves: [],
1693
+ ...(actionSubject ? { subject: actionSubject } : {}),
1694
+ };
1695
+ authored.set(a, row.value);
1696
+ for (const binding of boundaryBindings)
1697
+ if (!a.subject?.adapters.find((entry) => entry.binding === binding)
1698
+ ?.snapshot)
1699
+ fail(row, `boundary adapter ${binding} has no declared requirements`, "declare the adapter subject requirements, including an explicit empty list");
1700
+ currentAction = a;
1701
+ currentActionName = name;
1702
+ if (name !== "create") {
1703
+ const from = slots.get("from");
1704
+ const to = slots.get("to");
1705
+ if (!from || !to)
1706
+ fail(row, `action ${name} needs from and to`, "declare its source and destination states");
1707
+ const sources = from.kind === "list" ? from.items : [from];
1708
+ const fromStates = sources.map((e) => String(data(e)));
1709
+ const toState = String(data(to));
1710
+ sources.forEach((e, index) => checkState(e, fromStates[index]));
1711
+ if (toState !== "preserve")
1712
+ checkState(to, toState);
1713
+ lifecycle.transitions[name] = { from: fromStates, to: toState };
1714
+ }
1715
+ for (const [key, expr] of slots) {
1716
+ if (["from", "to", "input", "summary", "subject"].includes(key))
1717
+ continue;
1718
+ if (key === "moves") {
1719
+ const moves = expr.kind === "list" ? expr.items : [expr];
1720
+ for (const [index, move] of moves.entries()) {
1721
+ const lowered = a.moves.length;
1722
+ const parts = entries(asBlock(move));
1723
+ const op = parts.has("operation")
1724
+ ? String(data(parts.get("operation")))
1725
+ : "internal_transfer.create";
1726
+ if (op === "internal_transfer.reserve" && !parts.has("capture"))
1727
+ fail(move, "reserve needs a receipt field", "add capture: receipt_name");
1728
+ if ((op === "internal_transfer.post" ||
1729
+ op === "internal_transfer.void") &&
1730
+ !parts.has("transfer"))
1731
+ fail(move, "post and void need a receipt", "add transfer: receipt_name");
1732
+ const key = parts.has("key")
1733
+ ? String(data(parts.get("key")))
1734
+ : `move${index + 1}`;
1735
+ const lowerEconomics = (economicsExpr) => {
1736
+ if (economicsExpr) {
1737
+ const resolved = resolve(economicsExpr);
1738
+ if (resolved.kind === "name" &&
1739
+ resolved.value === "unclassified")
1740
+ return undefined;
1741
+ const choices = entries(asBlock(resolve(economicsExpr)));
1742
+ if (choices.has("recipient")) {
1743
+ for (const term of choices.keys())
1744
+ if (!["recipient", "company", "participant"].includes(term))
1745
+ fail(economicsExpr, `unknown economics choice ${term}`, "use recipient, company and participant");
1746
+ const recipient = String(data(choices.get("recipient")));
1747
+ if (!isParty(recipient) ||
1748
+ !choices.has("company") ||
1749
+ !choices.has("participant"))
1750
+ fail(economicsExpr, "economics choice needs a bound recipient and both branches", "declare recipient, company and participant");
1751
+ const company = recipient === "operator" ||
1752
+ recipient === "programOperator" ||
1753
+ document.parties[recipient]?.role === "program_operator";
1754
+ return lowerEconomics(choices.get(company ? "company" : "participant"));
1755
+ }
1756
+ }
1757
+ const economicsParts = economicsExpr
1758
+ ? entries(asBlock(resolve(economicsExpr)))
1759
+ : undefined;
1760
+ if (economicsParts) {
1761
+ for (const term of economicsParts.keys())
1762
+ if (!["purpose", "sourceParty", "reversalOf"].includes(term))
1763
+ fail(economicsExpr, `unknown economics term ${term}`, "use purpose, sourceParty or reversalOf");
1764
+ }
1765
+ const purpose = economicsParts?.get("purpose");
1766
+ const sourceParty = economicsParts?.get("sourceParty");
1767
+ if (economicsParts && (!purpose || !sourceParty))
1768
+ fail(economicsExpr, "economics needs purpose and sourceParty", "declare both economics fields");
1769
+ const purposeName = purpose ? String(data(purpose)) : undefined;
1770
+ if (purposeName &&
1771
+ ![
1772
+ "earning",
1773
+ "principal",
1774
+ "participant_payout",
1775
+ "internal",
1776
+ "prepaid_credit",
1777
+ "pass_through",
1778
+ ].includes(purposeName))
1779
+ fail(purpose, `unknown economic purpose ${purposeName}`, "choose a declared economic purpose");
1780
+ const sourcePartyName = sourceParty
1781
+ ? String(data(sourceParty))
1782
+ : undefined;
1783
+ if (sourcePartyName && !isParty(sourcePartyName))
1784
+ fail(sourceParty, `unknown source party ${sourcePartyName}`, "bind an authored party");
1785
+ return economicsParts
1786
+ ? {
1787
+ purpose: purposeName,
1788
+ sourceParty: sourcePartyName,
1789
+ ...(economicsParts.has("reversalOf")
1790
+ ? {
1791
+ reversalOf: path(economicsParts.get("reversalOf")),
1792
+ }
1793
+ : {}),
1794
+ }
1795
+ : undefined;
1796
+ };
1797
+ const economicsExpr = parts.get("economics");
1798
+ const feeEconomics = parts.has("fee") && economicsExpr
1799
+ ? entries(asBlock(resolve(economicsExpr)))
1800
+ : undefined;
1801
+ if (feeEconomics)
1802
+ for (const term of feeEconomics.keys())
1803
+ if (!["payee", "fee", "tax"].includes(term))
1804
+ fail(economicsExpr, `unknown fee economics leg ${term}`, "declare payee, fee or tax economics separately");
1805
+ const economics = lowerEconomics(feeEconomics ? feeEconomics.get("payee") : economicsExpr);
1806
+ if (op === "internal_transfer.create" ||
1807
+ op === "internal_transfer.reserve") {
1808
+ const amount = parts.get("amount"), from = parts.get("from"), to = parts.get("to");
1809
+ if (amount) {
1810
+ const resolvedAmount = resolve(amount);
1811
+ if (resolvedAmount.kind === "name" &&
1812
+ resolvedAmount.value.startsWith("subject.")) {
1813
+ const subField = resolvedAmount.value.split(".")[1];
1814
+ const req = a.subject?.requirements.find((r) => r.field.name === subField);
1815
+ if (req && req.field.type !== "money") {
1816
+ fail(amount, `move amount subject.${subField} must have type money`, "use a money field");
1817
+ }
1818
+ }
1819
+ }
1820
+ if (parts.has("shares")) {
1821
+ if (!amount ||
1822
+ !from ||
1823
+ op !== "internal_transfer.create" ||
1824
+ parts.has("fee"))
1825
+ fail(move, "split needs a posted amount and source", "declare amount, from and shares without a fee");
1826
+ const shares = [
1827
+ ...entries(asBlock(resolve(parts.get("shares")))),
1828
+ ];
1829
+ if (!shares.length || shares.length > 64)
1830
+ fail(move, "split needs 1 to 64 recipients", "name the receiving parties and percentages");
1831
+ let total = 0;
1832
+ const pieces = [];
1833
+ for (const [index, [party, rate]] of shares.entries()) {
1834
+ const recipient = resolve({
1835
+ kind: "name",
1836
+ value: party,
1837
+ span: rate.span,
1838
+ });
1839
+ if (recipient.kind !== "name" ||
1840
+ !isParty(recipient.value) ||
1841
+ rate.kind !== "percent")
1842
+ fail(rate, "split needs party percentages", "write party_name: 70%");
1843
+ total += Number(literal(rate));
1844
+ const target = `${name}_${key}_share${index + 1}`;
1845
+ if (fields.some((f) => f.name === target))
1846
+ fail(move, "split field name conflicts", "rename the move key");
1847
+ fields.push({ name: target, type: "money" });
1848
+ calculations.push(index === shares.length - 1 && pieces.length
1849
+ ? {
1850
+ target,
1851
+ op: "subtract",
1852
+ base: val(amount),
1853
+ subtract: [...pieces],
1854
+ }
1855
+ : {
1856
+ target,
1857
+ op: "rate",
1858
+ base: val(amount),
1859
+ bps: { literal: literal(rate) },
1860
+ rounding: "floor",
1861
+ });
1862
+ const piece = { field: `self.${target}` };
1863
+ pieces.push(piece);
1864
+ a.moves.push({
1865
+ key: `${key}_${index + 1}`,
1866
+ operation: "internal_transfer.create",
1867
+ ...(economics ? { economics } : {}),
1868
+ amount: piece,
1869
+ from: path(from),
1870
+ to: `party.${recipient.value}`,
1871
+ });
1872
+ }
1873
+ if (total !== 10000)
1874
+ fail(move, "split percentages must sum to 100%", "give the recipients exactly 100% together");
1875
+ for (const item of a.moves.slice(lowered))
1876
+ authored.set(item, move);
1877
+ continue;
1878
+ }
1879
+ if (!amount || !from || !to)
1880
+ fail(move, "move needs amount, from and to", "declare all three move operands");
1881
+ const transfer = {
1882
+ key,
1883
+ operation: op,
1884
+ ...(economics ? { economics } : {}),
1885
+ ...(parts.has("capture")
1886
+ ? { capture: String(data(parts.get("capture"))) }
1887
+ : {}),
1888
+ amount: val(amount),
1889
+ from: path(from),
1890
+ to: path(to),
1891
+ };
1892
+ const fee = parts.get("fee");
1893
+ if (!fee)
1894
+ a.moves.push(op === "internal_transfer.reserve"
1895
+ ? {
1896
+ ...transfer,
1897
+ operation: op,
1898
+ capture: String(data(parts.get("capture"))),
1899
+ ...(parts.has("boundary")
1900
+ ? {
1901
+ boundary: {
1902
+ adapter: text(resolve(entries(asBlock(parts.get("boundary"))).get("adapter"))),
1903
+ },
1904
+ }
1905
+ : {}),
1906
+ }
1907
+ : { ...transfer, operation: op });
1908
+ else {
1909
+ if (op !== "internal_transfer.create")
1910
+ fail(move, "fees settle with a posted transfer", "declare a separate posted settlement after releasing the reservation");
1911
+ const terms = entries(asBlock(resolve(fee)));
1912
+ for (const term of terms.keys())
1913
+ if (!["seller", "buyer", "tax"].includes(term))
1914
+ fail(fee, `unknown fee term ${term}`, "choose seller or buyer, and optional tax");
1915
+ const paidBy = terms.has("seller") ? "seller" : "buyer";
1916
+ if (terms.has("seller") === terms.has("buyer"))
1917
+ fail(fee, "fee needs exactly one paying party", "write seller: 1% or buyer: 1%");
1918
+ const quoted = terms.get(paidBy);
1919
+ const rate = quoted.kind === "capped" ? quoted.rate : quoted;
1920
+ if (rate.kind !== "percent")
1921
+ fail(rate, "fee rate needs a percentage", "write a percentage such as 1%");
1922
+ const prefix = `${name}_${key}`;
1923
+ const derived = (suffix, calculation) => {
1924
+ const target = prefix + "_" + suffix;
1925
+ if (fields.some((f) => f.name === target))
1926
+ fail(move, `generated fee field ${target} conflicts with a field`, "rename the authored field or move key");
1927
+ fields.push({ name: target, type: "money" });
1928
+ calculations.push({
1929
+ ...calculation,
1930
+ target,
1931
+ });
1932
+ return { field: `self.${target}` };
1933
+ };
1934
+ const gross = derived("feeGross", {
1935
+ op: "rate",
1936
+ base: transfer.amount,
1937
+ bps: { literal: literal(rate) },
1938
+ rounding: "floor",
1939
+ });
1940
+ let charge = gross;
1941
+ if (quoted.kind === "capped") {
1942
+ if (quoted.cap.kind !== "money")
1943
+ fail(quoted.cap, "fee cap needs money", "write an amount such as 750 SAR");
1944
+ charge = derived("fee", {
1945
+ op: "minimum",
1946
+ values: [gross, { literal: literal(quoted.cap) }],
1947
+ });
1948
+ }
1949
+ const tax = terms.get("tax");
1950
+ if (tax && tax.kind !== "percent")
1951
+ fail(tax, "tax needs a percentage of the fee", "write tax: 15%");
1952
+ const vat = tax
1953
+ ? derived("tax", {
1954
+ op: "rate",
1955
+ base: charge,
1956
+ bps: { literal: literal(tax) },
1957
+ rounding: "floor",
1958
+ })
1959
+ : undefined;
1960
+ const charges = vat ? [charge, vat] : [charge];
1961
+ const net = paidBy === "seller"
1962
+ ? derived("net", {
1963
+ op: "subtract",
1964
+ base: transfer.amount,
1965
+ subtract: charges,
1966
+ })
1967
+ : transfer.amount;
1968
+ for (const [party, role] of [
1969
+ ["programOperator", "program_operator"],
1970
+ ["programTax", "tax_payable"],
1971
+ ]) {
1972
+ if (document.parties[party] &&
1973
+ document.parties[party].role !== role)
1974
+ fail(move, `${party} is reserved for fees`, "rename this party");
1975
+ document.parties[party] = { kind: "business", role };
1976
+ }
1977
+ a.moves.push({
1978
+ ...transfer,
1979
+ operation: "internal_transfer.create",
1980
+ amount: net,
1981
+ });
1982
+ a.moves.push({
1983
+ key: key + "Fee",
1984
+ ...(feeEconomics?.has("fee")
1985
+ ? { economics: lowerEconomics(feeEconomics.get("fee")) }
1986
+ : {}),
1987
+ operation: "internal_transfer.create",
1988
+ amount: charge,
1989
+ from: transfer.from,
1990
+ to: "party.programOperator",
1991
+ });
1992
+ if (vat)
1993
+ a.moves.push({
1994
+ key: key + "Tax",
1995
+ ...(feeEconomics?.has("tax")
1996
+ ? { economics: lowerEconomics(feeEconomics.get("tax")) }
1997
+ : {}),
1998
+ operation: "internal_transfer.create",
1999
+ amount: vat,
2000
+ from: transfer.from,
2001
+ to: "party.programTax",
2002
+ });
2003
+ }
2004
+ }
2005
+ else if (op === "internal_transfer.post" ||
2006
+ op === "internal_transfer.void")
2007
+ a.moves.push({
2008
+ key,
2009
+ operation: op,
2010
+ ...(economics ? { economics } : {}),
2011
+ transfer: path(parts.get("transfer")),
2012
+ ...(parts.has("capture")
2013
+ ? { capture: String(data(parts.get("capture"))) }
2014
+ : {}),
2015
+ });
2016
+ else
2017
+ fail(move, `unknown transfer instruction ${op}`, "choose create, reserve, post, or void from internal_transfer");
2018
+ for (const item of a.moves.slice(lowered))
2019
+ authored.set(item, move);
2020
+ }
2021
+ }
2022
+ else
2023
+ a[key] = data(expr);
2024
+ }
2025
+ if (attachmentInfo && !attachmentInfo.child) {
2026
+ if (attachmentInfo.exposed.has(name)) {
2027
+ if (automatic(a.actor))
2028
+ fail(row, `${name} runs on the clock or its parent, not a caller`, "expose only caller actions");
2029
+ a.publicAction = attachmentInfo.exposed.get(name);
2030
+ }
2031
+ else {
2032
+ delete a.publicAction;
2033
+ }
2034
+ }
2035
+ if (automatic(a.actor))
2036
+ delete a.publicAction;
2037
+ const checkSubjectPaths = (obj, key = "") => {
2038
+ if (typeof obj === "string") {
2039
+ if ([
2040
+ "field",
2041
+ "fields",
2042
+ "reference",
2043
+ "anchor",
2044
+ "subject",
2045
+ "at",
2046
+ "instruction",
2047
+ ].includes(key) &&
2048
+ obj.startsWith("subject.")) {
2049
+ const subField = obj.split(".")[1];
2050
+ const req = a.subject?.requirements.find((r) => r.field.name === subField);
2051
+ if (!req) {
2052
+ failWithCode(row, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${name}`, `declare ${subField} in subject { ... }`);
2053
+ }
2054
+ }
2055
+ }
2056
+ else if (Array.isArray(obj)) {
2057
+ for (const item of obj)
2058
+ checkSubjectPaths(item, key);
2059
+ }
2060
+ else if (obj !== null && typeof obj === "object") {
2061
+ for (const [key, val] of Object.entries(obj))
2062
+ checkSubjectPaths(val, key);
2063
+ }
2064
+ };
2065
+ checkSubjectPaths(a.requires);
2066
+ checkSubjectPaths(a.set);
2067
+ checkSubjectPaths(a.invoke);
2068
+ currentAction = undefined;
2069
+ currentActionName = undefined;
2070
+ for (const requirement of a.subject?.requirements ?? []) {
2071
+ const entry = asBlock(subjectExpr).entries.find((entry) => entry.key === requirement.field.name) ??
2072
+ asBlock(subjectExpr).entries.find((entry) => entry.key === "adapter");
2073
+ requirementOrigins.set(requirement, {
2074
+ source: declarationSources.get(decl) ?? "program",
2075
+ span: entry?.span ?? row.span,
2076
+ message: `${id}.${name}.subject.${requirement.field.name}`,
2077
+ });
2078
+ }
2079
+ inst.actions[name] = a;
2080
+ inst.actionOrder.push(name);
2081
+ }
2082
+ if (attachmentInfo && !attachmentInfo.child)
2083
+ applyAttachmentEconomics(document, inst, attachmentInfo.parties, attachmentInfo.economics, data);
2084
+ for (const key of [
2085
+ "invariants",
2086
+ "reports",
2087
+ "revisioned",
2088
+ "scope",
2089
+ ])
2090
+ if (body.has(key))
2091
+ inst[key] = data(body.get(key));
2092
+ let usesTax = false;
2093
+ for (const action of Object.values(inst.actions)) {
2094
+ for (const move of action.moves) {
2095
+ if (!("from" in move))
2096
+ continue;
2097
+ for (const endpoint of ["from", "to"]) {
2098
+ if (move[endpoint] === "party.programTax") {
2099
+ move[endpoint] = "self.programTaxPayable";
2100
+ usesTax = true;
2101
+ }
2102
+ }
2103
+ }
2104
+ }
2105
+ if (usesTax) {
2106
+ if (inst.fields.some((field) => field.name === "programTaxPayable"))
2107
+ fail(decl, "programTaxPayable is reserved for the tax account", "rename the authored field");
2108
+ inst.fields.push({
2109
+ name: "programTaxPayable",
2110
+ type: "account",
2111
+ owner: "programOperator",
2112
+ book: "cash",
2113
+ key: "programTax",
2114
+ });
2115
+ }
2116
+ const shape = udlInstrumentSchema.safeParse(inst);
2117
+ if (!shape.success) {
2118
+ const issue = shape.error.issues[0];
2119
+ const actionName = issue.path[0] === "actions" ? String(issue.path[1]) : undefined;
2120
+ const node = decl.body.entries.find((entry) => entry.key ===
2121
+ (actionName ? `action ${actionName}` : String(issue.path[0]))) ?? decl;
2122
+ failWithCode({
2123
+ span: node.span,
2124
+ source: declarationSources.get(decl) ?? "program",
2125
+ }, "UDL1003", `${id}.${issue.path.join(".")}: ${issue.message}`, "use the UDL typed clause shape");
2126
+ }
2127
+ origins.push({
2128
+ path: `$.instruments[${document.instruments.length}]`,
2129
+ span: { ...origin, ...lineColAt(source, origin.start) },
2130
+ ...(standardOrigin ? { standardBlock: standardOrigin } : {}),
2131
+ });
2132
+ if (familyDeclaration && familyDeclaration.revision !== undefined) {
2133
+ inst.family = {
2134
+ module: familyDeclaration.module,
2135
+ exportPath: familyDeclaration.exportPath,
2136
+ revision: familyDeclaration.revision,
2137
+ };
2138
+ }
2139
+ document.instruments.push(inst);
2140
+ declarations.set(inst, decl);
2141
+ for (const [key, definition] of records) {
2142
+ const childAttachment = attachmentInfo
2143
+ ? {
2144
+ ...attachmentInfo,
2145
+ attachmentName: `${attachmentInfo.attachmentName}_${key}`,
2146
+ child: true,
2147
+ exposed: new Map(),
2148
+ }
2149
+ : undefined;
2150
+ if (childAttachment)
2151
+ childAttachment.attachments.push({
2152
+ name: childAttachment.attachmentName,
2153
+ parent: attachmentInfo.attachmentName,
2154
+ instrument: `${id}_${key}`,
2155
+ parties: childAttachment.parties,
2156
+ });
2157
+ const child = {
2158
+ kind: "instrument",
2159
+ name: key,
2160
+ parameters: [],
2161
+ body: asBlock(definition),
2162
+ span: origin,
2163
+ };
2164
+ declarationSources.set(child, declarationSources.get(decl) ?? "program");
2165
+ const childFamily = familyDeclaration
2166
+ ? {
2167
+ module: familyDeclaration.module,
2168
+ exportPath: `${familyDeclaration.exportPath}.${key}`,
2169
+ ...(familyDeclaration.revision !== undefined
2170
+ ? { revision: familyDeclaration.revision }
2171
+ : {}),
2172
+ }
2173
+ : undefined;
2174
+ addInstrument(child, `${id}_${key}`, emptyBlock, origin, new Map([
2175
+ ...environment,
2176
+ ["parent", { kind: "name", value: id, span: origin }],
2177
+ ]), enums, childAttachment, childFamily, standardOrigin
2178
+ ? {
2179
+ ...standardOrigin,
2180
+ recordPath: [standardOrigin.recordPath, key]
2181
+ .filter(Boolean)
2182
+ .join("."),
2183
+ }
2184
+ : undefined);
2185
+ }
2186
+ };
2187
+ const compileObject = (decl) => {
2188
+ const body = entries(decl.body);
2189
+ for (const key of body.keys()) {
2190
+ if (key !== "fields" &&
2191
+ key !== "columns" &&
2192
+ key !== "entryActions" &&
2193
+ !key.startsWith("attach ")) {
2194
+ fail(decl, `unknown object clause ${key}`, "use fields, columns, entryActions, or attach");
2195
+ }
2196
+ }
2197
+ const authoredFields = [];
2198
+ const authoredNames = [];
2199
+ const fieldsBlock = asBlock(body.get("fields"));
2200
+ for (const row of fieldsBlock.entries) {
2201
+ if (RESERVED_OBJECT_NAMES.some((name) => name === row.key)) {
2202
+ fail(row, `${row.key} is a reserved object name`, "rename this field");
2203
+ }
2204
+ const fieldDef = lowerObjectField(row);
2205
+ authoredFields.push(fieldDef);
2206
+ authoredNames.push(row.key);
2207
+ }
2208
+ const attachments = [];
2209
+ for (const entry of decl.body.entries) {
2210
+ if (!entry.key.startsWith("attach "))
2211
+ continue;
2212
+ const match = /^attach\s+([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(entry.key);
2213
+ if (!match) {
2214
+ fail(entry, "invalid attach syntax", "write attach name = template { ... }");
2215
+ }
2216
+ const attachmentName = match[1];
2217
+ const targetTemplate = match[2];
2218
+ const template = templates.get(targetTemplate);
2219
+ if (!template) {
2220
+ fail(entry, `unknown instrument ${targetTemplate}`, `add use ${targetTemplate.split(".")[0]} and choose a declared instrument`);
2221
+ }
2222
+ const instId = `${decl.name}_${attachmentName}`;
2223
+ const attachmentBlock = asBlock(entry.value);
2224
+ const renames = new Map();
2225
+ const renameEntries = new Map();
2226
+ const exposed = new Map();
2227
+ const economics = [];
2228
+ const tunableEntries = [];
2229
+ for (const row of attachmentBlock.entries) {
2230
+ if (row.key.startsWith("economics ")) {
2231
+ economics.push(row);
2232
+ }
2233
+ else if (row.key === "rename") {
2234
+ for (const r of asBlock(row.value).entries) {
2235
+ if (renames.has(r.key))
2236
+ fail(r, `duplicate rename ${r.key}`, "rename each subject field once");
2237
+ renames.set(r.key, text(r.value));
2238
+ renameEntries.set(r.key, r);
2239
+ }
2240
+ }
2241
+ else if (row.key === "expose") {
2242
+ if (row.value.kind === "call") {
2243
+ const actionName = row.value.name;
2244
+ const publicName = text(row.value.args[0]);
2245
+ if (exposed.has(actionName) ||
2246
+ !template.body.entries.some((entry) => entry.key === `action ${actionName}`))
2247
+ fail(row, `unknown or repeated action ${actionName}`, "expose one declared action once");
2248
+ exposed.set(actionName, publicName);
2249
+ exposures.push({
2250
+ instrument: instId,
2251
+ action: actionName,
2252
+ entry: row,
2253
+ });
2254
+ }
2255
+ }
2256
+ else {
2257
+ tunableEntries.push(row);
2258
+ }
2259
+ }
2260
+ const parties = {};
2261
+ attachments.push({ name: attachmentName, instrument: instId, parties });
2262
+ const tunableBlock = {
2263
+ kind: "block",
2264
+ entries: tunableEntries,
2265
+ span: entry.value.span,
2266
+ };
2267
+ const templateFamily = resolveFamily(targetTemplate, entry, false);
2268
+ const firstAttachedInstrument = document.instruments.length;
2269
+ try {
2270
+ addInstrument(template, instId, tunableBlock, entry.span, new Map(), new Map(), {
2271
+ subjectKindId: decl.name,
2272
+ attachmentName,
2273
+ renames,
2274
+ exposed,
2275
+ parties,
2276
+ attachments,
2277
+ economics,
2278
+ }, templateFamily ? { ...templateFamily } : undefined, standardOrigins.get(template));
2279
+ }
2280
+ catch (error) {
2281
+ if (!(error instanceof CompileFailure))
2282
+ throw error;
2283
+ bindingDiagnostics.push(error.diagnostic);
2284
+ continue;
2285
+ }
2286
+ const attachedInst = document.instruments.find((i) => i.id === instId);
2287
+ for (const createdInst of document.instruments
2288
+ .slice(firstAttachedInstrument)
2289
+ .filter((instrument) => instrument.id === instId ||
2290
+ instrument.actions.create?.publicAction)) {
2291
+ if (!createdInst.actions.create)
2292
+ continue;
2293
+ const owned = new Set(createdInst.calculate.map((node) => node.target));
2294
+ for (const action of Object.values(createdInst.actions)) {
2295
+ for (const node of action.calculate ?? [])
2296
+ owned.add(node.target);
2297
+ for (const move of action.moves)
2298
+ if ("capture" in move && move.capture)
2299
+ owned.add(move.capture);
2300
+ }
2301
+ const create = createdInst.actions.create;
2302
+ for (const field of createdInst.fields) {
2303
+ if (field.type === "account" ||
2304
+ (field.type === "ref" && field.targetKind === "instrument") ||
2305
+ (field.type === "list" &&
2306
+ field.item === "ref" &&
2307
+ field.targetKind === "instrument") ||
2308
+ field.optional ||
2309
+ "value" in field ||
2310
+ owned.has(field.name) ||
2311
+ create.input.some((input) => input.name === field.name))
2312
+ continue;
2313
+ create.subject ??= { requirements: [], adapters: [] };
2314
+ const existing = create.subject.requirements.find((item) => item.field.name === field.name);
2315
+ if (existing) {
2316
+ if (canonicalJson(existing.field) !== canonicalJson(field))
2317
+ failWithCode(entry, "subject_field_conflict", `${createdInst.id}.create.subject.${field.name} conflicts with its instrument field`, "use the instrument field's type and constraints");
2318
+ continue;
2319
+ }
2320
+ const objectField = renames.get(field.name);
2321
+ const requirement = {
2322
+ field,
2323
+ ...(objectField ? { objectField } : {}),
2324
+ };
2325
+ create.subject.requirements.push(requirement);
2326
+ requirementOrigins.set(requirement, {
2327
+ source: declarationSources.get(template) ?? "program",
2328
+ span: entry.span,
2329
+ message: `${createdInst.id}.create.fields.${field.name}`,
2330
+ });
2331
+ }
2332
+ }
2333
+ for (const [oldName] of renames) {
2334
+ const found = attachedInst &&
2335
+ Object.values(attachedInst.actions).some((action) => action.subject?.requirements.some((requirement) => requirement.field.name === oldName));
2336
+ if (!found) {
2337
+ const renameEntry = renameEntries.get(oldName) ?? entry;
2338
+ const declared = attachedInst
2339
+ ? Object.values(attachedInst.actions).flatMap((action) => action.subject?.requirements.map((requirement) => requirement.field.name) ?? [])
2340
+ : [];
2341
+ failWithCode(renameEntry, "subject_field_unknown", `rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`, declared.length
2342
+ ? `rename one of: ${[...new Set(declared)].join(", ")}`
2343
+ : "rename a declared subject requirement");
2344
+ }
2345
+ }
2346
+ }
2347
+ const columnsExpr = body.get("columns");
2348
+ let columns = [];
2349
+ if (columnsExpr) {
2350
+ if (columnsExpr.kind !== "list") {
2351
+ fail(columnsExpr, "columns needs a list of field names", "write columns: [name, ...]");
2352
+ }
2353
+ columns = columnsExpr.items.map(text);
2354
+ if (columns.length > 8) {
2355
+ fail(columnsExpr, `The object list selects ${columns.length} columns; this release supports 8. The object may retain all its fields.`, `Remove ${columns.length === 9 ? "one name" : `${columns.length - 8} names`} from \`columns\`, not from \`fields\`.`);
2356
+ }
2357
+ }
2358
+ const entryExpr = body.get("entryActions");
2359
+ let entryActions;
2360
+ if (entryExpr) {
2361
+ if (entryExpr.kind !== "list" || !entryExpr.items.length)
2362
+ fail(entryExpr, "entryActions needs a nonempty list", "name exposed create actions bound to actor");
2363
+ entryActions = entryExpr.items.map(text);
2364
+ }
2365
+ document.objects.push({
2366
+ id: decl.name,
2367
+ title: decl.title,
2368
+ ...(entryActions ? { entryActions } : {}),
2369
+ authoredFields: authoredNames,
2370
+ attachments,
2371
+ fields: authoredFields,
2372
+ columns,
2373
+ });
2374
+ };
2375
+ for (const decl of program.decls) {
2376
+ if (decl.kind === "instrument") {
2377
+ addInstrument(decl, decl.name, emptyBlock, decl.span);
2378
+ }
2379
+ if (decl.kind === "assignment") {
2380
+ const template = templates.get(decl.target);
2381
+ if (!template)
2382
+ fail(decl, `unknown instrument ${decl.target}`, `add use ${decl.target.split(".")[0]} and choose a declared instrument`);
2383
+ const templateFamily = resolveFamily(decl.target, decl, false);
2384
+ addInstrument(template, decl.name, decl.body, decl.span, new Map(), new Map(), undefined, templateFamily ? { ...templateFamily } : undefined, standardOrigins.get(template));
2385
+ }
2386
+ if (decl.kind === "object") {
2387
+ compileObject(decl);
2388
+ }
2389
+ }
2390
+ if (bindingDiagnostics.length)
2391
+ return {
2392
+ verdict: "invalid",
2393
+ diagnostics: distinct(bindingDiagnostics)
2394
+ .sort((a, b) => a.span.start - b.span.start)
2395
+ .map((d) => diagnostic(d, "check")),
2396
+ };
2397
+ // Resolve selectors after every attachment and child has its family.
2398
+ const retainSelectionFamilies = (value) => {
2399
+ if (!value || typeof value !== "object")
2400
+ return;
2401
+ const record = value;
2402
+ const selection = record.selection;
2403
+ if (selection && !selection.family) {
2404
+ const families = [selection.instrument]
2405
+ .flat()
2406
+ .map((id) => document.instruments.find((item) => item.id === id)?.family);
2407
+ const family = families[0];
2408
+ if (family &&
2409
+ families.every((item) => canonicalJson(item) === canonicalJson(family)))
2410
+ selection.family = family;
2411
+ }
2412
+ for (const child of Object.values(record))
2413
+ retainSelectionFamilies(child);
2414
+ };
2415
+ retainSelectionFamilies(document);
2416
+ // Propagate invoked requirements with the conditions on each invocation path.
2417
+ let changedInvocations = true;
2418
+ let invocationIterations = 0;
2419
+ const invocationDepth = document.instruments.reduce((total, inst) => total + inst.actionOrder.length, 0);
2420
+ while (changedInvocations && invocationIterations < invocationDepth) {
2421
+ changedInvocations = false;
2422
+ invocationIterations++;
2423
+ for (const inst of document.instruments) {
2424
+ for (const [actionName, action] of Object.entries(inst.actions)) {
2425
+ for (const call of action.invoke ?? []) {
2426
+ let targetIds = [];
2427
+ if ("instrument" in call) {
2428
+ targetIds = [call.instrument];
2429
+ }
2430
+ else if ("selection" in call) {
2431
+ targetIds = Array.isArray(call.selection.instrument)
2432
+ ? call.selection.instrument
2433
+ : [call.selection.instrument];
2434
+ }
2435
+ else if ("reference" in call) {
2436
+ const refField = resolveField(document, inst, call.reference, action.input, action);
2437
+ if (refField?.type === "ref" &&
2438
+ refField.targetKind === "instrument") {
2439
+ targetIds = Array.isArray(refField.target)
2440
+ ? refField.target
2441
+ : [refField.target];
2442
+ }
2443
+ }
2444
+ for (const targetId of targetIds) {
2445
+ const targetInst = document.instruments.find((i) => i.id === targetId);
2446
+ if (!targetInst)
2447
+ continue;
2448
+ if (inst.subject &&
2449
+ targetInst.subject &&
2450
+ inst.subject === targetInst.subject) {
2451
+ const targetAction = targetInst.actions[call.action];
2452
+ if (!targetAction?.subject)
2453
+ continue;
2454
+ if (!action.subject) {
2455
+ action.subject = { requirements: [], adapters: [] };
2456
+ }
2457
+ for (const adapter of call.guard
2458
+ ? []
2459
+ : targetAction.subject.adapters) {
2460
+ if (!action.subject.adapters.some((existing) => existing.binding === adapter.binding)) {
2461
+ action.subject.adapters.push(adapter);
2462
+ changedInvocations = true;
2463
+ }
2464
+ }
2465
+ for (const targetReq of targetAction.subject.requirements) {
2466
+ const targetObjFieldName = targetReq.objectField ?? targetReq.field.name;
2467
+ const existing = action.subject.requirements.find((cr) => (cr.objectField ?? cr.field.name) === targetObjFieldName);
2468
+ const guardValue = (value) => {
2469
+ if (!("field" in value) ||
2470
+ !value.field.startsWith("subject."))
2471
+ return value;
2472
+ const requirement = action.subject?.requirements.find((item) => item.field.name === value.field.slice(8));
2473
+ return {
2474
+ field: `subject.${requirement?.objectField ?? requirement?.field.name ?? value.field.slice(8)}`,
2475
+ };
2476
+ };
2477
+ const guardType = call.guard &&
2478
+ [call.guard.left, call.guard.right]
2479
+ .flatMap((value) => "field" in value
2480
+ ? [
2481
+ resolveField(document, inst, value.field, action.input, action)?.type,
2482
+ ]
2483
+ : [])
2484
+ .find((type) => type === "money" || type === "date");
2485
+ const condition = call.guard
2486
+ ? {
2487
+ instrument: inst.id,
2488
+ action: actionName,
2489
+ guard: {
2490
+ ...call.guard,
2491
+ left: guardValue(call.guard.left),
2492
+ right: guardValue(call.guard.right),
2493
+ },
2494
+ ...(guardType === "money" || guardType === "date"
2495
+ ? { valueType: guardType }
2496
+ : {}),
2497
+ }
2498
+ : undefined;
2499
+ const when = condition
2500
+ ? (targetReq.when ?? [[]]).map((path) => [
2501
+ condition,
2502
+ ...path.filter((item) => canonicalJson(item) !== canonicalJson(condition)),
2503
+ ])
2504
+ : targetReq.when;
2505
+ if (!existing) {
2506
+ const inherited = {
2507
+ field: { ...targetReq.field, name: targetObjFieldName },
2508
+ ...(when ? { when: when.map((path) => [...path]) } : {}),
2509
+ };
2510
+ action.subject.requirements.push(inherited);
2511
+ requirementOrigins.set(inherited, requirementOrigins.get(targetReq));
2512
+ changedInvocations = true;
2513
+ }
2514
+ else if (!sameObjectField(existing.field, targetReq.field)) {
2515
+ const first = requirementOrigins.get(existing);
2516
+ const second = requirementOrigins.get(targetReq);
2517
+ throw new CompileFailure({
2518
+ code: "subject_field_conflict",
2519
+ message: `${first.message} conflicts with ${second.message}: incompatible requirement '${targetObjFieldName}'`,
2520
+ fix: "ensure compatible requirement definitions across invoked actions",
2521
+ span: first.span,
2522
+ source: first.source,
2523
+ related: [first, second],
2524
+ });
2525
+ }
2526
+ else if (existing.when) {
2527
+ if (!when) {
2528
+ delete existing.when;
2529
+ changedInvocations = true;
2530
+ }
2531
+ else {
2532
+ for (const path of when) {
2533
+ if (!existing.when.some((known) => canonicalJson(known) === canonicalJson(path))) {
2534
+ existing.when.push(path);
2535
+ changedInvocations = true;
2536
+ }
2537
+ }
2538
+ }
2539
+ }
2540
+ }
2541
+ }
2542
+ }
2543
+ }
2544
+ }
2545
+ }
2546
+ }
2547
+ for (const kind of document.objects) {
2548
+ const origins = new Map(kind.fields.map((field) => [
2549
+ field.name,
2550
+ `authored field ${kind.id}.${field.name}`,
2551
+ ]));
2552
+ for (const instrument of document.instruments.filter((item) => item.subject === kind.id)) {
2553
+ for (const name of instrument.actionOrder) {
2554
+ for (const requirement of instrument.actions[name].subject
2555
+ ?.requirements ?? []) {
2556
+ const field = {
2557
+ ...requirement.field,
2558
+ name: requirement.objectField ?? requirement.field.name,
2559
+ };
2560
+ const previous = kind.fields.find((item) => item.name === field.name);
2561
+ const origin = `${instrument.id}.${name}.subject.${requirement.field.name}`;
2562
+ if (previous && !sameObjectField(previous, field)) {
2563
+ failWithCode(objects.get(kind.id), "subject_field_conflict", `${origin} conflicts with ${origins.get(field.name)}: incompatible types or constraints`, "rename fields with different meanings");
2564
+ }
2565
+ if (!previous) {
2566
+ kind.fields.push(field);
2567
+ origins.set(field.name, origin);
2568
+ }
2569
+ }
2570
+ }
2571
+ }
2572
+ for (const column of kind.columns) {
2573
+ if (!kind.fields.some((field) => field.name === column))
2574
+ failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
2575
+ }
2576
+ }
2577
+ const byId = new Map(document.instruments.map((inst) => [inst.id, inst]));
2578
+ const site = (owner, key, fallback) => {
2579
+ const expr = authored.get(owner);
2580
+ if (expr?.kind === "block")
2581
+ return expr.entries.find((entry) => entry.key === key)?.value ?? expr;
2582
+ if (expr?.kind === "list" && Array.isArray(owner))
2583
+ return expr.items.length === owner.length
2584
+ ? expr.items[Number(key)]
2585
+ : expr;
2586
+ return expr ?? fallback;
2587
+ };
2588
+ const keySite = (owner, key, fallback) => {
2589
+ const expr = authored.get(owner);
2590
+ const entry = expr?.kind === "block"
2591
+ ? expr.entries.find((item) => item.key === key)
2592
+ : undefined;
2593
+ if (!entry)
2594
+ return fallback;
2595
+ return {
2596
+ ...entry,
2597
+ span: { start: entry.span.start, end: entry.span.start + key.length },
2598
+ };
2599
+ };
2600
+ const builtins = (inst) => [
2601
+ "id",
2602
+ "now",
2603
+ "createdAt",
2604
+ "productRevision",
2605
+ "status",
2606
+ ...(inst.subject ? ["subject"] : []),
2607
+ ];
2608
+ const parties = (inst) => [
2609
+ ...Object.keys(document.parties),
2610
+ ...(inst.subject ? subjectPartyRoles : []),
2611
+ ];
2612
+ const members = (field) => {
2613
+ if (field.type === "account")
2614
+ return ["balance", "reserved"];
2615
+ if (field.type !== "ref")
2616
+ return [];
2617
+ const [id] = [field.target].flat();
2618
+ const target = field.targetKind === "object"
2619
+ ? document.objects.find((object) => object.id === id)
2620
+ : byId.get(id);
2621
+ if (!target)
2622
+ return [];
2623
+ return [
2624
+ ...target.fields.map((item) => item.name),
2625
+ ...("lifecycle" in target ? builtins(target) : []),
2626
+ ];
2627
+ };
2628
+ const checkParty = (name, at, inst) => {
2629
+ const names = parties(inst);
2630
+ if (!names.includes(name))
2631
+ fail(at, `\`${name}\` is not a declared party.${didYouMean(name, names)}`, `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`);
2632
+ };
2633
+ const checkInput = (name, at, label, input) => {
2634
+ const names = input.map((field) => field.name);
2635
+ if (!names.includes(name))
2636
+ fail(at, `\`${name}\` is not an input of \`${label}\`.${didYouMean(name, names)}`, `Declare \`${name}\` in the input of \`${label}\`${names.length ? `, or use one of ${names.join(", ")}` : ""}.`);
2637
+ };
2638
+ const checkTarget = (id, at, objects = false) => {
2639
+ const ids = [
2640
+ ...byId.keys(),
2641
+ ...(objects ? document.objects.map((object) => object.id) : []),
2642
+ ];
2643
+ const noun = objects ? "instrument or object" : "instrument";
2644
+ if (!ids.includes(id))
2645
+ fail(at, `\`${id}\` is not a declared ${noun}.${didYouMean(id, ids)}`, `Name a declared ${noun}.`);
2646
+ };
2647
+ const checkPath = (path, at, scope) => {
2648
+ const { inst, action } = scope;
2649
+ const parts = path.split(".");
2650
+ const found = (count) => resolveField(document, inst, parts.slice(0, count).join("."), action?.input, action);
2651
+ // Subject paths keep their own check against the action's requirements.
2652
+ if (found(parts.length) || parts[0] === "subject")
2653
+ return;
2654
+ if (parts[0] === "party" && parts[1]) {
2655
+ checkParty(parts[1], at, inst);
2656
+ if (!found(2))
2657
+ return;
2658
+ }
2659
+ if (!["self", "input", "party"].includes(parts[0]))
2660
+ parts.unshift("self");
2661
+ let count = 2;
2662
+ while (count <= parts.length && found(count))
2663
+ count++;
2664
+ if (count > parts.length)
2665
+ return;
2666
+ const name = parts[count - 1];
2667
+ if (count > 2) {
2668
+ const prefix = parts.slice(0, count - 1).join(".");
2669
+ const names = members(found(count - 1));
2670
+ fail(at, `\`${name}\` is not a field of \`${prefix}\`.${didYouMean(name, names)}`, names.length
2671
+ ? `Use one of ${names.join(", ")}.`
2672
+ : `\`${prefix}\` has no fields.`);
2673
+ }
2674
+ if (parts[0] === "input")
2675
+ return checkInput(name, at, scope.label, action?.input ?? []);
2676
+ const declared = declarations.get(inst).name;
2677
+ const fields = inst.fields.map((field) => field.name);
2678
+ fail(at, `\`${name}\` is not a field or party of \`${declared}\`.${didYouMean(name, [...fields, ...builtins(inst), ...parties(inst)])}`, `Use a field declared in \`${declared}\` (${fields.join(", ")}) or a declared party (${parties(inst).join(", ")}).`);
2679
+ };
2680
+ const scopeOf = (inst) => ({
2681
+ inst,
2682
+ label: declarations.get(inst).name,
2683
+ });
2684
+ const checkSelection = (selection, at, scope) => {
2685
+ walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
2686
+ walk(selection.where, "where", site(selection, "where", at), scope);
2687
+ for (const id of [selection.instrument].flat()) {
2688
+ checkTarget(id, site(selection, "instrument", at));
2689
+ const target = scopeOf(byId.get(id));
2690
+ const member = (key, where) => checkPath(key.startsWith("self.") ? key : `self.${key}`, where, target);
2691
+ for (const key of Object.keys(selection.where ?? {}))
2692
+ member(key, keySite(selection.where, key, at));
2693
+ member(selection.reference, site(selection, "reference", at));
2694
+ selection.order?.forEach((key, index) => member(key, site(selection.order, index, at)));
2695
+ if (selection.window)
2696
+ member(selection.window.field, site(selection.window, "field", at));
2697
+ }
2698
+ };
2699
+ const checkCall = (call, at, scope) => {
2700
+ const bind = "range" in call ? call.range?.bind : undefined;
2701
+ for (const [key, value] of Object.entries(call))
2702
+ if (key !== "input")
2703
+ walk(value, key, site(call, key, at), scope);
2704
+ for (const [key, value] of Object.entries(call.input))
2705
+ if (!("field" in value) || value.field !== bind)
2706
+ walk(value, key, site(call.input, key, at), scope);
2707
+ if ("instrument" in call)
2708
+ checkTarget(call.instrument, site(call, "instrument", at));
2709
+ const reference = "reference" in call
2710
+ ? resolveField(document, scope.inst, call.reference, scope.action?.input, scope.action)
2711
+ : undefined;
2712
+ const ids = "instrument" in call
2713
+ ? [call.instrument]
2714
+ : "selection" in call
2715
+ ? [call.selection.instrument].flat()
2716
+ : reference?.type === "ref" && reference.targetKind === "instrument"
2717
+ ? [reference.target].flat()
2718
+ : [];
2719
+ for (const id of ids) {
2720
+ const target = byId.get(id);
2721
+ if (!target)
2722
+ continue;
2723
+ const action = target.actions[call.action];
2724
+ if (!action)
2725
+ fail(site(call, "action", at), `\`${call.action}\` is not an action of \`${id}\`.${didYouMean(call.action, target.actionOrder)}`, `Use one of ${target.actionOrder.join(", ")}.`);
2726
+ for (const key of Object.keys(call.input))
2727
+ checkInput(key, keySite(call.input, key, at), `${id}.${call.action}`, action.input);
2728
+ }
2729
+ };
2730
+ const pathKeys = [
2731
+ "field",
2732
+ "reference",
2733
+ "anchor",
2734
+ "at",
2735
+ "list",
2736
+ "from",
2737
+ "to",
2738
+ "transfer",
2739
+ "subject",
2740
+ "instruction",
2741
+ "installment",
2742
+ "recipient",
2743
+ "dueAt",
2744
+ "fields",
2745
+ ];
2746
+ const walk = (value, key, at, scope) => {
2747
+ if (typeof value === "string") {
2748
+ if (key === "party")
2749
+ checkParty(value, at, scope.inst);
2750
+ if (key === "target")
2751
+ checkPath(`self.${value}`, at, scope);
2752
+ if (pathKeys.includes(key))
2753
+ checkPath(value, at, scope);
2754
+ return;
2755
+ }
2756
+ if (!value || typeof value !== "object")
2757
+ return;
2758
+ if (Array.isArray(value))
2759
+ return value.forEach((item, index) => walk(item, key, site(value, index, at), scope));
2760
+ if (key === "selection")
2761
+ return checkSelection(value, at, scope);
2762
+ if (key === "invoke")
2763
+ return checkCall(value, at, scope);
2764
+ if (key === "set")
2765
+ for (const name of Object.keys(value))
2766
+ checkPath(`self.${name}`, keySite(value, name, at), scope);
2767
+ for (const [child, item] of Object.entries(value))
2768
+ walk(item, child, site(value, child, at), scope);
2769
+ };
2770
+ for (const inst of document.instruments) {
2771
+ const decl = declarations.get(inst);
2772
+ const scope = scopeOf(inst);
2773
+ for (const field of [
2774
+ ...inst.fields,
2775
+ ...Object.values(inst.actions).flatMap((action) => action.input),
2776
+ ])
2777
+ if ("target" in field && field.target !== undefined)
2778
+ for (const id of [field.target].flat())
2779
+ checkTarget(id, site(field, "target", decl), true);
2780
+ walk(inst.calculate, "calculate", decl, scope);
2781
+ walk(inst.invariants, "invariants", decl, scope);
2782
+ for (const [name, action] of Object.entries(inst.actions))
2783
+ for (const [key, value] of Object.entries(action))
2784
+ if (key !== "input" && key !== "subject")
2785
+ walk(value, key, site(action, key, decl), {
2786
+ inst,
2787
+ label: `${decl.name}.${name}`,
2788
+ action,
2789
+ });
2790
+ }
2791
+ const eliminatedStates = new Map();
2792
+ // Remove branches excluded by immutable tunables before checking reference states.
2793
+ for (const inst of document.instruments) {
2794
+ if (!inst.lifecycle.states.includes(inst.lifecycle.initial) ||
2795
+ Object.values(inst.lifecycle.transitions).some((edge) => edge.from.some((state) => !inst.lifecycle.states.includes(state)) ||
2796
+ (edge.to !== "preserve" &&
2797
+ !inst.lifecycle.states.includes(edge.to))))
2798
+ continue;
2799
+ let specialized = false;
2800
+ for (const [key, action] of Object.entries(inst.actions)) {
2801
+ const constant = (value) => {
2802
+ if ("literal" in value)
2803
+ return value.literal;
2804
+ const f = inst.fields.find((f) => `self.${f.name}` === value.field);
2805
+ return f && "value" in f ? f.value : undefined;
2806
+ };
2807
+ if (action.requires.some((r) => {
2808
+ if (r.kind !== "compare")
2809
+ return false;
2810
+ const left = constant(r.left), right = constant(r.right);
2811
+ if (left === undefined || right === undefined)
2812
+ return false;
2813
+ return r.operator === "=="
2814
+ ? left !== right
2815
+ : r.operator === "!="
2816
+ ? left === right
2817
+ : false;
2818
+ })) {
2819
+ delete inst.actions[key];
2820
+ delete inst.lifecycle.transitions[key];
2821
+ specialized = true;
2822
+ }
2823
+ }
2824
+ if (!specialized)
2825
+ continue;
2826
+ const reachable = new Set([inst.lifecycle.initial]);
2827
+ for (let n = 0; n < inst.lifecycle.states.length; n++)
2828
+ for (const edge of Object.values(inst.lifecycle.transitions))
2829
+ if (edge.from.some((state) => reachable.has(state)))
2830
+ if (edge.to !== "preserve")
2831
+ reachable.add(edge.to);
2832
+ for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
2833
+ edge.from = edge.from.filter((state) => reachable.has(state));
2834
+ if (!edge.from.length) {
2835
+ delete inst.actions[key];
2836
+ delete inst.lifecycle.transitions[key];
2837
+ }
2838
+ }
2839
+ eliminatedStates.set(inst.id, new Set(inst.lifecycle.states.filter((state) => !reachable.has(state))));
2840
+ inst.lifecycle.states = inst.lifecycle.states.filter((state) => reachable.has(state));
2841
+ inst.actionOrder = inst.actionOrder.filter((key) => inst.actions[key]);
2842
+ }
2843
+ for (const inst of document.instruments)
2844
+ for (const action of Object.values(inst.actions)) {
2845
+ for (const requirement of action.requires) {
2846
+ if (requirement.kind !== "state")
2847
+ continue;
2848
+ const field = inst.fields.find((f) => `self.${f.name}` === requirement.reference);
2849
+ if (field?.type === "ref")
2850
+ requirement.states = requirement.states.filter((state) => !(typeof field.target === "string"
2851
+ ? [field.target]
2852
+ : field.target).some((id) => eliminatedStates.get(id)?.has(state)));
2853
+ }
2854
+ }
2855
+ for (const exposure of exposures)
2856
+ if (!document.instruments.find((inst) => inst.id === exposure.instrument)
2857
+ ?.actions[exposure.action])
2858
+ fail(exposure.entry, `action ${exposure.action} is excluded by these bindings`, "expose an action available with these tunables");
2859
+ const changed = new Set();
2860
+ for (const decl of program.decls)
2861
+ if (decl.kind === "hide" || decl.kind === "expose") {
2862
+ const parts = decl.target.split(".");
2863
+ const key = parts.pop();
2864
+ const id = parts.join("_");
2865
+ const action = document.instruments.find((i) => i.id === id)?.actions[key ?? ""];
2866
+ if (!action || changed.has(decl.target))
2867
+ fail(decl, `unknown or repeated action ${decl.target}`, "name one declared object.action once");
2868
+ changed.add(decl.target);
2869
+ if (decl.kind === "hide")
2870
+ delete action.publicAction;
2871
+ else if (automatic(action.actor))
2872
+ fail(decl, `${decl.target} runs on the clock or its parent, not a caller`, "expose only caller actions");
2873
+ else
2874
+ action.publicAction = decl.name;
2875
+ }
2876
+ for (const decl of program.decls) {
2877
+ if (decl.kind !== "expose")
2878
+ continue;
2879
+ const parts = decl.target.split(".");
2880
+ parts.pop();
2881
+ const instrument = document.instruments.find((item) => item.id === parts.join("_"));
2882
+ if (Object.values(instrument.actions).filter((action) => action.publicAction === decl.name).length > 1)
2883
+ fail(decl, `public action ${decl.name} is used more than once on ${parts.join(".")}`, "give each exposed action a distinct public name");
2884
+ }
2885
+ for (const kind of document.objects) {
2886
+ for (const name of kind.entryActions ?? []) {
2887
+ const eligible = kind.attachments.some((attachment) => {
2888
+ const create = document.instruments.find((instrument) => instrument.id === attachment.instrument)?.actions.create;
2889
+ return (create?.publicAction === name &&
2890
+ typeof create.actor === "object" &&
2891
+ "party" in create.actor &&
2892
+ create.actor.party === "actor");
2893
+ });
2894
+ if (!eligible)
2895
+ fail(objects.get(kind.id), `entry action ${name} must create an attachment as actor`, "expose its create action and bind its acting party to actor");
2896
+ }
2897
+ }
2898
+ const validated = validateUdl(document);
2899
+ if (!validated.ok)
2900
+ return {
2901
+ verdict: "invalid",
2902
+ diagnostics: validated.issues.map((i) => {
2903
+ const origin = [...origins]
2904
+ .reverse()
2905
+ .find((o) => i.path.startsWith(o.path));
2906
+ const index = /^\$\.instruments\[(\d+)\]/.exec(i.path)?.[1];
2907
+ const instrument = index === undefined
2908
+ ? undefined
2909
+ : document.instruments[Number(index)];
2910
+ const attachment = document.objects
2911
+ .flatMap((object) => object.attachments)
2912
+ .find((attachment) => attachment.instrument === instrument?.id);
2913
+ const stranded = i.stranded;
2914
+ if (instrument && stranded?.accounts.length) {
2915
+ const binding = refundBindings
2916
+ .get(instrument.id)
2917
+ ?.find((binding) => binding.defaultState === stranded.state &&
2918
+ instrument.actions[binding.action]?.moves.some((move) => "amount" in move &&
2919
+ stranded.accounts.some((account) => move.from === `self.${account}`)));
2920
+ return diagnostic({
2921
+ code: i.code,
2922
+ message: `\`${attachment?.name ?? instrument.id}\` can reach \`${stranded.state}\` with money in ${stranded.accounts.map((account) => `\`${account}\``).join(", ")}, but no action leaves that state and disposes of the balance.`,
2923
+ fix: binding
2924
+ ? `Restore \`${binding.parameter}: ${stranded.state}\`, or author a complete refund path for every reachable funded state.`
2925
+ : "Author a complete refund path for every reachable funded state.",
2926
+ span: binding?.span ?? origin?.span ?? program.span,
2927
+ related: stranded.accounts.map((account) => ({
2928
+ source: "program",
2929
+ span: origin?.span ?? program.span,
2930
+ message: `State path: ${instrument.lifecycle.initial} -> ${(stranded.paths[account] ?? stranded.actions).join(" -> ")} -> ${stranded.state}; owned accounts: ${account}.`,
2931
+ })),
2932
+ }, "lower");
2933
+ }
2934
+ return diagnostic({
2935
+ code: i.code,
2936
+ message: `${i.path}: ${i.message}`,
2937
+ fix: i.fix,
2938
+ span: origin?.span ?? program.span,
2939
+ }, "lower");
2940
+ }),
2941
+ };
2942
+ return {
2943
+ verdict: "valid",
2944
+ diagnostics: [],
2945
+ artifacts: {
2946
+ document: validated.value,
2947
+ costManifest: buildUdlCostManifest(validated.value),
2948
+ originMap: origins,
2949
+ },
2950
+ };
2951
+ }
2952
+ catch (error) {
2953
+ if (error instanceof CompileFailure)
2954
+ return {
2955
+ verdict: "invalid",
2956
+ diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map((d) => diagnostic(d, "check")),
2957
+ };
2958
+ throw error;
125
2959
  }
126
- offsets[source.length] = bytes;
127
- return offsets;
128
2960
  }
129
2961
  //# sourceMappingURL=compile.js.map