@hyperscale0/hsx 1.0.0-beta.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +58 -9
  2. package/LICENSING.md +3 -3
  3. package/README.md +39 -149
  4. package/TRADEMARKS.md +4 -4
  5. package/bin/hsx.ts +2 -0
  6. package/dist/bin/hsx.js +2 -0
  7. package/dist/bin/hsx.js.map +1 -1
  8. package/dist/src/ast.d.ts +135 -10
  9. package/dist/src/ast.d.ts.map +1 -1
  10. package/dist/src/ast.js +24 -0
  11. package/dist/src/ast.js.map +1 -1
  12. package/dist/src/cli.d.ts +4 -1
  13. package/dist/src/cli.d.ts.map +1 -1
  14. package/dist/src/cli.js +172 -16
  15. package/dist/src/cli.js.map +1 -1
  16. package/dist/src/compile.d.ts +42 -7
  17. package/dist/src/compile.d.ts.map +1 -1
  18. package/dist/src/compile.js +82 -16
  19. package/dist/src/compile.js.map +1 -1
  20. package/dist/src/cost.d.ts +78 -0
  21. package/dist/src/cost.d.ts.map +1 -0
  22. package/dist/src/cost.js +369 -0
  23. package/dist/src/cost.js.map +1 -0
  24. package/dist/src/diagnostics.d.ts +11 -0
  25. package/dist/src/diagnostics.d.ts.map +1 -0
  26. package/dist/src/diagnostics.js +420 -0
  27. package/dist/src/diagnostics.js.map +1 -0
  28. package/dist/src/emit.d.ts +34 -0
  29. package/dist/src/emit.d.ts.map +1 -0
  30. package/dist/src/emit.js +137 -0
  31. package/dist/src/emit.js.map +1 -0
  32. package/dist/src/entry-overrides.d.ts +17 -3
  33. package/dist/src/entry-overrides.d.ts.map +1 -1
  34. package/dist/src/entry-overrides.js +180 -35
  35. package/dist/src/entry-overrides.js.map +1 -1
  36. package/dist/src/format.d.ts +10 -0
  37. package/dist/src/format.d.ts.map +1 -0
  38. package/dist/src/format.js +183 -0
  39. package/dist/src/format.js.map +1 -0
  40. package/dist/src/index.d.ts +11 -4
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +10 -3
  43. package/dist/src/index.js.map +1 -1
  44. package/dist/src/ir.d.ts +66 -0
  45. package/dist/src/ir.d.ts.map +1 -0
  46. package/dist/src/ir.js +15 -0
  47. package/dist/src/ir.js.map +1 -0
  48. package/dist/src/lex.d.ts +9 -1
  49. package/dist/src/lex.d.ts.map +1 -1
  50. package/dist/src/lex.js +50 -4
  51. package/dist/src/lex.js.map +1 -1
  52. package/dist/src/limits.d.ts +2 -3
  53. package/dist/src/limits.d.ts.map +1 -1
  54. package/dist/src/limits.js +2 -3
  55. package/dist/src/limits.js.map +1 -1
  56. package/dist/src/lsp/server.d.ts +32 -0
  57. package/dist/src/lsp/server.d.ts.map +1 -0
  58. package/dist/src/lsp/server.js +391 -0
  59. package/dist/src/lsp/server.js.map +1 -0
  60. package/dist/src/modules.d.ts +38 -0
  61. package/dist/src/modules.d.ts.map +1 -0
  62. package/dist/src/modules.js +368 -0
  63. package/dist/src/modules.js.map +1 -0
  64. package/dist/src/parse.d.ts.map +1 -1
  65. package/dist/src/parse.js +590 -17
  66. package/dist/src/parse.js.map +1 -1
  67. package/dist/src/std-bundle.d.ts +2 -0
  68. package/dist/src/std-bundle.d.ts.map +1 -0
  69. package/dist/src/std-bundle.js +88 -0
  70. package/dist/src/std-bundle.js.map +1 -0
  71. package/dist/src/std-library.d.ts +5 -0
  72. package/dist/src/std-library.d.ts.map +1 -0
  73. package/dist/src/std-library.js +9 -0
  74. package/dist/src/std-library.js.map +1 -0
  75. package/dist/src/typecheck.d.ts +8 -0
  76. package/dist/src/typecheck.d.ts.map +1 -0
  77. package/dist/src/typecheck.js +2958 -0
  78. package/dist/src/typecheck.js.map +1 -0
  79. package/dist/src/version.d.ts +4 -15
  80. package/dist/src/version.d.ts.map +1 -1
  81. package/dist/src/version.js +4 -15
  82. package/dist/src/version.js.map +1 -1
  83. package/docs/README.md +26 -0
  84. package/docs/guide/01-first-program.md +24 -0
  85. package/docs/guide/02-money.md +25 -0
  86. package/docs/guide/03-instruments.md +22 -0
  87. package/docs/guide/04-lifecycles.md +25 -0
  88. package/docs/guide/05-fees-and-splits.md +45 -0
  89. package/docs/guide/06-schedules.md +22 -0
  90. package/docs/guide/07-composition.md +46 -0
  91. package/docs/guide/08-writing-a-module.md +26 -0
  92. package/docs/guide/09-cost.md +24 -0
  93. package/docs/guide/10-diagnostics.md +27 -0
  94. package/docs/llms-full.txt +1739 -0
  95. package/docs/llms.txt +45 -0
  96. package/docs/playground.md +54 -0
  97. package/docs/reference/cli.md +35 -0
  98. package/docs/reference/diagnostics.md +619 -0
  99. package/docs/reference/grammar.md +39 -0
  100. package/docs/reference/std/advance.md +35 -0
  101. package/docs/reference/std/cancellable_booking.md +35 -0
  102. package/docs/reference/std/captured_payment.md +43 -0
  103. package/docs/reference/std/conditional_disbursement.md +33 -0
  104. package/docs/reference/std/credit_facility.md +35 -0
  105. package/docs/reference/std/held_payment.md +64 -0
  106. package/docs/reference/std/instant_transfer.md +30 -0
  107. package/docs/reference/std/metered.md +27 -0
  108. package/docs/reference/std/pooled_split.md +26 -0
  109. package/docs/reference/std/premium_forward.md +39 -0
  110. package/docs/reference/std/reconciled_payout.md +33 -0
  111. package/docs/reference/std/recurring_collection.md +25 -0
  112. package/docs/reference/std/rotating_pool.md +42 -0
  113. package/docs/reference/std/scheduled.md +51 -0
  114. package/docs/reference/std/security_deposit.md +46 -0
  115. package/docs/reference/std/settlement_batch.md +42 -0
  116. package/docs/reference/std/swap.md +46 -0
  117. package/docs/reference/std/threshold_pool.md +47 -0
  118. package/docs/reference/std/weighted_distribution.md +39 -0
  119. package/docs/reference/types.md +20 -0
  120. package/docs/reference/udl-output.md +13 -0
  121. package/docs/sessions/2026-09-02-two-instruments.hsx +21 -0
  122. package/docs/sessions/2026-09-02-two-instruments.md +41 -0
  123. package/examples/01-first-program/README.md +45 -0
  124. package/examples/01-first-program/tip-jar.hsx +17 -0
  125. package/examples/02-imports-and-archetypes/README.md +96 -0
  126. package/examples/02-imports-and-archetypes/photo-booth.hsx +37 -0
  127. package/examples/03-diagnostics/README.md +68 -0
  128. package/examples/03-diagnostics/corner-shop-fixed.hsx +22 -0
  129. package/examples/03-diagnostics/corner-shop.hsx +19 -0
  130. package/examples/04-complete-product/README.md +84 -0
  131. package/examples/04-complete-product/study-hall.hsx +67 -0
  132. package/examples/05-watch-club/README.md +9 -0
  133. package/examples/05-watch-club/watch-club.hsx +124 -0
  134. package/examples/README.md +18 -0
  135. package/examples/advance/README.md +3 -0
  136. package/examples/advance/advance.hsx +13 -0
  137. package/examples/advance/advance.udl +339 -0
  138. package/examples/cancellable_booking/README.md +3 -0
  139. package/examples/cancellable_booking/cancellable_booking.hsx +14 -0
  140. package/examples/cancellable_booking/cancellable_booking.udl +391 -0
  141. package/examples/captured_payment/README.md +3 -0
  142. package/examples/captured_payment/captured_payment.hsx +21 -0
  143. package/examples/captured_payment/captured_payment.udl +677 -0
  144. package/examples/conditional_disbursement/README.md +3 -0
  145. package/examples/conditional_disbursement/conditional_disbursement.hsx +17 -0
  146. package/examples/conditional_disbursement/conditional_disbursement.udl +412 -0
  147. package/examples/cost-table.json +386 -0
  148. package/examples/credit_facility/README.md +3 -0
  149. package/examples/credit_facility/credit_facility.hsx +27 -0
  150. package/examples/credit_facility/credit_facility.udl +1373 -0
  151. package/examples/held_payment/README.md +3 -0
  152. package/examples/held_payment/held_payment.hsx +13 -0
  153. package/examples/held_payment/held_payment.udl +507 -0
  154. package/examples/instant_transfer/README.md +3 -0
  155. package/examples/instant_transfer/instant_transfer.hsx +16 -0
  156. package/examples/instant_transfer/instant_transfer.udl +364 -0
  157. package/examples/metered/README.md +3 -0
  158. package/examples/metered/metered.hsx +13 -0
  159. package/examples/metered/metered.udl +247 -0
  160. package/examples/pooled_split/README.md +3 -0
  161. package/examples/pooled_split/pooled_split.hsx +15 -0
  162. package/examples/pooled_split/pooled_split.udl +425 -0
  163. package/examples/premium_forward/README.md +3 -0
  164. package/examples/premium_forward/premium_forward.hsx +19 -0
  165. package/examples/premium_forward/premium_forward.udl +594 -0
  166. package/examples/reconciled_payout/README.md +3 -0
  167. package/examples/reconciled_payout/reconciled_payout.hsx +13 -0
  168. package/examples/reconciled_payout/reconciled_payout.udl +303 -0
  169. package/examples/recurring_collection/README.md +3 -0
  170. package/examples/recurring_collection/recurring_collection.hsx +21 -0
  171. package/examples/recurring_collection/recurring_collection.udl +1130 -0
  172. package/examples/rotating_pool/README.md +3 -0
  173. package/examples/rotating_pool/rotating_pool.hsx +18 -0
  174. package/examples/rotating_pool/rotating_pool.udl +4352 -0
  175. package/examples/scheduled/README.md +3 -0
  176. package/examples/scheduled/scheduled.hsx +12 -0
  177. package/examples/scheduled/scheduled.udl +335 -0
  178. package/examples/security_deposit/README.md +3 -0
  179. package/examples/security_deposit/security_deposit.hsx +21 -0
  180. package/examples/security_deposit/security_deposit.udl +293 -0
  181. package/examples/settlement_batch/README.md +3 -0
  182. package/examples/settlement_batch/settlement_batch.hsx +19 -0
  183. package/examples/settlement_batch/settlement_batch.udl +753 -0
  184. package/examples/swap/README.md +3 -0
  185. package/examples/swap/swap.hsx +19 -0
  186. package/examples/swap/swap.udl +876 -0
  187. package/examples/threshold_pool/README.md +3 -0
  188. package/examples/threshold_pool/threshold_pool.hsx +16 -0
  189. package/examples/threshold_pool/threshold_pool.udl +667 -0
  190. package/examples/weighted_distribution/README.md +3 -0
  191. package/examples/weighted_distribution/weighted_distribution.hsx +20 -0
  192. package/examples/weighted_distribution/weighted_distribution.udl +337 -0
  193. package/package.json +27 -6
  194. package/skills/hsx/SKILL.md +483 -0
  195. package/src/ast.ts +193 -9
  196. package/src/cli.ts +191 -20
  197. package/src/compile.ts +160 -23
  198. package/src/cost.ts +579 -0
  199. package/src/diagnostics.ts +442 -0
  200. package/src/emit.ts +201 -0
  201. package/src/entry-overrides.ts +263 -48
  202. package/src/format.ts +261 -0
  203. package/src/index.ts +34 -4
  204. package/src/ir.ts +99 -0
  205. package/src/lex.ts +59 -4
  206. package/src/limits.ts +2 -3
  207. package/src/lsp/server.ts +460 -0
  208. package/src/modules.ts +446 -0
  209. package/src/parse.ts +655 -21
  210. package/src/std-bundle.ts +91 -0
  211. package/src/std-library.ts +12 -0
  212. package/src/typecheck.ts +3876 -0
  213. package/src/version.ts +4 -16
  214. package/std/SEMANTICS.md +130 -0
  215. package/std/settlements/advance.hsx +177 -0
  216. package/std/settlements/cancellable_booking.hsx +87 -0
  217. package/std/settlements/captured_payment.hsx +144 -0
  218. package/std/settlements/conditional_disbursement.hsx +185 -0
  219. package/std/settlements/credit_facility.hsx +167 -0
  220. package/std/settlements/held_payment.hsx +578 -0
  221. package/std/settlements/index.hsx +3 -0
  222. package/std/settlements/instant_transfer.hsx +190 -0
  223. package/std/settlements/metered.hsx +67 -0
  224. package/std/settlements/pooled_split.hsx +68 -0
  225. package/std/settlements/premium_forward.hsx +257 -0
  226. package/std/settlements/reconciled_payout.hsx +77 -0
  227. package/std/settlements/recurring_collection.hsx +27 -0
  228. package/std/settlements/rotating_pool.hsx +252 -0
  229. package/std/settlements/scheduled.hsx +641 -0
  230. package/std/settlements/security_deposit.hsx +166 -0
  231. package/std/settlements/settlement_batch.hsx +122 -0
  232. package/std/settlements/swap.hsx +1114 -0
  233. package/std/settlements/threshold_pool.hsx +221 -0
  234. package/std/settlements/weighted_distribution.hsx +141 -0
  235. package/dist/src/archetypes.d.ts +0 -17
  236. package/dist/src/archetypes.d.ts.map +0 -1
  237. package/dist/src/archetypes.js +0 -327
  238. package/dist/src/archetypes.js.map +0 -1
  239. package/dist/src/check.d.ts +0 -12
  240. package/dist/src/check.d.ts.map +0 -1
  241. package/dist/src/check.js +0 -1910
  242. package/dist/src/check.js.map +0 -1
  243. package/dist/src/lower.d.ts +0 -146
  244. package/dist/src/lower.d.ts.map +0 -1
  245. package/dist/src/lower.js +0 -4570
  246. package/dist/src/lower.js.map +0 -1
  247. package/dist/src/model.d.ts +0 -467
  248. package/dist/src/model.d.ts.map +0 -1
  249. package/dist/src/model.js +0 -15
  250. package/dist/src/model.js.map +0 -1
  251. package/spec/hsx-ir.schema.json +0 -920
  252. package/src/archetypes.ts +0 -340
  253. package/src/check.ts +0 -2938
  254. package/src/lower.ts +0 -5416
  255. package/src/model.ts +0 -519
@@ -0,0 +1,2958 @@
1
+ import { deriveUdlActionEffects, udlClauseVocabulary, } from "@hyperscale0/udl";
2
+ import { requiredFields } from "./emit.js";
3
+ const SNAKE_CASE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
4
+ const CAMEL_CASE = /^[a-z][A-Za-z0-9]*$/;
5
+ const CURRENCY = /^[A-Z]{3}$/;
6
+ const MONEY_PATTERN = "^[1-9][0-9]{0,17}$";
7
+ const OPTIONAL_MONEY_PATTERN = "^(0|[1-9][0-9]{0,17})$";
8
+ const MAX_COMPREHENSION_EXPANSIONS = 256;
9
+ const NONE_SENTINEL = "__hsx_none__";
10
+ const actionClauseBySpelling = new Map(udlClauseVocabulary
11
+ .filter((definition) => definition.scope === "action")
12
+ .map((definition) => [definition.spelling, definition]));
13
+ const instrumentClauseBySpelling = new Map(udlClauseVocabulary
14
+ .filter((definition) => definition.scope === "instrument")
15
+ .map((definition) => [definition.spelling, definition]));
16
+ const minorUnits = {
17
+ BHD: 3,
18
+ JPY: 0,
19
+ KWD: 3,
20
+ SAR: 2,
21
+ USD: 2,
22
+ };
23
+ export function checkGeneralProgram(program, options = {}) {
24
+ const diagnostics = [];
25
+ const report = (code, span, message, fix, severity = "error") => {
26
+ diagnostics.push({ code, fix, message, severity, span });
27
+ };
28
+ const headers = program.decls.filter((decl) => decl.kind === "program");
29
+ const header = headers[0];
30
+ const module = program.decls.find((decl) => decl.kind === "module");
31
+ if (!header && !module) {
32
+ report("HSX1000", program.span, "this file needs one program header or one module declaration", 'add `program product_name "Product title"`');
33
+ }
34
+ for (const extra of headers.slice(1)) {
35
+ report("HSX1002", extra.span, "a file declares exactly one program", "delete the extra program declaration");
36
+ }
37
+ if (header && !SNAKE_CASE.test(header.name.name)) {
38
+ report("HSX1003", header.name.span, `program ${header.name.name} is not snake_case`, "rename the program with lowercase words joined by underscores");
39
+ }
40
+ const parties = new Set(program.decls
41
+ .filter((decl) => decl.kind === "party")
42
+ .map((decl) => decl.name.name));
43
+ const ports = new Map(program.decls
44
+ .filter((decl) => decl.kind === "port")
45
+ .map((decl) => [decl.name.name, decl]));
46
+ const assetSubjects = program.decls
47
+ .filter((decl) => decl.kind === "asset")
48
+ .map((decl) => ({
49
+ declaredValue: "optional",
50
+ kind: decl.name.name,
51
+ origin: decl.span,
52
+ schema: { additionalProperties: false, properties: {}, type: "object" },
53
+ title: sentenceCase(decl.name.name),
54
+ version: 1,
55
+ }));
56
+ const subjects = [...assetSubjects];
57
+ const subjectKinds = new Set(assetSubjects.map((subject) => subject.kind));
58
+ for (const decl of program.decls.filter((candidate) => candidate.kind === "subject")) {
59
+ if (subjectKinds.has(decl.name.name)) {
60
+ report("HSX1008", decl.name.span, `subject ${decl.name.name} is declared twice`, "keep one subject declaration or rename one kind");
61
+ continue;
62
+ }
63
+ const title = stringSlot(decl.body, "title");
64
+ const declaredValue = stringSlot(decl.body, "declaredValue", "declared_value");
65
+ const versionRow = entry(decl.body, "version");
66
+ const version = versionRow?.value.kind === "number" && !versionRow.value.raw.includes(".")
67
+ ? Number(versionRow.value.raw)
68
+ : Number.NaN;
69
+ const schemaRow = entry(decl.body, "schema");
70
+ const schema = schemaRow?.value.kind === "block"
71
+ ? blockToObject(schemaRow.value, new Map(), diagnostics)
72
+ : undefined;
73
+ if (!title ||
74
+ !["none", "optional", "required"].includes(declaredValue ?? "") ||
75
+ !Number.isSafeInteger(version) ||
76
+ version < 1 ||
77
+ !schema) {
78
+ report("HSX1008", decl.span, `subject ${decl.name.name} needs title, positive integer version, declared_value, and schema`, "declare the complete UDL subject-kind document");
79
+ continue;
80
+ }
81
+ subjectKinds.add(decl.name.name);
82
+ subjects.push({
83
+ declaredValue: declaredValue,
84
+ kind: decl.name.name,
85
+ origin: decl.span,
86
+ schema,
87
+ title,
88
+ version,
89
+ });
90
+ }
91
+ const compositionDeclarations = program.decls.filter((declaration) => declaration.kind === "use" || declaration.kind === "expose");
92
+ const authoredInstrumentDeclarations = program.decls.filter((declaration) => declaration.kind === "instrument" ||
93
+ declaration.kind === "instrument_apply");
94
+ if (options.publishedCatalog &&
95
+ (compositionDeclarations.length > 0 ||
96
+ authoredInstrumentDeclarations.length === 0)) {
97
+ return checkPublishedProgram(program, options.publishedCatalog, header, subjects, diagnostics, report);
98
+ }
99
+ for (const declaration of compositionDeclarations) {
100
+ report("HSX1018", declaration.span, "this compiler host did not supply a published instrument catalog", "pass the canonical UDL catalog as the publishedCatalog compile option");
101
+ }
102
+ const aliases = new Map(program.decls
103
+ .filter((decl) => decl.kind === "type")
104
+ .map((decl) => [decl.name.name, decl.value]));
105
+ checkConstants(program.decls, aliases, diagnostics);
106
+ const templates = new Map();
107
+ for (const decl of program.decls) {
108
+ if (decl.kind !== "instrument")
109
+ continue;
110
+ const prior = templates.get(decl.name.name);
111
+ if (prior) {
112
+ report("HSX1004", decl.name.span, `instrument ${decl.name.name} is declared twice`, "keep one declaration or rename one instrument");
113
+ }
114
+ else
115
+ templates.set(decl.name.name, decl);
116
+ }
117
+ const concrete = [];
118
+ const scopedDeclarations = (declarationScope) => {
119
+ const scopedAliases = new Map(aliases);
120
+ const scopedConstants = resolveConstants([
121
+ ...program.decls,
122
+ ...declarationScope,
123
+ ]);
124
+ const scopedParties = new Set(parties);
125
+ const scopedPorts = new Map(ports);
126
+ const scopedTemplates = new Map(templates);
127
+ for (const local of declarationScope) {
128
+ if (local.kind === "type")
129
+ scopedAliases.set(local.name.name, local.value);
130
+ if (local.kind === "party")
131
+ scopedParties.add(local.name.name);
132
+ if (local.kind === "port")
133
+ scopedPorts.set(local.name.name, local);
134
+ if (local.kind === "instrument") {
135
+ scopedTemplates.set(local.name.name, local);
136
+ }
137
+ }
138
+ checkConstants(declarationScope, scopedAliases, diagnostics);
139
+ return {
140
+ aliases: scopedAliases,
141
+ constants: scopedConstants,
142
+ parties: scopedParties,
143
+ ports: scopedPorts,
144
+ templates: scopedTemplates,
145
+ };
146
+ };
147
+ for (const decl of program.decls) {
148
+ if (decl.kind === "instrument" &&
149
+ !decl.hasParameterList &&
150
+ decl.typeParameters.length === 0) {
151
+ const scoped = scopedDeclarations(decl.declarationScope ?? []);
152
+ concrete.push({
153
+ aliases: scoped.aliases,
154
+ body: substituteExpr(decl.body, scoped.constants),
155
+ generatedPrefix: false,
156
+ name: decl.name,
157
+ parties: scoped.parties,
158
+ });
159
+ }
160
+ if (decl.kind !== "instrument_apply")
161
+ continue;
162
+ const application = decl.application;
163
+ const callee = application.callee.parts.at(-1)?.name ?? "";
164
+ const globalTemplate = templates.get(callee);
165
+ const declarationScope = [
166
+ ...new Set([
167
+ ...(globalTemplate?.declarationScope ?? []),
168
+ ...(decl.declarationScope ?? []),
169
+ ]),
170
+ ];
171
+ const scoped = scopedDeclarations(declarationScope);
172
+ const template = scoped.templates.get(callee);
173
+ if (!template) {
174
+ report("HSX1001", application.callee.span, `instrument function ${callee} is not bound by this module graph`, `import ${callee} from its module or declare it before instantiation`);
175
+ continue;
176
+ }
177
+ const body = instantiate(template, application, diagnostics, decl.name, scoped.ports, scoped.parties, scoped.aliases, scoped.constants);
178
+ if (body) {
179
+ const merged = decl.metadata
180
+ ? mergeApplicationMetadata(body, decl.metadata, diagnostics)
181
+ : body;
182
+ concrete.push(...constructedInstruments(decl.name, merged, diagnostics).map((candidate) => ({
183
+ ...candidate,
184
+ aliases: scoped.aliases,
185
+ parties: scoped.parties,
186
+ })));
187
+ }
188
+ }
189
+ const allocated = allocateGeneratedPrefixes(concrete);
190
+ const instruments = [];
191
+ const ids = new Set();
192
+ for (const candidate of allocated) {
193
+ if (ids.has(candidate.name.name)) {
194
+ report("HSX1004", candidate.name.span, `instrument ${candidate.name.name} is emitted twice`, "rename one instrument");
195
+ continue;
196
+ }
197
+ ids.add(candidate.name.name);
198
+ const expandedBody = expandComprehensions(candidate.body, diagnostics);
199
+ const checked = checkInstrument(candidate.name, expandedBody, candidate.parties, candidate.aliases, diagnostics);
200
+ if (checked)
201
+ instruments.push(checked);
202
+ }
203
+ crossInstrumentReferenceDiagnostics(instruments, diagnostics);
204
+ if (header &&
205
+ instruments.length === 0 &&
206
+ !diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
207
+ report("HSX1502", header.span, `program ${header.name.name} emits no instruments`, "declare or instantiate at least one instrument");
208
+ }
209
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
210
+ return { diagnostics };
211
+ }
212
+ const name = header?.name.name ?? module?.name.parts.at(-1)?.name ?? "module";
213
+ const typed = {
214
+ instruments,
215
+ kind: "typed_program",
216
+ name,
217
+ origin: program.span,
218
+ subjects,
219
+ title: header?.title?.value ?? sentenceCase(name),
220
+ };
221
+ return { diagnostics, program: typed };
222
+ }
223
+ function checkPublishedProgram(program, catalog, header, authoredSubjects, diagnostics, report) {
224
+ if (!header) {
225
+ return { diagnostics };
226
+ }
227
+ if (!header.title || header.title.value.length === 0) {
228
+ report("HSX1015", header.span, `composed program ${header.name.name} needs a product title`, 'write the title after the product id, like `program product_name "Product title"`');
229
+ }
230
+ if (header.name.name.length > 60) {
231
+ report("HSX1015", header.name.span, "a composed program product id is at most 60 characters", "shorten the product id without changing its snake_case spelling");
232
+ }
233
+ if ((header.title?.value.length ?? 0) > 80) {
234
+ report("HSX1015", header.title?.span ?? header.span, "a composed program title is at most 80 characters", "shorten the product title to 80 characters or fewer");
235
+ }
236
+ const authoredInstruments = program.decls.filter((decl) => decl.kind === "instrument" || decl.kind === "instrument_apply");
237
+ for (const declaration of authoredInstruments) {
238
+ report("HSX1022", declaration.span, "a composed program cannot author an instrument", "publish the instrument in the catalog, then select it with use");
239
+ }
240
+ const uses = program.decls.filter((decl) => decl.kind === "use");
241
+ if (uses.length === 0) {
242
+ report("HSX1016", header.span, `composed program ${header.name.name} uses no published instruments`, "add at least one `use published_instrument` declaration");
243
+ }
244
+ const catalogById = new Map(catalog.instruments.map((instrument) => [instrument.id, instrument]));
245
+ const useById = new Map();
246
+ for (const use of uses) {
247
+ const id = use.instrument.name;
248
+ if (!SNAKE_CASE.test(id)) {
249
+ report("HSX1018", use.instrument.span, `published instrument id ${id} is not snake_case`, "write the published catalog id exactly");
250
+ continue;
251
+ }
252
+ if (useById.has(id)) {
253
+ report("HSX1017", use.instrument.span, `published instrument ${id} is used twice`, "delete the duplicate use declaration");
254
+ continue;
255
+ }
256
+ useById.set(id, use);
257
+ if (!catalogById.has(id)) {
258
+ report("HSX1018", use.instrument.span, `published instrument ${id} does not exist in this catalog`, "choose an instrument id from the supplied published catalog");
259
+ }
260
+ }
261
+ const selected = publishedInstrumentClosure(uses, catalogById);
262
+ const selectedById = new Map(selected.map((entry) => [entry.instrument.id, entry]));
263
+ const exposures = program.decls.filter((decl) => decl.kind === "expose");
264
+ const publicNames = new Map();
265
+ const exposedTargets = new Map();
266
+ for (const exposure of exposures) {
267
+ const target = `${exposure.instrument.name}.${exposure.action.name}`;
268
+ const explicitlyUsed = useById.has(exposure.instrument.name);
269
+ const instrument = selectedById.get(exposure.instrument.name)?.instrument;
270
+ const action = instrument?.actions[exposure.action.name];
271
+ if (!explicitlyUsed || !instrument || !action) {
272
+ report("HSX1021", exposure.span, `${target} is not an action on an explicitly used published instrument`, "add the matching use declaration and choose one of that instrument's actions");
273
+ continue;
274
+ }
275
+ if (!CAMEL_CASE.test(exposure.publicName.name)) {
276
+ report("HSX1020", exposure.publicName.span, `public action name ${exposure.publicName.name} is not camelCase`, "write a lowercase camelCase public action name");
277
+ continue;
278
+ }
279
+ const priorName = publicNames.get(exposure.publicName.name);
280
+ const priorTarget = exposedTargets.get(target);
281
+ if (priorName || priorTarget) {
282
+ report("HSX1020", exposure.span, priorTarget
283
+ ? `${target} is exposed more than once`
284
+ : `public action name ${exposure.publicName.name} is declared more than once`, "keep one public name for one instrument action");
285
+ continue;
286
+ }
287
+ publicNames.set(exposure.publicName.name, exposure);
288
+ exposedTargets.set(target, exposure);
289
+ }
290
+ const referencedSubjectKinds = new Set(selected.flatMap(({ instrument }) => instrument.subject?.kinds ?? []));
291
+ const catalogSubjects = catalog.subjects
292
+ .filter((subject) => referencedSubjectKinds.has(subject.kind))
293
+ .map((subject) => typedPublishedSubject(subject, uses[0]?.span ?? header.span));
294
+ const catalogSubjectKinds = new Set(catalogSubjects.map((subject) => subject.kind));
295
+ for (const subject of authoredSubjects) {
296
+ if (!catalogSubjectKinds.has(subject.kind))
297
+ continue;
298
+ report("HSX1008", subject.origin, `subject ${subject.kind} is already supplied by the published catalog`, "delete the duplicate subject declaration or use a distinct subject kind");
299
+ }
300
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
301
+ return { diagnostics };
302
+ }
303
+ const typed = {
304
+ instruments: selected.map(({ instrument, origin }) => {
305
+ const projected = projectedPublishedInstrument(instrument, exposures);
306
+ return typedPublishedInstrument(projected, origin, exposures.filter((exposure) => exposure.instrument.name === instrument.id));
307
+ }),
308
+ kind: "typed_program",
309
+ name: header.name.name,
310
+ origin: program.span,
311
+ subjects: [
312
+ ...catalogSubjects,
313
+ ...authoredSubjects.filter((subject) => !catalogSubjectKinds.has(subject.kind)),
314
+ ],
315
+ title: header.title?.value ?? sentenceCase(header.name.name),
316
+ };
317
+ return { diagnostics, program: typed };
318
+ }
319
+ function projectedPublishedInstrument(source, exposures) {
320
+ const publicNameByAction = new Map(exposures
321
+ .filter((exposure) => exposure.instrument.name === source.id)
322
+ .map((exposure) => [exposure.action.name, exposure.publicName.name]));
323
+ return {
324
+ ...structuredClone(source),
325
+ actions: Object.fromEntries(source.actionOrder.map((name) => {
326
+ const { publicAction: _publicAction, ...privateAction } = source.actions[name];
327
+ const publicName = publicNameByAction.get(name);
328
+ return [
329
+ name,
330
+ publicName
331
+ ? { ...privateAction, publicAction: publicName }
332
+ : privateAction,
333
+ ];
334
+ })),
335
+ };
336
+ }
337
+ function publishedInstrumentClosure(uses, catalogById) {
338
+ const selected = [];
339
+ const selectedIds = new Set();
340
+ const queue = uses.map((use) => ({
341
+ id: use.instrument.name,
342
+ origin: use.span,
343
+ }));
344
+ const idByPrefix = new Map([...catalogById.values()].map((instrument) => [
345
+ instrument.idPrefix,
346
+ instrument.id,
347
+ ]));
348
+ while (queue.length > 0) {
349
+ const next = queue.shift();
350
+ if (!next || selectedIds.has(next.id))
351
+ continue;
352
+ const instrument = catalogById.get(next.id);
353
+ if (!instrument)
354
+ continue;
355
+ selectedIds.add(next.id);
356
+ selected.push({ instrument, origin: next.origin });
357
+ for (const dependency of publishedInstrumentDependencies(instrument, idByPrefix)) {
358
+ if (!selectedIds.has(dependency))
359
+ queue.push({ id: dependency, origin: next.origin });
360
+ }
361
+ }
362
+ return selected;
363
+ }
364
+ function publishedInstrumentDependencies(instrument, idByPrefix) {
365
+ const found = new Set();
366
+ const visit = (value) => {
367
+ if (Array.isArray(value)) {
368
+ value.forEach(visit);
369
+ return;
370
+ }
371
+ if (!value || typeof value !== "object")
372
+ return;
373
+ for (const [key, child] of Object.entries(value)) {
374
+ if ((key === "instrumentId" || key === "childInstrumentId") &&
375
+ typeof child === "string") {
376
+ found.add(child);
377
+ }
378
+ visit(child);
379
+ }
380
+ };
381
+ visit(instrument);
382
+ for (const field of Object.values(instrument.fields)) {
383
+ const prefix = typeof field.pattern === "string"
384
+ ? /^\^([a-z]{2,8})_\(sandbox\|live\)_/.exec(field.pattern)?.[1]
385
+ : undefined;
386
+ const dependency = prefix ? idByPrefix.get(prefix) : undefined;
387
+ if (dependency)
388
+ found.add(dependency);
389
+ }
390
+ found.delete(instrument.id);
391
+ return [...found];
392
+ }
393
+ function typedPublishedInstrument(source, origin, exposures) {
394
+ const exposureByAction = new Map(exposures.map((exposure) => [exposure.action.name, exposure]));
395
+ const { actionOrder, actions, fields, id: _id, ...slots } = structuredClone(source);
396
+ const requiredFields = new Set(source.required);
397
+ return {
398
+ actions: actionOrder.map((name) => {
399
+ const exposure = exposureByAction.get(name);
400
+ const { publicAction: _publicAction, ...privateAction } = actions[name];
401
+ return {
402
+ effects: (privateAction.effects ?? {}),
403
+ name,
404
+ origin: exposure?.span ?? origin,
405
+ slots: (exposure
406
+ ? { ...privateAction, publicAction: exposure.publicName.name }
407
+ : privateAction),
408
+ };
409
+ }),
410
+ fields: Object.entries(fields).map(([name, schema]) => ({
411
+ name,
412
+ origin,
413
+ required: requiredFields.has(name),
414
+ schema,
415
+ type: { kind: "unknown" },
416
+ })),
417
+ id: source.id,
418
+ origin,
419
+ slots: {
420
+ ...slots,
421
+ required: [...source.required],
422
+ },
423
+ };
424
+ }
425
+ function typedPublishedSubject(subject, origin) {
426
+ return {
427
+ declaredValue: subject.declaredValue,
428
+ kind: subject.kind,
429
+ origin,
430
+ schema: structuredClone(subject.schema),
431
+ title: subject.title,
432
+ version: subject.version,
433
+ };
434
+ }
435
+ // Presentation only. An application may restate how a settlement reads to a
436
+ // human or an agent; it may never restate what the settlement does. Both sets
437
+ // carry agent_description for the same reason they carry description: it is
438
+ // prose about the shape, and the catalog's voice is more concrete than the
439
+ // parameterized template's interpolated one.
440
+ const applicationMetadataKeys = new Set([
441
+ "action",
442
+ "agent_description",
443
+ "description",
444
+ "nav",
445
+ "navigation",
446
+ "summary",
447
+ "surface_visibility",
448
+ "template_id",
449
+ "title",
450
+ "visibility",
451
+ ]);
452
+ const applicationActionMetadataKeys = new Set([
453
+ "agent_description",
454
+ "description",
455
+ "public",
456
+ "public_action",
457
+ "summary",
458
+ ]);
459
+ function applicationInstrumentMetadataKey(key) {
460
+ if (key === "navigation")
461
+ return "nav";
462
+ if (key === "visibility")
463
+ return "surface_visibility";
464
+ return key;
465
+ }
466
+ function applicationActionMetadataKey(key) {
467
+ return key === "public_action" ? "public" : key;
468
+ }
469
+ function mergeApplicationMetadata(body, metadata, diagnostics) {
470
+ let entries = [...body.entries];
471
+ for (const row of metadata.entries) {
472
+ if (!applicationMetadataKeys.has(row.key.name)) {
473
+ diagnostics.push({
474
+ code: "HSX1507",
475
+ fix: "move contract mechanics into the parameterized instrument",
476
+ message: `application metadata cannot replace ${row.key.name}`,
477
+ severity: "error",
478
+ span: row.key.span,
479
+ });
480
+ continue;
481
+ }
482
+ if (row.key.name !== "action") {
483
+ const overlayKey = applicationInstrumentMetadataKey(row.key.name);
484
+ const normalizedRow = overlayKey === row.key.name
485
+ ? row
486
+ : { ...row, key: { ...row.key, name: overlayKey } };
487
+ entries = [
488
+ ...entries.filter((candidate) => applicationInstrumentMetadataKey(candidate.key.name) !== overlayKey),
489
+ normalizedRow,
490
+ ];
491
+ continue;
492
+ }
493
+ const actionName = row.qualifiers[0]?.name;
494
+ const targetIndex = entries.findIndex((candidate) => candidate.key.name === "action" &&
495
+ candidate.qualifiers[0]?.name === actionName);
496
+ const target = entries[targetIndex];
497
+ if (!actionName ||
498
+ row.value.kind !== "block" ||
499
+ !target ||
500
+ target.value.kind !== "block") {
501
+ diagnostics.push({
502
+ code: "HSX1507",
503
+ fix: "name an action declared by the parameterized instrument",
504
+ message: `application metadata action ${actionName ?? "<missing>"} does not exist`,
505
+ severity: "error",
506
+ span: row.span,
507
+ });
508
+ continue;
509
+ }
510
+ const invalid = row.value.entries.find((candidate) => !applicationActionMetadataKeys.has(candidate.key.name));
511
+ if (invalid) {
512
+ diagnostics.push({
513
+ code: "HSX1507",
514
+ fix: "keep only summary, description, agent_description, public, or public_action here",
515
+ message: `application metadata cannot replace action clause ${invalid.key.name}`,
516
+ severity: "error",
517
+ span: invalid.span,
518
+ });
519
+ continue;
520
+ }
521
+ const overlayKeys = new Set(row.value.entries.map((entry) => applicationActionMetadataKey(entry.key.name)));
522
+ entries[targetIndex] = {
523
+ ...target,
524
+ value: {
525
+ ...target.value,
526
+ entries: [
527
+ ...target.value.entries.filter((candidate) => !overlayKeys.has(applicationActionMetadataKey(candidate.key.name))),
528
+ ...row.value.entries,
529
+ ],
530
+ },
531
+ };
532
+ }
533
+ return { ...body, entries };
534
+ }
535
+ function constructedInstruments(rootName, body, diagnostics) {
536
+ const expanded = expandComprehensions(body, diagnostics);
537
+ const construction = entry(expanded, "instruments");
538
+ const root = {
539
+ body: {
540
+ ...expanded,
541
+ entries: expanded.entries.filter((row) => row !== construction),
542
+ },
543
+ generatedPrefix: false,
544
+ name: rootName,
545
+ };
546
+ if (construction?.value.kind !== "block")
547
+ return [root];
548
+ const companions = construction.value.entries.flatMap((row) => {
549
+ if (row.key.name !== "instrument" || row.value.kind !== "block") {
550
+ diagnostics.push({
551
+ code: "HSX1406",
552
+ fix: 'write `instrument { id: concat(instrument, "_suffix"); ... }` inside the instruments block',
553
+ message: "an instruments block contains only fixed instrument constructors",
554
+ severity: "error",
555
+ span: row.span,
556
+ });
557
+ return [];
558
+ }
559
+ const idRow = entry(row.value, "id");
560
+ const id = idRow ? nameText(idRow.value) : "";
561
+ if (!id || id === "invalid_compile_time_name") {
562
+ diagnostics.push({
563
+ code: "HSX1406",
564
+ fix: "give the companion instrument a compile-time id",
565
+ message: "a companion instrument id must resolve at compile time",
566
+ severity: "error",
567
+ span: idRow?.span ?? row.span,
568
+ });
569
+ return [];
570
+ }
571
+ const generated = entry(row.value, "generatedPrefix");
572
+ return [
573
+ {
574
+ body: {
575
+ ...row.value,
576
+ entries: row.value.entries.filter((child) => child !== idRow && child !== generated),
577
+ },
578
+ generatedPrefix: generated?.value.kind === "boolean" && generated.value.value,
579
+ name: {
580
+ kind: "ident",
581
+ name: id,
582
+ span: idRow?.span ?? row.span,
583
+ },
584
+ },
585
+ ];
586
+ });
587
+ return [root, ...companions];
588
+ }
589
+ function allocateGeneratedPrefixes(candidates) {
590
+ const used = new Set(candidates
591
+ .filter((candidate) => !candidate.generatedPrefix)
592
+ .map((candidate) => stringSlot(candidate.body, "idPrefix", "id_prefix") ??
593
+ prefixFor(candidate.name.name)));
594
+ let ordinal = 0;
595
+ const next = () => {
596
+ while (true) {
597
+ const high = String.fromCharCode(97 + Math.floor(ordinal / 26));
598
+ const low = String.fromCharCode(97 + (ordinal % 26));
599
+ ordinal += 1;
600
+ const candidate = `zz${high}${low}`;
601
+ if (!used.has(candidate)) {
602
+ used.add(candidate);
603
+ return candidate;
604
+ }
605
+ }
606
+ };
607
+ return candidates.map((candidate) => {
608
+ if (!candidate.generatedPrefix)
609
+ return candidate;
610
+ const value = {
611
+ kind: "string",
612
+ span: candidate.name.span,
613
+ value: next(),
614
+ };
615
+ return {
616
+ ...candidate,
617
+ body: {
618
+ ...candidate.body,
619
+ entries: [
620
+ ...candidate.body.entries,
621
+ {
622
+ key: {
623
+ kind: "ident",
624
+ name: "id_prefix",
625
+ span: candidate.name.span,
626
+ },
627
+ qualifiers: [],
628
+ span: candidate.name.span,
629
+ value,
630
+ },
631
+ ],
632
+ },
633
+ };
634
+ });
635
+ }
636
+ function inferSettlementTypeArgument(template, rows, typeParameter) {
637
+ for (const parameter of template.parameters) {
638
+ const parameterType = parameter.type;
639
+ if (parameterType.kind !== "type_apply" ||
640
+ parameterType.callee.name !== "money" ||
641
+ parameterType.args[0]?.kind !== "ident" ||
642
+ parameterType.args[0].name !== typeParameter) {
643
+ continue;
644
+ }
645
+ const value = rows.get(parameter.name.name);
646
+ const valueType = value?.kind === "binding" ? value.type : value;
647
+ if ((valueType?.kind === "call" || valueType?.kind === "type_apply") &&
648
+ valueType.callee.name === "money") {
649
+ return valueType.args[0];
650
+ }
651
+ if (valueType?.kind === "money")
652
+ return valueType.currency;
653
+ }
654
+ return undefined;
655
+ }
656
+ function checkConstants(decls, aliases, diagnostics) {
657
+ for (const decl of decls) {
658
+ if (decl.kind !== "const" || !decl.type)
659
+ continue;
660
+ const expected = typeOf(decl.type, aliases);
661
+ const actual = typeOf(decl.value, aliases);
662
+ if (expected.kind === "money" &&
663
+ actual.kind === "money" &&
664
+ expected.currency !== actual.currency) {
665
+ diagnostics.push({
666
+ code: "HSX1101",
667
+ fix: `write a ${expected.currency} literal or change ${decl.name.name}'s declared type`,
668
+ message: `${decl.name.name} is money<${expected.currency}> but receives money<${actual.currency}>; currencies never coerce`,
669
+ severity: "error",
670
+ span: decl.value.span,
671
+ });
672
+ }
673
+ if (decl.value.kind === "money")
674
+ validateMoneyLiteral(decl, diagnostics);
675
+ }
676
+ }
677
+ function resolveConstants(decls) {
678
+ const constants = new Map(decls
679
+ .filter((decl) => decl.kind === "const")
680
+ .map((decl) => [decl.name.name, decl.value]));
681
+ for (let pass = 0; pass < constants.size; pass += 1) {
682
+ for (const [name, value] of constants) {
683
+ constants.set(name, substituteExpr(value, constants));
684
+ }
685
+ }
686
+ return constants;
687
+ }
688
+ function validateMoneyLiteral(decl, diagnostics) {
689
+ if (decl.value.kind !== "money")
690
+ return;
691
+ const precision = minorUnits[decl.value.currency.name] ?? 2;
692
+ const fraction = decl.value.raw.split(".")[1] ?? "";
693
+ if (fraction.length <= precision)
694
+ return;
695
+ diagnostics.push({
696
+ code: "HSX1102",
697
+ fix: `round the literal to ${precision} minor-unit digit${precision === 1 ? "" : "s"}`,
698
+ message: `${decl.value.currency.name} ${decl.value.raw} has ${fraction.length} decimal places; ${decl.value.currency.name} settles to ${precision}`,
699
+ severity: "error",
700
+ span: decl.value.span,
701
+ });
702
+ }
703
+ function instantiate(template, application, diagnostics, instrumentName, ports, parties, aliases, constants) {
704
+ const partyKeyedParameters = partyKeyedBlockParameters(template, aliases);
705
+ const named = new Map();
706
+ const positional = [];
707
+ const parameterNames = new Set(template.parameters.map((parameter) => parameter.name.name));
708
+ for (const arg of application.args) {
709
+ if (arg.kind !== "binding") {
710
+ positional.push(arg);
711
+ continue;
712
+ }
713
+ if (!parameterNames.has(arg.name.name)) {
714
+ diagnostics.push({
715
+ code: "HSX1012",
716
+ fix: `remove ${arg.name.name} or name one of ${[...parameterNames].join(", ")}`,
717
+ message: `${template.name.name} has no argument named ${arg.name.name}`,
718
+ severity: "error",
719
+ span: arg.name.span,
720
+ });
721
+ continue;
722
+ }
723
+ if (named.has(arg.name.name)) {
724
+ diagnostics.push({
725
+ code: "HSX1013",
726
+ fix: `pass ${arg.name.name} once`,
727
+ message: `${template.name.name} receives ${arg.name.name} more than once`,
728
+ severity: "error",
729
+ span: arg.name.span,
730
+ });
731
+ continue;
732
+ }
733
+ named.set(arg.name.name, arg.type);
734
+ }
735
+ const namedParameterCount = template.parameters.filter((parameter) => named.has(parameter.name.name)).length;
736
+ const maximumPositional = template.parameters.length - namedParameterCount;
737
+ if (positional.length > maximumPositional) {
738
+ diagnostics.push({
739
+ code: "HSX1012",
740
+ fix: `pass at most ${template.parameters.length} arguments`,
741
+ message: `${template.name.name} receives ${application.args.length} arguments but declares ${template.parameters.length}`,
742
+ severity: "error",
743
+ span: application.span,
744
+ });
745
+ }
746
+ const typeArgs = application.typeArgs.length > 0
747
+ ? application.typeArgs
748
+ : template.typeParameters.map((parameter) => inferSettlementTypeArgument(template, named, parameter.name.name) ??
749
+ parameter.name);
750
+ if (template.typeParameters.length !== typeArgs.length) {
751
+ diagnostics.push({
752
+ code: "HSX1010",
753
+ fix: `pass ${template.typeParameters.length} type argument${template.typeParameters.length === 1 ? "" : "s"}`,
754
+ message: `${template.name.name} expects ${template.typeParameters.length} type arguments but receives ${application.typeArgs.length}`,
755
+ severity: "error",
756
+ span: application.span,
757
+ });
758
+ return undefined;
759
+ }
760
+ const values = new Map(constants);
761
+ const missingOptional = new Map();
762
+ values.set("instrument", instrumentName);
763
+ const fieldBindings = new Map();
764
+ template.typeParameters.forEach((parameter, index) => {
765
+ const value = typeArgs[index];
766
+ if (value)
767
+ values.set(parameter.name.name, value);
768
+ });
769
+ for (const [name, value] of named)
770
+ values.set(name, value);
771
+ let position = 0;
772
+ for (const parameter of template.parameters) {
773
+ const value = named.get(parameter.name.name) ?? positional[position++];
774
+ if (!value) {
775
+ if (optionalInner(parameter.type)) {
776
+ values.set(parameter.name.name, booleanExpr(false, parameter.span));
777
+ missingOptional.set(parameter.name.name, parameter);
778
+ continue;
779
+ }
780
+ diagnostics.push({
781
+ code: "HSX1011",
782
+ fix: `pass ${parameter.name.name}: <${typeWords(typeOf(parameter.type, aliases))}>`,
783
+ message: `${template.name.name} needs argument ${parameter.name.name}`,
784
+ severity: "error",
785
+ span: application.span,
786
+ });
787
+ continue;
788
+ }
789
+ const expectedExpr = substituteExpr(optionalInner(parameter.type) ?? parameter.type, values);
790
+ const expected = typeOf(expectedExpr, aliases);
791
+ const actual = typeOf(value, aliases);
792
+ const nonPartyKeys = partyKeyedParameters.get(parameter.name.name);
793
+ if (nonPartyKeys && value.kind === "block") {
794
+ const declaredParties = [...parties].sort();
795
+ for (const row of value.entries) {
796
+ if (parties.has(row.key.name) || nonPartyKeys.has(row.key.name))
797
+ continue;
798
+ diagnostics.push({
799
+ code: "HSX1001",
800
+ fix: declaredParties.length > 0
801
+ ? `replace ${row.key.name} with one of ${declaredParties.join(", ")}`
802
+ : `declare the party named by ${row.key.name}`,
803
+ message: `${parameter.name.name} uses party key ${row.key.name}, which is not declared`,
804
+ severity: "error",
805
+ span: row.key.span,
806
+ });
807
+ }
808
+ }
809
+ if (expected.kind === "money" &&
810
+ actual.kind === "money" &&
811
+ expected.currency &&
812
+ actual.currency &&
813
+ expected.currency !== actual.currency) {
814
+ diagnostics.push({
815
+ code: "HSX1101",
816
+ fix: `pass money<${expected.currency}> to ${parameter.name.name}`,
817
+ message: `${parameter.name.name} needs money<${expected.currency}> but receives money<${actual.currency}>`,
818
+ severity: "error",
819
+ span: value.span,
820
+ });
821
+ }
822
+ if (!argumentMatches(expectedExpr, value, parties, ports, aliases)) {
823
+ diagnostics.push({
824
+ code: "HSX1104",
825
+ fix: `pass ${typeWords(expected)} to ${parameter.name.name}`,
826
+ message: `${parameter.name.name} needs ${typeWords(expected)} but receives ${argumentWords(value, aliases)}`,
827
+ severity: "error",
828
+ span: value.span,
829
+ });
830
+ }
831
+ if (value.kind === "binding") {
832
+ fieldBindings.set(parameter.name.name, {
833
+ name: value.name,
834
+ type: value.type,
835
+ });
836
+ values.set(parameter.name.name, value.name);
837
+ }
838
+ else if (expected.kind !== "party" && value.kind === "ident") {
839
+ fieldBindings.set(parameter.name.name, {
840
+ name: value,
841
+ type: substituteExpr(optionalInner(parameter.type) ?? parameter.type, values),
842
+ });
843
+ values.set(parameter.name.name, value);
844
+ }
845
+ else {
846
+ values.set(parameter.name.name, value);
847
+ }
848
+ if (value.kind === "port_ref") {
849
+ bindPortCompileValues(values, parameter.name.name, value, ports, diagnostics);
850
+ }
851
+ }
852
+ const selected = selectCompileTimeRows(template.body, values, diagnostics);
853
+ const dependencyDiagnosticStart = diagnostics.length;
854
+ for (const [name, parameter] of missingOptional) {
855
+ if (!referencesIdentifier(selected, name, values))
856
+ continue;
857
+ const inner = optionalInner(parameter.type);
858
+ if (!inner)
859
+ continue;
860
+ diagnostics.push({
861
+ code: "HSX1011",
862
+ fix: `pass ${name}: <${typeWords(typeOf(inner, aliases))}>`,
863
+ message: `${template.name.name} needs argument ${name} for the selected options`,
864
+ severity: "error",
865
+ span: application.span,
866
+ });
867
+ }
868
+ if (diagnostics.length > dependencyDiagnosticStart)
869
+ return undefined;
870
+ const body = substituteExpr(selected, values);
871
+ return {
872
+ ...body,
873
+ entries: body.entries.map((row) => {
874
+ if (row.key.name !== "fields" || row.value.kind !== "block")
875
+ return row;
876
+ return {
877
+ ...row,
878
+ value: {
879
+ ...row.value,
880
+ entries: row.value.entries.map((field) => {
881
+ const binding = fieldBindings.get(field.key.name);
882
+ return binding
883
+ ? {
884
+ ...field,
885
+ key: binding.name,
886
+ value: field.value.kind === "block"
887
+ ? {
888
+ ...field.value,
889
+ entries: field.value.entries.map((row) => row.key.name === "type"
890
+ ? { ...row, value: binding.type }
891
+ : row),
892
+ }
893
+ : binding.type,
894
+ }
895
+ : field;
896
+ }),
897
+ },
898
+ };
899
+ }),
900
+ };
901
+ }
902
+ function referencesIdentifier(expr, name, values) {
903
+ if (expr.kind === "ident")
904
+ return expr.name === name;
905
+ if (expr.kind === "block") {
906
+ return expr.entries.some((row) => row.key.name !== "let" && referencesIdentifier(row.value, name, values));
907
+ }
908
+ if (expr.kind === "call") {
909
+ if (expr.callee.name === "if_eq") {
910
+ const [left, right, whenEqual, whenDifferent] = expr.args;
911
+ const actualLeft = left ? substituteCompileArgument(left, values) : left;
912
+ const actualRight = right
913
+ ? substituteCompileArgument(right, values)
914
+ : right;
915
+ const selectedBranch = actualLeft &&
916
+ actualRight &&
917
+ nameText(actualLeft) === nameText(actualRight)
918
+ ? whenEqual
919
+ : whenDifferent;
920
+ return selectedBranch
921
+ ? referencesIdentifier(selectedBranch, name, values)
922
+ : false;
923
+ }
924
+ return expr.args.some((argument) => referencesIdentifier(argument, name, values));
925
+ }
926
+ if (expr.kind === "type_apply") {
927
+ return expr.args.some((argument) => referencesIdentifier(argument, name, values));
928
+ }
929
+ if (expr.kind === "apply") {
930
+ return (expr.args.some((argument) => referencesIdentifier(argument, name, values)) ||
931
+ expr.typeArgs.some((argument) => referencesIdentifier(argument, name, values)));
932
+ }
933
+ if (expr.kind === "list") {
934
+ return expr.items.some((item) => referencesIdentifier(item, name, values));
935
+ }
936
+ if (expr.kind === "binding")
937
+ return referencesIdentifier(expr.type, name, values);
938
+ if (expr.kind === "decided_amount") {
939
+ return referencesIdentifier(expr.body, name, values);
940
+ }
941
+ return false;
942
+ }
943
+ function partyKeyedBlockParameters(template, aliases) {
944
+ const partyParameters = new Set(template.parameters
945
+ .filter((parameter) => typeOf(optionalInner(parameter.type) ?? parameter.type, aliases)
946
+ .kind === "party")
947
+ .map((parameter) => parameter.name.name));
948
+ const partyListParameters = new Set(template.parameters
949
+ .filter((parameter) => {
950
+ const type = resolveAlias(optionalInner(parameter.type) ?? parameter.type, aliases);
951
+ const item = type.kind === "type_apply" ? type.args[0] : undefined;
952
+ return (type.kind === "type_apply" &&
953
+ type.callee.name === "list" &&
954
+ item !== undefined &&
955
+ typeOf(item, aliases).kind === "party");
956
+ })
957
+ .map((parameter) => parameter.name.name));
958
+ const blockParameters = new Set(template.parameters
959
+ .filter((parameter) => {
960
+ const type = resolveAlias(optionalInner(parameter.type) ?? parameter.type, aliases);
961
+ return type.kind === "ident" && type.name === "block";
962
+ })
963
+ .map((parameter) => parameter.name.name));
964
+ const keyed = new Set();
965
+ const nonPartyKeys = new Map();
966
+ const partyBindings = new Set(partyParameters);
967
+ const visit = (expr) => {
968
+ if (expr.kind === "call") {
969
+ const [owner, key] = expr.args;
970
+ if (expr.callee.name === "get" &&
971
+ owner?.kind === "ident" &&
972
+ key?.kind === "ident" &&
973
+ blockParameters.has(owner.name) &&
974
+ partyBindings.has(key.name)) {
975
+ keyed.add(owner.name);
976
+ }
977
+ expr.args.forEach(visit);
978
+ return;
979
+ }
980
+ if (expr.kind === "type_apply") {
981
+ expr.args.forEach(visit);
982
+ return;
983
+ }
984
+ if (expr.kind === "apply") {
985
+ expr.args.forEach(visit);
986
+ expr.typeArgs.forEach(visit);
987
+ return;
988
+ }
989
+ if (expr.kind === "list") {
990
+ expr.items.forEach(visit);
991
+ return;
992
+ }
993
+ if (expr.kind === "block") {
994
+ for (const row of expr.entries) {
995
+ if (row.key.name === "let") {
996
+ const binding = row.qualifiers[0];
997
+ const value = row.value;
998
+ const aliasesParty = value.kind === "ident" && partyBindings.has(value.name);
999
+ const aliasesPartyList = value.kind === "ident" && partyListParameters.has(value.name);
1000
+ const derivesBlockKeys = value.kind === "call" &&
1001
+ value.callee.name === "keys_except" &&
1002
+ value.args[0]?.kind === "ident" &&
1003
+ blockParameters.has(value.args[0].name);
1004
+ const indexesPartyList = value.kind === "call" &&
1005
+ value.callee.name === "at" &&
1006
+ value.args[0]?.kind === "ident" &&
1007
+ partyListParameters.has(value.args[0].name);
1008
+ if (binding && (aliasesPartyList || derivesBlockKeys)) {
1009
+ partyListParameters.add(binding.name);
1010
+ }
1011
+ if (derivesBlockKeys) {
1012
+ const owner = value.args[0];
1013
+ if (owner?.kind === "ident") {
1014
+ const omitted = nonPartyKeys.get(owner.name) ?? new Set();
1015
+ for (const argument of value.args.slice(1)) {
1016
+ omitted.add(nameText(argument));
1017
+ }
1018
+ nonPartyKeys.set(owner.name, omitted);
1019
+ }
1020
+ }
1021
+ if (binding && (aliasesParty || indexesPartyList)) {
1022
+ partyBindings.add(binding.name);
1023
+ }
1024
+ }
1025
+ visit(row.value);
1026
+ }
1027
+ return;
1028
+ }
1029
+ if (expr.kind === "decided_amount") {
1030
+ visit(expr.body);
1031
+ return;
1032
+ }
1033
+ if (expr.kind === "binding")
1034
+ visit(expr.type);
1035
+ };
1036
+ visit(template.body);
1037
+ return new Map([...keyed].map((parameter) => [
1038
+ parameter,
1039
+ nonPartyKeys.get(parameter) ?? new Set(),
1040
+ ]));
1041
+ }
1042
+ function bindPortCompileValues(values, binding, value, ports, diagnostics) {
1043
+ const port = ports.get(value.name.name);
1044
+ if (!port) {
1045
+ diagnostics.push({
1046
+ code: "HSX1008",
1047
+ fix: `declare port ${value.name.name} { allowed: [...]; } or correct the port name`,
1048
+ message: `decision port ${value.name.name} is not declared`,
1049
+ severity: "error",
1050
+ span: value.name.span,
1051
+ });
1052
+ return;
1053
+ }
1054
+ const allowed = port ? entry(port.body, "allowed")?.value : undefined;
1055
+ const shape = entry(port.body, "shape")?.value ?? {
1056
+ entries: [],
1057
+ kind: "block",
1058
+ span: port.body.span,
1059
+ };
1060
+ if (allowed)
1061
+ values.set(`${binding}_allowed`, allowed);
1062
+ values.set(`${binding}_fields`, shape);
1063
+ if (value.within)
1064
+ values.set(`${binding}_within`, value.within);
1065
+ if (value.deadline)
1066
+ values.set(`${binding}_deadline`, value.deadline);
1067
+ }
1068
+ function substituteExpr(expr, values) {
1069
+ if (expr.kind === "ident") {
1070
+ return values.get(expr.name) ?? substituteName(expr, values);
1071
+ }
1072
+ if (expr.kind === "string") {
1073
+ return {
1074
+ ...expr,
1075
+ value: expr.value.replaceAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g, (_, name) => {
1076
+ const value = values.get(String(name));
1077
+ if (value?.kind === "ident")
1078
+ return value.name;
1079
+ if (value?.kind === "number")
1080
+ return value.raw;
1081
+ if (value?.kind === "string")
1082
+ return value.value;
1083
+ if (value?.kind === "percent")
1084
+ return `${value.raw}%`;
1085
+ if (value?.kind === "binding")
1086
+ return value.name.name;
1087
+ if (value?.kind === "port_ref")
1088
+ return value.name.name;
1089
+ return `{${String(name)}}`;
1090
+ }),
1091
+ };
1092
+ }
1093
+ if (expr.kind === "block") {
1094
+ return {
1095
+ ...expr,
1096
+ entries: expr.entries.map((entry) => {
1097
+ const nestedValues = entry.iteration
1098
+ ? new Map([...values].filter(([name]) => name !== entry.iteration?.binding.name))
1099
+ : values;
1100
+ return {
1101
+ ...entry,
1102
+ ...(entry.iteration
1103
+ ? {
1104
+ iteration: {
1105
+ ...entry.iteration,
1106
+ bound: substituteExpr(entry.iteration.bound, values),
1107
+ },
1108
+ }
1109
+ : {}),
1110
+ key: substituteName(entry.key, nestedValues),
1111
+ qualifiers: entry.qualifiers.map((qualifier, index) => {
1112
+ const value = nestedValues.get(qualifier.name);
1113
+ const fixed = fixedQualifier(entry.key.name, qualifier.name, index);
1114
+ return !fixed && value
1115
+ ? nameFromValue(qualifier, value)
1116
+ : substituteName(qualifier, nestedValues);
1117
+ }),
1118
+ value: substituteExpr(entry.value, nestedValues),
1119
+ };
1120
+ }),
1121
+ };
1122
+ }
1123
+ if (expr.kind === "list") {
1124
+ return {
1125
+ ...expr,
1126
+ items: expr.items.map((item) => substituteExpr(item, values)),
1127
+ };
1128
+ }
1129
+ if (expr.kind === "type_apply") {
1130
+ return {
1131
+ ...expr,
1132
+ args: expr.args.map((arg) => substituteExpr(arg, values)),
1133
+ };
1134
+ }
1135
+ if (expr.kind === "call") {
1136
+ const hasUnbound = expr.args.some((arg) => hasUnboundCompileIdentifier(arg, values));
1137
+ const substituted = {
1138
+ ...expr,
1139
+ args: expr.args.map((arg) => substituteCompileArgument(arg, values)),
1140
+ };
1141
+ const structural = new Set([
1142
+ "at",
1143
+ "get",
1144
+ "keys",
1145
+ "keys_except",
1146
+ "kind",
1147
+ "len",
1148
+ "values",
1149
+ ]).has(expr.callee.name);
1150
+ return hasUnbound && !structural
1151
+ ? substituted
1152
+ : (evaluateCompileTimeCall(substituted, !hasUnbound) ?? substituted);
1153
+ }
1154
+ if (expr.kind === "apply") {
1155
+ return {
1156
+ ...expr,
1157
+ args: expr.args.map((arg) => substituteExpr(arg, values)),
1158
+ typeArgs: expr.typeArgs.map((arg) => substituteExpr(arg, values)),
1159
+ };
1160
+ }
1161
+ if (expr.kind === "binding") {
1162
+ return { ...expr, type: substituteExpr(expr.type, values) };
1163
+ }
1164
+ if (expr.kind === "decided_amount") {
1165
+ return { ...expr, body: substituteExpr(expr.body, values) };
1166
+ }
1167
+ return expr;
1168
+ }
1169
+ function substituteCompileArgument(expr, values) {
1170
+ if (expr.kind !== "ident")
1171
+ return substituteExpr(expr, values);
1172
+ const value = values.get(expr.name);
1173
+ if (!value)
1174
+ return expr;
1175
+ if (value.kind === "ident" || value.kind === "port_ref") {
1176
+ return { kind: "string", span: value.span, value: nameText(value) };
1177
+ }
1178
+ return value;
1179
+ }
1180
+ function hasUnboundCompileIdentifier(expr, values) {
1181
+ if (expr.kind === "ident")
1182
+ return !values.has(expr.name);
1183
+ if (expr.kind === "call" || expr.kind === "type_apply") {
1184
+ return expr.args.some((arg) => hasUnboundCompileIdentifier(arg, values));
1185
+ }
1186
+ if (expr.kind === "list") {
1187
+ return expr.items.some((item) => hasUnboundCompileIdentifier(item, values));
1188
+ }
1189
+ return false;
1190
+ }
1191
+ function optionalInner(type) {
1192
+ return type.kind === "type_apply" &&
1193
+ type.callee.name === "optional" &&
1194
+ type.args.length === 1
1195
+ ? type.args[0]
1196
+ : undefined;
1197
+ }
1198
+ function resolveAlias(expr, aliases, seen = new Set()) {
1199
+ if (expr.kind !== "ident")
1200
+ return expr;
1201
+ const alias = aliases.get(expr.name);
1202
+ if (!alias || seen.has(expr.name))
1203
+ return expr;
1204
+ return resolveAlias(alias, aliases, new Set([...seen, expr.name]));
1205
+ }
1206
+ function argumentMatches(expectedExpression, actualExpression, parties, ports, aliases) {
1207
+ const expected = resolveAlias(expectedExpression, aliases);
1208
+ const actual = actualExpression.kind === "binding"
1209
+ ? resolveAlias(actualExpression.type, aliases)
1210
+ : actualExpression;
1211
+ if (expected.kind === "type_apply") {
1212
+ if (expected.callee.name === "list") {
1213
+ const itemType = expected.args[0];
1214
+ return Boolean(itemType &&
1215
+ actual.kind === "list" &&
1216
+ actual.items.every((item) => argumentMatches(itemType, item, parties, ports, aliases)));
1217
+ }
1218
+ if (expected.callee.name === "optional") {
1219
+ const inner = expected.args[0];
1220
+ return Boolean(inner && argumentMatches(inner, actual, parties, ports, aliases));
1221
+ }
1222
+ }
1223
+ const expectedType = typeOf(expected, aliases);
1224
+ const actualType = typeOf(actual, aliases);
1225
+ switch (expectedType.kind) {
1226
+ case "money":
1227
+ return actualType.kind === "money";
1228
+ case "party":
1229
+ return actual.kind === "ident";
1230
+ case "condition":
1231
+ return actual.kind === "port_ref";
1232
+ case "text":
1233
+ return actual.kind === "string" || actual.kind === "ident";
1234
+ case "date":
1235
+ return actualType.kind === "date" || actual.kind === "ident";
1236
+ case "bps":
1237
+ return actualType.kind === "bps" || actual.kind === "percent";
1238
+ case "percent":
1239
+ return actual.kind === "percent";
1240
+ case "integer":
1241
+ case "boolean":
1242
+ case "account":
1243
+ return actualType.kind === expectedType.kind;
1244
+ case "ref":
1245
+ return (actualType.kind === "ref" || actualExpression.kind === "settlement_ref");
1246
+ case "unknown":
1247
+ if (expected.kind !== "ident")
1248
+ return false;
1249
+ if (expected.name === "unknown")
1250
+ return true;
1251
+ return (expected.name === "block" &&
1252
+ (actual.kind === "block" || actual.kind === "decided_amount"));
1253
+ }
1254
+ }
1255
+ function argumentWords(expression, aliases) {
1256
+ if (expression.kind === "list")
1257
+ return "list";
1258
+ if (expression.kind === "block")
1259
+ return "block";
1260
+ if (expression.kind === "port_ref")
1261
+ return "decision port";
1262
+ return typeWords(typeOf(expression, aliases));
1263
+ }
1264
+ function booleanExpr(value, span) {
1265
+ return { kind: "boolean", span, value };
1266
+ }
1267
+ function selectCompileTimeRows(block, values, diagnostics) {
1268
+ const entries = [];
1269
+ const locals = new Map(values);
1270
+ for (const row of block.entries) {
1271
+ if (row.key.name === "let") {
1272
+ const binding = row.qualifiers[0];
1273
+ if (binding)
1274
+ locals.set(binding.name, substituteExpr(row.value, locals));
1275
+ entries.push(row);
1276
+ continue;
1277
+ }
1278
+ if (row.key.name === "unsupported") {
1279
+ const value = substituteExpr(row.value, locals);
1280
+ if (value.kind !== "block") {
1281
+ diagnostics.push({
1282
+ code: "HSX1405",
1283
+ fix: "give unsupported a block with code, message, and fix rows",
1284
+ message: "unsupported needs a fixed diagnostic block",
1285
+ severity: "error",
1286
+ span: row.span,
1287
+ });
1288
+ continue;
1289
+ }
1290
+ const diagnosticValue = (name) => {
1291
+ const entry = value.entries.find((candidate) => candidate.key.name === name);
1292
+ return entry ? nameText(entry.value) : undefined;
1293
+ };
1294
+ diagnostics.push({
1295
+ code: diagnosticValue("code") ?? "HSX1110",
1296
+ fix: diagnosticValue("fix") ??
1297
+ "choose a supported compile-time parameter combination",
1298
+ message: diagnosticValue("message") ??
1299
+ "this compile-time parameter combination is not supported",
1300
+ severity: "error",
1301
+ span: row.span,
1302
+ });
1303
+ continue;
1304
+ }
1305
+ if (row.key.name !== "when" &&
1306
+ row.key.name !== "when_not" &&
1307
+ row.key.name !== "when_eq") {
1308
+ entries.push({
1309
+ ...row,
1310
+ value: row.value.kind === "block" && !row.iteration
1311
+ ? selectCompileTimeRows(row.value, locals, diagnostics)
1312
+ : row.value,
1313
+ });
1314
+ continue;
1315
+ }
1316
+ const subject = row.qualifiers[0];
1317
+ const actual = subject ? locals.get(subject.name) : undefined;
1318
+ let include = false;
1319
+ if (row.key.name === "when_eq") {
1320
+ const expected = row.qualifiers[1];
1321
+ include = Boolean(actual && expected && nameText(actual) === expected.name);
1322
+ }
1323
+ else {
1324
+ include = compileTimeTruthy(actual);
1325
+ if (row.key.name === "when_not")
1326
+ include = !include;
1327
+ }
1328
+ if (row.value.kind !== "block") {
1329
+ diagnostics.push({
1330
+ code: "HSX1405",
1331
+ fix: "give the compile-time condition a body in braces",
1332
+ message: `${row.key.name} needs a fixed block to select`,
1333
+ severity: "error",
1334
+ span: row.span,
1335
+ });
1336
+ continue;
1337
+ }
1338
+ if (include) {
1339
+ entries.push(...selectCompileTimeRows(row.value, locals, diagnostics).entries);
1340
+ }
1341
+ }
1342
+ return { ...block, entries };
1343
+ }
1344
+ function compileTimeTruthy(value) {
1345
+ if (!value)
1346
+ return false;
1347
+ if (value.kind === "string" && value.value === NONE_SENTINEL)
1348
+ return false;
1349
+ if (value.kind === "boolean")
1350
+ return value.value;
1351
+ if (value.kind === "number")
1352
+ return Number(value.raw) !== 0;
1353
+ if (value.kind === "list")
1354
+ return value.items.length > 0;
1355
+ if (value.kind === "block")
1356
+ return value.entries.length > 0;
1357
+ return true;
1358
+ }
1359
+ function evaluateCompileTimeCall(expr, resolveMissing = true) {
1360
+ const [first, second, third, fourth] = expr.args;
1361
+ const number = (value) => {
1362
+ if (value?.kind === "percent")
1363
+ return value.bps;
1364
+ if (value?.kind !== "number" || value.raw.includes("."))
1365
+ return undefined;
1366
+ const parsed = Number(value.raw);
1367
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
1368
+ };
1369
+ const numeric = (value) => ({
1370
+ kind: "number",
1371
+ raw: String(value),
1372
+ span: expr.span,
1373
+ });
1374
+ const text = (value) => ({
1375
+ kind: "string",
1376
+ span: expr.span,
1377
+ value,
1378
+ });
1379
+ const a = number(first);
1380
+ const b = number(second);
1381
+ switch (expr.callee.name) {
1382
+ case "add":
1383
+ return a === undefined || b === undefined ? undefined : numeric(a + b);
1384
+ case "sub":
1385
+ return a === undefined || b === undefined ? undefined : numeric(a - b);
1386
+ case "mul":
1387
+ return a === undefined || b === undefined ? undefined : numeric(a * b);
1388
+ case "div_floor":
1389
+ return a === undefined || b === undefined || b === 0
1390
+ ? undefined
1391
+ : numeric(Math.floor(a / b));
1392
+ case "mod":
1393
+ return a === undefined || b === undefined || b === 0
1394
+ ? undefined
1395
+ : numeric(a % b);
1396
+ case "min":
1397
+ return a === undefined || b === undefined
1398
+ ? undefined
1399
+ : numeric(Math.min(a, b));
1400
+ case "lt":
1401
+ return a === undefined || b === undefined
1402
+ ? undefined
1403
+ : numeric(a < b ? 1 : 0);
1404
+ case "len":
1405
+ return first?.kind === "list"
1406
+ ? numeric(first.items.length)
1407
+ : first?.kind === "block"
1408
+ ? numeric(first.entries.length)
1409
+ : first?.kind === "boolean" && !first.value
1410
+ ? numeric(0)
1411
+ : undefined;
1412
+ case "at": {
1413
+ const index = number(second);
1414
+ if (index === undefined)
1415
+ return undefined;
1416
+ if (first?.kind === "list")
1417
+ return first.items[index - 1];
1418
+ if (first?.kind === "block")
1419
+ return first.entries[index - 1]?.value;
1420
+ return undefined;
1421
+ }
1422
+ case "get": {
1423
+ const key = second ? nameText(second) : "";
1424
+ if (!key || key === "invalid_compile_time_name")
1425
+ return undefined;
1426
+ const body = first?.kind === "decided_amount" ? first.body : first;
1427
+ if (body?.kind !== "block") {
1428
+ return resolveMissing
1429
+ ? { kind: "string", span: expr.span, value: NONE_SENTINEL }
1430
+ : undefined;
1431
+ }
1432
+ const value = body.entries.find((row) => row.key.name === key)?.value;
1433
+ if (value || !resolveMissing)
1434
+ return value;
1435
+ return { kind: "string", span: expr.span, value: NONE_SENTINEL };
1436
+ }
1437
+ case "keys":
1438
+ return first?.kind === "block" || first?.kind === "decided_amount"
1439
+ ? {
1440
+ items: (first.kind === "decided_amount"
1441
+ ? first.body
1442
+ : first).entries.map((row) => ({ ...row.key })),
1443
+ kind: "list",
1444
+ span: expr.span,
1445
+ }
1446
+ : undefined;
1447
+ case "values":
1448
+ return first?.kind === "block"
1449
+ ? {
1450
+ items: first.entries.map((row) => row.value),
1451
+ kind: "list",
1452
+ span: expr.span,
1453
+ }
1454
+ : undefined;
1455
+ case "keys_except": {
1456
+ if (first?.kind !== "block")
1457
+ return undefined;
1458
+ const omitted = new Set(expr.args.slice(1).map(nameText));
1459
+ return {
1460
+ items: first.entries
1461
+ .filter((row) => !omitted.has(row.key.name))
1462
+ .map((row) => ({ ...row.key })),
1463
+ kind: "list",
1464
+ span: expr.span,
1465
+ };
1466
+ }
1467
+ case "kind":
1468
+ return first
1469
+ ? text(first.kind === "string" && first.value === NONE_SENTINEL
1470
+ ? "none"
1471
+ : first.kind)
1472
+ : undefined;
1473
+ case "basis_points":
1474
+ return first?.kind === "percent" ? numeric(first.bps) : undefined;
1475
+ case "names": {
1476
+ const count = number(second);
1477
+ if (!first || count === undefined || count < 0 || !third)
1478
+ return undefined;
1479
+ return {
1480
+ items: Array.from({ length: count }, (_, index) => text(`${nameText(first)}${index + 1}${nameText(third)}`)),
1481
+ kind: "list",
1482
+ span: expr.span,
1483
+ };
1484
+ }
1485
+ case "suffix_each":
1486
+ return first?.kind === "list" && second
1487
+ ? {
1488
+ items: first.items.map((item) => text(`${camel(nameText(item))}${nameText(second)}`)),
1489
+ kind: "list",
1490
+ span: expr.span,
1491
+ }
1492
+ : undefined;
1493
+ case "concat_lists":
1494
+ return expr.args.every((arg) => arg.kind === "list")
1495
+ ? {
1496
+ items: expr.args.flatMap((arg) => arg.kind === "list" ? arg.items : []),
1497
+ kind: "list",
1498
+ span: expr.span,
1499
+ }
1500
+ : undefined;
1501
+ case "concat":
1502
+ return text(expr.args.map(nameText).join(""));
1503
+ case "camel":
1504
+ return first ? text(camel(nameText(first))) : undefined;
1505
+ case "title":
1506
+ return first ? text(titleize(nameText(first))) : undefined;
1507
+ case "sentence":
1508
+ return first ? text(sentenceCase(nameText(first))) : undefined;
1509
+ case "percent_text": {
1510
+ const basisPoints = number(first);
1511
+ if (basisPoints === undefined)
1512
+ return undefined;
1513
+ const whole = Math.trunc(basisPoints / 100);
1514
+ const fraction = basisPoints % 100;
1515
+ const decimal = fraction === 0
1516
+ ? ""
1517
+ : fraction % 10 === 0
1518
+ ? `.${fraction / 10}`
1519
+ : `.${String(fraction).padStart(2, "0")}`;
1520
+ return text(`${whole}${decimal}%`);
1521
+ }
1522
+ case "words":
1523
+ return first ? text(nameText(first).replaceAll("_", " ")) : undefined;
1524
+ case "prefix":
1525
+ return first ? text(prefixFor(nameText(first))) : undefined;
1526
+ case "owner":
1527
+ return first?.kind === "settlement_ref"
1528
+ ? text(first.owner.name)
1529
+ : undefined;
1530
+ case "member":
1531
+ return first?.kind === "settlement_ref"
1532
+ ? text(first.member.name)
1533
+ : undefined;
1534
+ case "scale_duration": {
1535
+ if (!first || b === undefined)
1536
+ return undefined;
1537
+ const match = /^P([0-9]+)([DWMY])$/.exec(nameText(first));
1538
+ if (!match)
1539
+ return undefined;
1540
+ const amount = Number(match[1]) * b;
1541
+ return match[2] === "W"
1542
+ ? text(`P${amount * 7}D`)
1543
+ : text(`P${amount}${match[2]}`);
1544
+ }
1545
+ case "if_eq":
1546
+ return first && second && nameText(first) === nameText(second)
1547
+ ? third
1548
+ : fourth;
1549
+ default:
1550
+ return undefined;
1551
+ }
1552
+ }
1553
+ function substituteName(name, values) {
1554
+ return {
1555
+ ...name,
1556
+ name: name.name.replaceAll(/\[([A-Za-z][A-Za-z0-9_]*)\]/g, (whole, key) => {
1557
+ const value = values.get(String(key));
1558
+ return value ? nameText(value) : String(whole);
1559
+ }),
1560
+ };
1561
+ }
1562
+ function fixedQualifier(key, qualifier, index) {
1563
+ if (["action", "when", "when_not", "when_eq"].includes(key))
1564
+ return true;
1565
+ if (key === "on" && index === 0)
1566
+ return true;
1567
+ const normalizedKey = key.replaceAll("_", " ");
1568
+ return udlClauseVocabulary.some((definition) => definition.spelling === `${normalizedKey} ${qualifier}` ||
1569
+ definition.spelling ===
1570
+ `${normalizedKey} ${qualifier.replaceAll("_", " ")}`);
1571
+ }
1572
+ function nameFromValue(origin, value) {
1573
+ return { ...origin, name: nameText(value) };
1574
+ }
1575
+ function nameText(value) {
1576
+ if (value.kind === "binding")
1577
+ return value.name.name;
1578
+ if (value.kind === "ident")
1579
+ return value.name;
1580
+ if (value.kind === "number")
1581
+ return value.raw;
1582
+ if (value.kind === "string")
1583
+ return value.value;
1584
+ if (value.kind === "percent")
1585
+ return `${value.raw}%`;
1586
+ if (value.kind === "port_ref")
1587
+ return value.name.name;
1588
+ if (value.kind === "settlement_ref")
1589
+ return `${value.owner.name}.${value.member.name}`;
1590
+ return "invalid_compile_time_name";
1591
+ }
1592
+ function expandComprehensions(block, diagnostics) {
1593
+ const budget = { used: 0 };
1594
+ const expand = (current, inherited = new Map()) => {
1595
+ const entries = [];
1596
+ const locals = new Map(inherited);
1597
+ for (const row of current.entries) {
1598
+ if (row.key.name === "let") {
1599
+ const binding = row.qualifiers[0];
1600
+ if (!binding) {
1601
+ diagnostics.push({
1602
+ code: "HSX1405",
1603
+ fix: "write `let(name): <compile-time value>`",
1604
+ message: "a compile-time binding needs one name",
1605
+ severity: "error",
1606
+ span: row.span,
1607
+ });
1608
+ continue;
1609
+ }
1610
+ locals.set(binding.name, substituteExpr(row.value, locals));
1611
+ continue;
1612
+ }
1613
+ if (row.key.name === "when" ||
1614
+ row.key.name === "when_not" ||
1615
+ row.key.name === "when_eq") {
1616
+ const subject = row.qualifiers[0];
1617
+ const actual = subject ? locals.get(subject.name) : undefined;
1618
+ const expected = row.qualifiers[1];
1619
+ let include = row.key.name === "when_eq"
1620
+ ? Boolean(actual && expected && nameText(actual) === expected.name)
1621
+ : compileTimeTruthy(actual);
1622
+ if (row.key.name === "when_not")
1623
+ include = !include;
1624
+ if (include && row.value.kind === "block") {
1625
+ entries.push(...expand(row.value, locals).entries);
1626
+ }
1627
+ continue;
1628
+ }
1629
+ if (!row.iteration) {
1630
+ const substituted = {
1631
+ ...row,
1632
+ key: substituteName(row.key, locals),
1633
+ qualifiers: row.qualifiers.map((qualifier, index) => {
1634
+ const value = locals.get(qualifier.name);
1635
+ const fixed = fixedQualifier(row.key.name, qualifier.name, index);
1636
+ return !fixed && value
1637
+ ? nameFromValue(qualifier, value)
1638
+ : substituteName(qualifier, locals);
1639
+ }),
1640
+ value: substituteExpr(row.value, locals),
1641
+ };
1642
+ entries.push({
1643
+ ...substituted,
1644
+ value: substituted.value.kind === "block"
1645
+ ? expand(substituted.value, locals)
1646
+ : substituted.value,
1647
+ });
1648
+ continue;
1649
+ }
1650
+ if (row.value.kind !== "block")
1651
+ continue;
1652
+ const bound = substituteExpr(row.iteration.bound, locals);
1653
+ const values = finiteIterationValues(bound);
1654
+ if (!values) {
1655
+ diagnostics.push({
1656
+ code: "HSX1403",
1657
+ fix: "pass an integer literal or a literal finite list as the comprehension bound",
1658
+ message: `comprehension bound for ${row.iteration.binding.name} is not known at compile time`,
1659
+ severity: "error",
1660
+ span: bound.span,
1661
+ });
1662
+ continue;
1663
+ }
1664
+ if (budget.used + values.length > MAX_COMPREHENSION_EXPANSIONS) {
1665
+ diagnostics.push({
1666
+ code: "HSX1404",
1667
+ fix: `reduce the fixed expansion to at most ${MAX_COMPREHENSION_EXPANSIONS} rows`,
1668
+ message: `bounded comprehensions expand past the ${MAX_COMPREHENSION_EXPANSIONS}-row compiler limit`,
1669
+ severity: "error",
1670
+ span: bound.span,
1671
+ });
1672
+ continue;
1673
+ }
1674
+ budget.used += values.length;
1675
+ for (const value of values) {
1676
+ const substitutions = new Map([
1677
+ ...locals,
1678
+ [row.iteration.binding.name, value],
1679
+ ]);
1680
+ const body = substituteExpr(row.value, substitutions);
1681
+ if (body.kind === "block")
1682
+ entries.push(...expand(body, substitutions).entries);
1683
+ }
1684
+ }
1685
+ return { ...current, entries };
1686
+ };
1687
+ return expand(block);
1688
+ }
1689
+ function finiteIterationValues(bound) {
1690
+ if (bound.kind === "call") {
1691
+ const evaluated = evaluateCompileTimeCall(bound);
1692
+ return evaluated ? finiteIterationValues(evaluated) : undefined;
1693
+ }
1694
+ if (bound.kind === "list")
1695
+ return bound.items;
1696
+ if (bound.kind !== "number" || bound.raw.includes("."))
1697
+ return undefined;
1698
+ const count = Number(bound.raw);
1699
+ if (!Number.isSafeInteger(count) || count < 0)
1700
+ return undefined;
1701
+ return Array.from({ length: count }, (_, index) => ({
1702
+ kind: "number",
1703
+ raw: String(index + 1),
1704
+ span: bound.span,
1705
+ }));
1706
+ }
1707
+ function checkInstrument(name, body, parties, aliases, diagnostics) {
1708
+ if (!SNAKE_CASE.test(name.name)) {
1709
+ diagnostics.push({
1710
+ code: "HSX1003",
1711
+ fix: "rename the instrument with lowercase words joined by underscores",
1712
+ message: `instrument ${name.name} is not snake_case`,
1713
+ severity: "error",
1714
+ span: name.span,
1715
+ });
1716
+ }
1717
+ const fieldsEntry = entry(body, "fields");
1718
+ const fields = fieldsEntry?.value.kind === "block"
1719
+ ? checkFields(fieldsEntry.value, aliases, diagnostics)
1720
+ : [];
1721
+ if (!fieldsEntry) {
1722
+ diagnostics.push({
1723
+ code: "HSX1503",
1724
+ fix: "add a fields { ... } block, even when it is empty",
1725
+ message: `instrument ${name.name} has no fields block`,
1726
+ severity: "error",
1727
+ span: name.span,
1728
+ });
1729
+ }
1730
+ const fieldNames = new Set(fields.map((field) => field.name));
1731
+ if (fields.some((field) => field.type.kind === "money") &&
1732
+ !fieldNames.has("currency")) {
1733
+ fields.push({
1734
+ name: "currency",
1735
+ origin: fieldsEntry?.span ?? name.span,
1736
+ required: true,
1737
+ schema: {
1738
+ description: "ISO 4217 currency code",
1739
+ maxLength: 3,
1740
+ minLength: 3,
1741
+ pattern: "^[A-Z]{3}$",
1742
+ type: "string",
1743
+ },
1744
+ type: { kind: "text" },
1745
+ });
1746
+ fieldNames.add("currency");
1747
+ }
1748
+ const endpointFields = new Map();
1749
+ for (const field of fields) {
1750
+ if (field.type.kind === "account")
1751
+ endpointFields.set(field.name, field.name);
1752
+ }
1753
+ const slots = {};
1754
+ const partiesEntry = entry(body, "parties");
1755
+ if (partiesEntry?.value.kind === "block") {
1756
+ const roleObject = blockToObject(partiesEntry.value, new Map(), diagnostics);
1757
+ for (const [role, value] of Object.entries(roleObject)) {
1758
+ if (typeof value !== "string")
1759
+ continue;
1760
+ if (fieldNames.has(value)) {
1761
+ endpointFields.set(role, value);
1762
+ endpointFields.set(value, value);
1763
+ continue;
1764
+ }
1765
+ if (parties.has(value)) {
1766
+ const field = `${camel(value)}AccountId`;
1767
+ roleObject[role] = field;
1768
+ endpointFields.set(role, field);
1769
+ endpointFields.set(value, field);
1770
+ if (!fieldNames.has(field)) {
1771
+ fields.push({
1772
+ name: field,
1773
+ origin: partiesEntry.span,
1774
+ required: true,
1775
+ schema: {
1776
+ description: `The ${value.replaceAll("_", " ")} account`,
1777
+ pattern: "^acct_(sandbox|live)_[a-z0-9]{8,64}$",
1778
+ type: "string",
1779
+ "x-hyperscale-reference-filter": {
1780
+ column: "role",
1781
+ values: ["customer_balance"],
1782
+ },
1783
+ },
1784
+ type: { kind: "account" },
1785
+ });
1786
+ fieldNames.add(field);
1787
+ }
1788
+ continue;
1789
+ }
1790
+ diagnostics.push({
1791
+ code: "HSX1001",
1792
+ fix: "bind the role to a declared party or field",
1793
+ message: `party role refers to ${String(value)}, which is not declared`,
1794
+ severity: "error",
1795
+ span: partiesEntry.value.entries.find((entry) => entry.key.name === role)
1796
+ ?.value.span ?? partiesEntry.span,
1797
+ });
1798
+ }
1799
+ slots.parties = roleObject;
1800
+ }
1801
+ const actionEntries = body.entries.filter((candidate) => candidate.key.name === "action" && candidate.value.kind === "block");
1802
+ const actionNames = new Set(actionEntries.map((candidate) => candidate.qualifiers[0]?.name ?? ""));
1803
+ const holdDestinations = new Map();
1804
+ for (const actionEntry of actionEntries) {
1805
+ if (actionEntry.value.kind !== "block")
1806
+ continue;
1807
+ for (const move of friendlyMoveRows(actionEntry.value)) {
1808
+ if (move.operation === "post" && move.from && move.to) {
1809
+ const destination = endpointFields.get(move.to);
1810
+ if (destination)
1811
+ holdDestinations.set(move.from, destination);
1812
+ }
1813
+ }
1814
+ }
1815
+ for (const actionEntry of actionEntries) {
1816
+ if (actionEntry.value.kind !== "block")
1817
+ continue;
1818
+ for (const move of friendlyMoveRows(actionEntry.value)) {
1819
+ for (const endpoint of [move.from, move.to]) {
1820
+ if (!endpoint ||
1821
+ endpoint.startsWith("refs.") ||
1822
+ endpointFields.has(endpoint) ||
1823
+ holdDestinations.has(endpoint)) {
1824
+ continue;
1825
+ }
1826
+ const field = `${camel(endpoint)}AccountId`;
1827
+ endpointFields.set(endpoint, field);
1828
+ if (!fieldNames.has(field)) {
1829
+ fields.push({
1830
+ name: field,
1831
+ origin: actionEntry.span,
1832
+ required: true,
1833
+ schema: {
1834
+ pattern: "^acct_(sandbox|live)_[a-z0-9]{8,64}$",
1835
+ type: "string",
1836
+ },
1837
+ type: { kind: "account" },
1838
+ });
1839
+ fieldNames.add(field);
1840
+ }
1841
+ }
1842
+ }
1843
+ }
1844
+ const actions = actionEntries.flatMap((candidate) => {
1845
+ const actionName = candidate.qualifiers[0];
1846
+ if (!actionName || candidate.value.kind !== "block")
1847
+ return [];
1848
+ return [
1849
+ checkAction(actionName, candidate.value, fields, name.name, endpointFields, holdDestinations, diagnostics),
1850
+ ];
1851
+ });
1852
+ if (!actionNames.has("create")) {
1853
+ diagnostics.push({
1854
+ code: "HSX1504",
1855
+ fix: 'add action create { summary: "Create ..."; steps: [] }',
1856
+ message: `instrument ${name.name} has no create action`,
1857
+ severity: "error",
1858
+ span: name.span,
1859
+ });
1860
+ }
1861
+ const lifecycleEntry = entry(body, "lifecycle");
1862
+ const lifecycle = lifecycleEntry?.value.kind === "block"
1863
+ ? checkLifecycle(name.name, lifecycleEntry.value, actionNames, diagnostics)
1864
+ : undefined;
1865
+ if (!lifecycle) {
1866
+ diagnostics.push({
1867
+ code: "HSX1402",
1868
+ fix: "add lifecycle { states ...; initial ...; on ...; }",
1869
+ message: `instrument ${name.name} has no valid lifecycle`,
1870
+ severity: "error",
1871
+ span: name.span,
1872
+ });
1873
+ }
1874
+ for (const row of body.entries) {
1875
+ if (row.key.name === "fields" ||
1876
+ row.key.name === "lifecycle" ||
1877
+ row.key.name === "parties" ||
1878
+ row.key.name === "action")
1879
+ continue;
1880
+ if (row.key.name === "required") {
1881
+ const explicit = exprToJsonForUdlSlot(row.value, "required", diagnostics);
1882
+ const checked = checkRequiredFields(explicit, fields, row.span, diagnostics);
1883
+ if (checked)
1884
+ slots.required = checked;
1885
+ continue;
1886
+ }
1887
+ const spelling = clauseSpelling(row, instrumentClauseBySpelling);
1888
+ const definition = instrumentClauseBySpelling.get(spelling);
1889
+ if (definition?.scope === "instrument") {
1890
+ setClause(slots, definition.target, definition.cardinality, exprToJsonForUdlSlot(row.value, definition.target, diagnostics), definition.spelling, row.span, diagnostics);
1891
+ continue;
1892
+ }
1893
+ if (!definition && row.key.name !== "required") {
1894
+ diagnostics.push({
1895
+ code: "HSX1501",
1896
+ fix: "use a clause exported by the targeted UDL definition set",
1897
+ message: `instrument clause ${spelling} does not exist in the targeted UDL vocabulary`,
1898
+ severity: "error",
1899
+ span: row.key.span,
1900
+ });
1901
+ }
1902
+ }
1903
+ if (lifecycle) {
1904
+ slots.lifecycle = lifecycle.value;
1905
+ if (Object.keys(lifecycle.parked).length > 0) {
1906
+ slots.callerParkedStates = lifecycle.parked;
1907
+ }
1908
+ }
1909
+ slots.idPrefix ??=
1910
+ stringSlot(body, "idPrefix", "id_prefix") ?? prefixFor(name.name);
1911
+ slots.summary ??= `${titleize(name.name)} instrument`;
1912
+ slots.title ??= titleize(name.name);
1913
+ linearityDiagnostics(actionEntries, diagnostics);
1914
+ return { actions, fields, id: name.name, origin: name.span, slots };
1915
+ }
1916
+ function checkRequiredFields(value, fields, span, diagnostics) {
1917
+ const report = (message, fix) => {
1918
+ diagnostics.push({
1919
+ code: "HSX1023",
1920
+ fix,
1921
+ message,
1922
+ severity: "error",
1923
+ span,
1924
+ });
1925
+ };
1926
+ if (!Array.isArray(value)) {
1927
+ report("required must be a list of field names", "write required as a list, such as [amount, currency]");
1928
+ return undefined;
1929
+ }
1930
+ const fieldByName = new Map(fields.map((field) => [field.name, field]));
1931
+ const seen = new Set();
1932
+ let valid = true;
1933
+ for (const item of value) {
1934
+ if (typeof item !== "string") {
1935
+ valid = false;
1936
+ report("required contains a value that is not a field name", "keep only declared non-optional field names in required");
1937
+ continue;
1938
+ }
1939
+ if (seen.has(item)) {
1940
+ valid = false;
1941
+ report(`required names ${item} more than once`, `keep one ${item} entry in required`);
1942
+ continue;
1943
+ }
1944
+ seen.add(item);
1945
+ const field = fieldByName.get(item);
1946
+ if (!field) {
1947
+ valid = false;
1948
+ report(`required names unknown field ${item}`, "name a declared non-optional field in required");
1949
+ continue;
1950
+ }
1951
+ if (!field.required) {
1952
+ valid = false;
1953
+ report(`required names optional field ${item}`, `remove ${item} from required or make the field non-optional`);
1954
+ }
1955
+ }
1956
+ return valid ? value : undefined;
1957
+ }
1958
+ function checkFields(block, aliases, diagnostics) {
1959
+ const result = [];
1960
+ const names = new Set();
1961
+ for (const row of block.entries) {
1962
+ const name = camel(row.key.name);
1963
+ if (!CAMEL_CASE.test(name)) {
1964
+ diagnostics.push({
1965
+ code: "HSX1005",
1966
+ fix: "rename the field in lower camelCase",
1967
+ message: `field ${row.key.name} is not lower camelCase`,
1968
+ severity: "error",
1969
+ span: row.key.span,
1970
+ });
1971
+ }
1972
+ if (names.has(name)) {
1973
+ diagnostics.push({
1974
+ code: "HSX1004",
1975
+ fix: `keep one ${name} field`,
1976
+ message: `field ${name} is declared twice`,
1977
+ severity: "error",
1978
+ span: row.key.span,
1979
+ });
1980
+ continue;
1981
+ }
1982
+ names.add(name);
1983
+ let typeExpr = row.value;
1984
+ let description;
1985
+ let required = true;
1986
+ let extra = {};
1987
+ if (row.value.kind === "block") {
1988
+ const typeRow = entry(row.value, "type");
1989
+ if (typeRow)
1990
+ typeExpr = typeRow.value;
1991
+ const desc = entry(row.value, "description") ?? entry(row.value, "desc");
1992
+ if (desc?.value.kind === "string")
1993
+ description = desc.value.value;
1994
+ const optional = entry(row.value, "optional");
1995
+ if (optional?.value.kind === "boolean")
1996
+ required = !optional.value.value;
1997
+ extra = blockToObject({
1998
+ ...row.value,
1999
+ entries: row.value.entries.filter((item) => !["type", "description", "desc", "optional"].includes(item.key.name)),
2000
+ }, new Map(), diagnostics);
2001
+ }
2002
+ const type = typeOf(typeExpr, aliases);
2003
+ const fixedMoneyIssue = invalidFixedMoneyBinding(typeExpr);
2004
+ if (fixedMoneyIssue) {
2005
+ diagnostics.push({
2006
+ code: "HSX1103",
2007
+ fix: `write ${name} as money(SAR, 2500) with a positive safe integer in minor units`,
2008
+ message: `field ${name} ${fixedMoneyIssue}`,
2009
+ severity: "error",
2010
+ span: typeExpr.span,
2011
+ });
2012
+ }
2013
+ if ((typeExpr.kind === "ident" && typeExpr.name === "ref") ||
2014
+ (type.kind === "ref" && !type.target)) {
2015
+ diagnostics.push({
2016
+ code: "HSX1007",
2017
+ fix: "declare the target instrument as ref<instrument_id>",
2018
+ message: `field ${name} declares ref without an instrument target`,
2019
+ severity: "error",
2020
+ span: typeExpr.span,
2021
+ });
2022
+ }
2023
+ const structuralType = Object.hasOwn(extra, "items")
2024
+ ? "array"
2025
+ : Object.hasOwn(extra, "properties") ||
2026
+ Object.hasOwn(extra, "additionalProperties")
2027
+ ? "object"
2028
+ : undefined;
2029
+ const schema = {
2030
+ ...schemaFor(type, required),
2031
+ ...extra,
2032
+ ...(structuralType ? { type: structuralType } : {}),
2033
+ ...(description ? { description } : {}),
2034
+ };
2035
+ result.push({ name, origin: row.span, required, schema, type });
2036
+ }
2037
+ return result;
2038
+ }
2039
+ function checkLifecycle(instrument, block, actions, diagnostics) {
2040
+ const statesRows = block.entries.filter((row) => row.key.name === "states");
2041
+ const initialRow = entry(block, "initial");
2042
+ const states = statesRows.flatMap((row) => row.value.kind === "list"
2043
+ ? row.value.items.flatMap((item) => item.kind === "ident"
2044
+ ? [item.name]
2045
+ : item.kind === "string"
2046
+ ? [item.value]
2047
+ : [])
2048
+ : []);
2049
+ const initial = initialRow?.value.kind === "ident"
2050
+ ? initialRow.value.name
2051
+ : initialRow?.value.kind === "string"
2052
+ ? initialRow.value.value
2053
+ : "";
2054
+ if (states.length === 0 || !initial)
2055
+ return undefined;
2056
+ const stateSet = new Set(states);
2057
+ if (!stateSet.has(initial)) {
2058
+ diagnostics.push({
2059
+ code: "HSX1001",
2060
+ fix: `add ${initial} to the states row or choose a declared initial state`,
2061
+ message: `lifecycle initial state ${initial} is not declared by ${instrument}`,
2062
+ severity: "error",
2063
+ span: initialRow?.span ?? block.span,
2064
+ });
2065
+ }
2066
+ const transitions = {};
2067
+ const parked = {};
2068
+ for (const row of block.entries) {
2069
+ if (row.key.name === "parked") {
2070
+ const state = row.qualifiers[0]?.name;
2071
+ if (state && row.value.kind === "string")
2072
+ parked[state] = row.value.value;
2073
+ continue;
2074
+ }
2075
+ if (row.key.name !== "on")
2076
+ continue;
2077
+ const action = row.qualifiers[0]?.name ?? "";
2078
+ const from = row.qualifiers.slice(1).map((item) => item.name);
2079
+ const to = row.value.kind === "ident"
2080
+ ? row.value.name
2081
+ : row.value.kind === "string"
2082
+ ? row.value.value
2083
+ : "";
2084
+ if (!actions.has(action)) {
2085
+ diagnostics.push({
2086
+ code: "HSX1001",
2087
+ fix: `declare action ${action} { ... } or remove its transition`,
2088
+ message: `lifecycle transition names action ${action}, which ${instrument} does not declare`,
2089
+ severity: "error",
2090
+ span: row.span,
2091
+ });
2092
+ }
2093
+ for (const state of [...from, to]) {
2094
+ if (stateSet.has(state))
2095
+ continue;
2096
+ diagnostics.push({
2097
+ code: "HSX1001",
2098
+ fix: `add ${state} to states or correct the transition`,
2099
+ message: `lifecycle transition ${action} refers to undeclared state ${state}`,
2100
+ severity: "error",
2101
+ span: row.span,
2102
+ });
2103
+ }
2104
+ transitions[action] = { from, to };
2105
+ }
2106
+ return { parked, value: { initial, states, transitions } };
2107
+ }
2108
+ function checkAction(name, body, fields, instrumentId, endpointFields, holdDestinations, diagnostics) {
2109
+ const slots = {};
2110
+ let omitsPublicAction = false;
2111
+ const fieldNames = new Set(fields.map((field) => field.name));
2112
+ for (const row of body.entries) {
2113
+ if (["for", "while", "loop", "recurse"].includes(row.key.name)) {
2114
+ diagnostics.push({
2115
+ code: "HSX1401",
2116
+ fix: "use a bounded comprehension over list<T, N> or a declared finite schedule",
2117
+ message: `${row.key.name} is unbounded; every HSX action must terminate at compile time`,
2118
+ severity: "error",
2119
+ span: row.key.span,
2120
+ });
2121
+ continue;
2122
+ }
2123
+ if (row.key.name === "public" &&
2124
+ row.qualifiers.length === 0 &&
2125
+ row.value.kind === "ident" &&
2126
+ row.value.name === "none") {
2127
+ omitsPublicAction = true;
2128
+ continue;
2129
+ }
2130
+ const spelling = clauseSpelling(row, actionClauseBySpelling);
2131
+ const definition = actionClauseBySpelling.get(spelling);
2132
+ if (definition?.scope === "action" && spelling === "notify") {
2133
+ const role = row.qualifiers[0]?.name;
2134
+ const channel = row.qualifiers[1]?.name;
2135
+ if (role && channel) {
2136
+ const notification = { channel, role };
2137
+ setClause(slots, definition.target, definition.cardinality, notification, definition.spelling, row.span, diagnostics);
2138
+ }
2139
+ continue;
2140
+ }
2141
+ if (definition?.scope === "action") {
2142
+ const rawValue = exprToJsonForUdlSlot(row.value, definition.target, diagnostics);
2143
+ const moveOffset = Array.isArray(slots.moves) ? slots.moves.length : 0;
2144
+ const value = definition.target === "moves"
2145
+ ? normalizeMoves(rawValue, name.name, instrumentId, fields, endpointFields, holdDestinations, diagnostics, row.span, moveOffset)
2146
+ : rawValue;
2147
+ setClause(slots, definition.target, definition.cardinality, value, definition.spelling, row.span, diagnostics);
2148
+ continue;
2149
+ }
2150
+ diagnostics.push({
2151
+ code: "HSX1501",
2152
+ fix: "use a clause exported by the targeted UDL definition set",
2153
+ message: `action clause ${spelling} does not exist in the targeted UDL vocabulary`,
2154
+ severity: "error",
2155
+ span: row.key.span,
2156
+ });
2157
+ }
2158
+ slots.summary ??= `${titleize(name.name)} ${fields.length === 1 ? (fields[0]?.name ?? "instrument") : "instrument"}`;
2159
+ slots.steps ??= [];
2160
+ slots.moves ??= [];
2161
+ if (omitsPublicAction && slots.publicAction !== undefined) {
2162
+ diagnostics.push({
2163
+ code: "HSX1506",
2164
+ fix: "keep either `public: none` or `public action: actionName`",
2165
+ message: `action ${name.name} both omits and names its public action`,
2166
+ severity: "error",
2167
+ span: name.span,
2168
+ });
2169
+ }
2170
+ if (!omitsPublicAction &&
2171
+ slots.publicAction === undefined &&
2172
+ slots.due === undefined &&
2173
+ slots.reconcile === undefined) {
2174
+ slots.publicAction = camel(`${name.name}_${instrumentId}`);
2175
+ }
2176
+ bindFieldReferences(slots, fieldNames, name, diagnostics);
2177
+ const effects = deriveUdlActionEffects(slots, udlClauseVocabulary);
2178
+ if (Object.keys(effects).length > 0) {
2179
+ slots.effects = effects;
2180
+ }
2181
+ else {
2182
+ delete slots.effects;
2183
+ }
2184
+ return { effects, name: name.name, origin: name.span, slots };
2185
+ }
2186
+ function normalizeMoves(value, action, instrumentId, fields, endpointFields, holdDestinations, diagnostics, span, offset = 0) {
2187
+ if (!Array.isArray(value))
2188
+ return value;
2189
+ const fieldByName = new Map(fields.map((field) => [field.name, field]));
2190
+ return value.map((candidate, index) => {
2191
+ if (!isJsonObject(candidate) || candidate.bind !== undefined)
2192
+ return candidate;
2193
+ const amount = typeof candidate.amount === "string" ? candidate.amount : undefined;
2194
+ const from = typeof candidate.from === "string" ? candidate.from : undefined;
2195
+ const to = typeof candidate.to === "string" ? candidate.to : undefined;
2196
+ const operationName = typeof candidate.operation === "string" ? candidate.operation : "create";
2197
+ if (!amount || !from || !to)
2198
+ return candidate;
2199
+ const operation = `internal_transfer.${operationName}`;
2200
+ const key = typeof candidate.key === "string"
2201
+ ? candidate.key
2202
+ : `${action}_${offset + index + 1}`;
2203
+ if (operationName === "post" || operationName === "void") {
2204
+ return {
2205
+ bind: {
2206
+ transferId: { from: "instance", path: "refs.reservationId" },
2207
+ },
2208
+ key,
2209
+ operation,
2210
+ };
2211
+ }
2212
+ const sourcePath = from.startsWith("refs.")
2213
+ ? from
2214
+ : endpointFields.get(from)
2215
+ ? `fields.${endpointFields.get(from)}`
2216
+ : undefined;
2217
+ const destinationEndpoint = operationName === "reserve"
2218
+ ? (holdDestinations.get(to) ?? endpointFields.get(to))
2219
+ : endpointFields.get(to);
2220
+ const destinationPath = to.startsWith("refs.")
2221
+ ? to
2222
+ : destinationEndpoint
2223
+ ? `fields.${destinationEndpoint}`
2224
+ : undefined;
2225
+ if (!sourcePath || !destinationPath) {
2226
+ const missing = !sourcePath ? from : to;
2227
+ diagnostics.push({
2228
+ code: "HSX1001",
2229
+ fix: `bind ${missing} in the instrument parties block`,
2230
+ message: `action ${action} moves money through ${missing}, which has no bound account field`,
2231
+ severity: "error",
2232
+ span,
2233
+ });
2234
+ return candidate;
2235
+ }
2236
+ const amountPath = amount.includes(".") ? amount : `fields.${amount}`;
2237
+ const move = {
2238
+ bind: {
2239
+ amount: { from: "instance", path: amountPath },
2240
+ currency: { from: "instance", path: "fields.currency" },
2241
+ destinationAccountId: {
2242
+ from: "instance",
2243
+ path: destinationPath,
2244
+ },
2245
+ sourceAccountId: {
2246
+ from: "instance",
2247
+ path: sourcePath,
2248
+ },
2249
+ "metadata.instrumentId": { from: "const", value: instrumentId },
2250
+ "metadata.instrumentInstanceId": {
2251
+ from: "instance",
2252
+ path: "instrumentInstanceId",
2253
+ },
2254
+ "metadata.phase": { from: "const", value: action },
2255
+ productId: { from: "instance", path: "productId" },
2256
+ },
2257
+ key,
2258
+ operation,
2259
+ };
2260
+ if (operationName === "reserve") {
2261
+ move.capture = {
2262
+ [camel(`${action}_${key}_transfer_id`)]: "transferId",
2263
+ };
2264
+ }
2265
+ else if (operationName === "create") {
2266
+ move.capture = {
2267
+ [camel(`${action}_${key}_transfer_id`)]: "transferId",
2268
+ };
2269
+ }
2270
+ if (!amountPath.startsWith("refs.") &&
2271
+ !fieldByName.has(amountPath.replace(/^fields\./, ""))) {
2272
+ diagnostics.push({
2273
+ code: "HSX1001",
2274
+ fix: `declare the money field named by ${amount}`,
2275
+ message: `action ${action} moves ${amount}, which is not a declared money field`,
2276
+ severity: "error",
2277
+ span,
2278
+ });
2279
+ }
2280
+ return move;
2281
+ });
2282
+ }
2283
+ function friendlyMoveRows(body) {
2284
+ const moves = entry(body, "moves")?.value;
2285
+ if (moves?.kind !== "list")
2286
+ return [];
2287
+ return moves.items.flatMap((item) => {
2288
+ if (item.kind !== "block")
2289
+ return [];
2290
+ const from = stringSlot(item, "from");
2291
+ const operation = stringSlot(item, "operation");
2292
+ const to = stringSlot(item, "to");
2293
+ return [
2294
+ {
2295
+ ...(from ? { from } : {}),
2296
+ ...(operation ? { operation } : {}),
2297
+ ...(to ? { to } : {}),
2298
+ },
2299
+ ];
2300
+ });
2301
+ }
2302
+ function isJsonObject(value) {
2303
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2304
+ }
2305
+ function bindFieldReferences(slots, fields, action, diagnostics) {
2306
+ const externalFieldSlot = (path) => {
2307
+ const key = path.at(-1);
2308
+ return ((path.includes("requiresAggregate") && key === "refField") ||
2309
+ (path.includes("requiresAggregate") && key === "amountField") ||
2310
+ (path.includes("signedSum") && key === "refField") ||
2311
+ (path.includes("signedSum") && key === "amountField") ||
2312
+ (path.includes("remainder") && key === "refField") ||
2313
+ (path.includes("remainder") && key === "amountField") ||
2314
+ (path.includes("requiresExposure") && key === "capField") ||
2315
+ (path.includes("reconcile") &&
2316
+ path.includes("exception") &&
2317
+ ["amountField", "reasonField", "refField"].includes(key ?? "")));
2318
+ };
2319
+ const visit = (value, path = []) => {
2320
+ if (Array.isArray(value)) {
2321
+ value.forEach((item, index) => visit(item, [...path, String(index)]));
2322
+ return;
2323
+ }
2324
+ if (value && typeof value === "object") {
2325
+ Object.entries(value).forEach(([childKey, child]) => visit(child, [...path, childKey]));
2326
+ return;
2327
+ }
2328
+ const key = path.at(-1) ?? "";
2329
+ if (typeof value === "string" &&
2330
+ key.endsWith("Field") &&
2331
+ !externalFieldSlot(path) &&
2332
+ !fields.has(value)) {
2333
+ diagnostics.push({
2334
+ code: "HSX1001",
2335
+ fix: `declare field ${value} or correct ${key}`,
2336
+ message: `action ${action.name} refers to field ${value}, which does not exist`,
2337
+ severity: "error",
2338
+ span: action.span,
2339
+ });
2340
+ }
2341
+ };
2342
+ visit(slots);
2343
+ }
2344
+ function crossInstrumentReferenceDiagnostics(instruments, diagnostics) {
2345
+ const byId = new Map(instruments.map((instrument) => [instrument.id, instrument]));
2346
+ const report = (owner, span, message, fix) => {
2347
+ diagnostics.push({
2348
+ code: "HSX1007",
2349
+ fix,
2350
+ message: `${owner.id}: ${message}`,
2351
+ severity: "error",
2352
+ span,
2353
+ });
2354
+ };
2355
+ const fieldIn = (instrument, name) => instrument.fields.find((field) => field.name === name);
2356
+ const statesOf = (instrument) => {
2357
+ const lifecycle = jsonObject(instrument.slots.lifecycle);
2358
+ return new Set(Array.isArray(lifecycle?.states)
2359
+ ? lifecycle.states.filter((state) => typeof state === "string")
2360
+ : []);
2361
+ };
2362
+ for (const owner of instruments) {
2363
+ for (const field of owner.fields) {
2364
+ if (field.type.kind !== "ref" || !field.type.target)
2365
+ continue;
2366
+ if (!byId.has(field.type.target)) {
2367
+ report(owner, field.origin, `field ${field.name} has ref<${field.type.target}>, but that instrument does not exist`, `declare instrument ${field.type.target} or correct the ref target`);
2368
+ }
2369
+ }
2370
+ for (const invariant of jsonObjects(owner.slots.aggregateInvariants)) {
2371
+ const childId = invariant.childInstrumentId;
2372
+ if (typeof childId !== "string")
2373
+ continue;
2374
+ const child = byId.get(childId);
2375
+ if (!child) {
2376
+ report(owner, owner.origin, `aggregate invariant refers to instrument ${childId}, which does not exist`, `declare instrument ${childId} or correct childInstrumentId`);
2377
+ continue;
2378
+ }
2379
+ const childRefField = invariant.childRefField;
2380
+ if (typeof childRefField === "string") {
2381
+ const field = fieldIn(child, childRefField);
2382
+ if (!field ||
2383
+ field.type.kind !== "ref" ||
2384
+ (field.type.target !== undefined && field.type.target !== owner.id)) {
2385
+ report(owner, owner.origin, `${childId}.${childRefField} is not ref<${owner.id}>`, `declare ${childId}.${childRefField} as ref<${owner.id}> or correct childRefField`);
2386
+ }
2387
+ }
2388
+ const childField = invariant.childField;
2389
+ if (typeof childField === "string" &&
2390
+ fieldIn(child, childField)?.type.kind !== "money") {
2391
+ report(owner, owner.origin, `${childId}.${childField} is not declared money`, `declare ${childId}.${childField} as money<C> or correct childField`);
2392
+ }
2393
+ const childStates = statesOf(child);
2394
+ for (const status of Array.isArray(invariant.childStatuses)
2395
+ ? invariant.childStatuses
2396
+ : []) {
2397
+ if (typeof status === "string" && !childStates.has(status)) {
2398
+ report(owner, owner.origin, `${childId} has no lifecycle state ${status}`, `add ${status} to ${childId} or correct childStatuses`);
2399
+ }
2400
+ }
2401
+ const parentField = invariant.parentField;
2402
+ const parent = typeof parentField === "string"
2403
+ ? fieldIn(owner, parentField)
2404
+ : undefined;
2405
+ if (typeof parentField === "string" &&
2406
+ (!parent ||
2407
+ (invariant.count === true
2408
+ ? parent.type.kind !== "integer"
2409
+ : parent.type.kind !== "money"))) {
2410
+ const expected = invariant.count === true ? "integer" : "money<C>";
2411
+ report(owner, owner.origin, `${owner.id}.${parentField} is not declared ${expected}`, `declare ${owner.id}.${parentField} as ${expected} or correct parentField`);
2412
+ }
2413
+ }
2414
+ const checkRelation = (action, relation, targetKey, checkAmountField = true) => {
2415
+ const targetId = relation[targetKey];
2416
+ if (typeof targetId !== "string")
2417
+ return;
2418
+ const target = byId.get(targetId);
2419
+ if (!target) {
2420
+ report(owner, action.origin, `action ${action.name} refers to instrument ${targetId}, which does not exist`, `declare instrument ${targetId} or correct ${targetKey}`);
2421
+ return;
2422
+ }
2423
+ const refField = relation.refField;
2424
+ if (typeof refField === "string") {
2425
+ const field = fieldIn(target, refField);
2426
+ if (!field) {
2427
+ report(owner, action.origin, `${targetId}.${refField} does not exist`, `declare ${refField} on ${targetId} as ref<${owner.id}> or correct the clause`);
2428
+ }
2429
+ else if (field.type.kind !== "ref" ||
2430
+ (field.type.target !== undefined && field.type.target !== owner.id)) {
2431
+ report(owner, action.origin, `${targetId}.${refField} has ${typeWords(field.type)}; this relation needs ref<${owner.id}>`, `change ${targetId}.${refField} to ref<${owner.id}>`);
2432
+ }
2433
+ }
2434
+ const check = jsonObject(relation.check);
2435
+ const amountField = relation.amountField ?? check?.amountField;
2436
+ if (checkAmountField &&
2437
+ typeof amountField === "string" &&
2438
+ relation.path !== "refs") {
2439
+ const field = fieldIn(target, amountField);
2440
+ if (!field || field.type.kind !== "money") {
2441
+ report(owner, action.origin, `${targetId}.${amountField} is not declared money`, `declare ${targetId}.${amountField} as money<C> or correct the clause`);
2442
+ }
2443
+ }
2444
+ const targetField = check?.targetField;
2445
+ if (typeof targetField === "string" &&
2446
+ fieldIn(owner, targetField)?.type.kind !== "money") {
2447
+ report(owner, action.origin, `${owner.id}.${targetField} is not declared money`, `declare ${owner.id}.${targetField} as money<C> or correct the aggregate target`);
2448
+ }
2449
+ const statuses = relation.statuses;
2450
+ if (Array.isArray(statuses)) {
2451
+ const targetStates = statesOf(target);
2452
+ for (const status of statuses) {
2453
+ if (typeof status === "string" && !targetStates.has(status)) {
2454
+ report(owner, action.origin, `${targetId} has no lifecycle state ${status}`, `add ${status} to ${targetId} or correct the clause status`);
2455
+ }
2456
+ }
2457
+ }
2458
+ };
2459
+ for (const action of owner.actions) {
2460
+ for (const gate of jsonObjects(action.slots.requiresRefs)) {
2461
+ const fieldName = gate.field;
2462
+ if (typeof fieldName !== "string")
2463
+ continue;
2464
+ const field = fieldIn(owner, fieldName);
2465
+ if (!field || field.type.kind !== "ref" || !field.type.target)
2466
+ continue;
2467
+ const target = byId.get(field.type.target);
2468
+ if (!target)
2469
+ continue;
2470
+ const statuses = Array.isArray(gate.statuses) ? gate.statuses : [];
2471
+ const targetStates = statesOf(target);
2472
+ for (const status of statuses) {
2473
+ if (typeof status === "string" && !targetStates.has(status)) {
2474
+ report(owner, action.origin, `${field.type.target} has no lifecycle state ${status}`, `correct requires refs statuses for ${fieldName}`);
2475
+ }
2476
+ }
2477
+ }
2478
+ for (const relation of jsonObjects(action.slots.requiresAggregate)) {
2479
+ checkRelation(action, relation, "instrumentId");
2480
+ }
2481
+ for (const relation of jsonObjects(action.slots.requiresExposure)) {
2482
+ const childId = relation.childInstrumentId;
2483
+ if (typeof childId === "string" && childId !== owner.id) {
2484
+ report(owner, action.origin, `requires exposure names child ${childId}, but the action belongs to ${owner.id}`, `set childInstrumentId to ${owner.id}`);
2485
+ }
2486
+ const amountField = relation.amountField;
2487
+ if (typeof amountField === "string" &&
2488
+ fieldIn(owner, amountField)?.type.kind !== "money") {
2489
+ report(owner, action.origin, `${owner.id}.${amountField} is not declared money`, `declare ${amountField} as money<C> or correct requires exposure`);
2490
+ }
2491
+ const anchorField = relation.anchorField;
2492
+ const anchor = typeof anchorField === "string"
2493
+ ? fieldIn(owner, anchorField)
2494
+ : undefined;
2495
+ if (typeof anchorField === "string" &&
2496
+ (!anchor || anchor.type.kind !== "ref" || !anchor.type.target)) {
2497
+ report(owner, action.origin, `${owner.id}.${anchorField} is not a typed cross-instrument ref`, `declare ${anchorField} as ref<parent_instrument>`);
2498
+ }
2499
+ else if (anchor?.type.target) {
2500
+ const parent = byId.get(anchor.type.target);
2501
+ const capField = relation.capField;
2502
+ if (parent &&
2503
+ typeof capField === "string" &&
2504
+ fieldIn(parent, capField)?.type.kind !== "money") {
2505
+ report(owner, action.origin, `${parent.id}.${capField} is not declared money`, `declare ${parent.id}.${capField} as money<C> or correct requires exposure`);
2506
+ }
2507
+ }
2508
+ }
2509
+ const signedSum = jsonObject(action.slots.signedSum);
2510
+ for (const relation of jsonObjects(signedSum?.sources)) {
2511
+ checkRelation(action, relation, "instrumentId");
2512
+ }
2513
+ const remainder = jsonObject(action.slots.remainder);
2514
+ for (const relation of jsonObjects(remainder?.collected)) {
2515
+ checkRelation(action, relation, "instrumentId");
2516
+ }
2517
+ for (const reconcile of jsonObjects(action.slots.reconcile)) {
2518
+ const exception = jsonObject(reconcile.exception);
2519
+ if (!exception)
2520
+ continue;
2521
+ checkRelation(action, exception, "childInstrumentId", false);
2522
+ const childId = exception.childInstrumentId;
2523
+ if (typeof childId !== "string")
2524
+ continue;
2525
+ const child = byId.get(childId);
2526
+ if (!child)
2527
+ continue;
2528
+ const required = new Set(requiredFields(child));
2529
+ for (const [key, expectedKind, expectedWords] of [
2530
+ ["amountField", "money", "money<C>"],
2531
+ ["reasonField", "text", "text"],
2532
+ ]) {
2533
+ const fieldName = exception[key];
2534
+ if (typeof fieldName !== "string")
2535
+ continue;
2536
+ const field = fieldIn(child, fieldName);
2537
+ if (!field) {
2538
+ report(owner, action.origin, `reconcile exception ${key} names ${childId}.${fieldName}, which does not exist`, `declare ${childId}.${fieldName} as ${expectedWords} or correct ${key}`);
2539
+ }
2540
+ else if (field.type.kind !== expectedKind) {
2541
+ report(owner, action.origin, `reconcile exception ${key} names ${childId}.${fieldName}, which has ${typeWords(field.type)}; expected ${expectedWords}`, `change ${childId}.${fieldName} to ${expectedWords} or correct ${key}`);
2542
+ }
2543
+ else if (!field.required || !required.has(fieldName)) {
2544
+ report(owner, action.origin, `reconcile exception ${key} names ${childId}.${fieldName}, which is optional; exception fields must be required`, `add ${childId}.${fieldName} to the child required list or correct ${key}`);
2545
+ }
2546
+ else if (key === "reasonField") {
2547
+ const constraint = field.schema.pattern !== undefined
2548
+ ? "pattern"
2549
+ : field.schema.format !== undefined
2550
+ ? "format"
2551
+ : field.schema.enum !== undefined
2552
+ ? "enum"
2553
+ : field.schema.const !== undefined
2554
+ ? "const"
2555
+ : undefined;
2556
+ if (constraint) {
2557
+ const article = constraint === "enum" ? "an" : "a";
2558
+ report(owner, action.origin, `reason field ${childId}.${fieldName} carries ${article} ${constraint} constraint; reason fields must be unconstrained text`, `remove the ${constraint} constraint from ${childId}.${fieldName} or correct reasonField`);
2559
+ }
2560
+ }
2561
+ }
2562
+ }
2563
+ }
2564
+ }
2565
+ }
2566
+ function jsonObject(value) {
2567
+ return value !== undefined && isJsonObject(value) ? value : undefined;
2568
+ }
2569
+ function jsonObjects(value) {
2570
+ if (Array.isArray(value))
2571
+ return value.filter(isJsonObject);
2572
+ const object = jsonObject(value);
2573
+ return object ? [object] : [];
2574
+ }
2575
+ function linearityDiagnostics(actionEntries, diagnostics) {
2576
+ const clauses = actionEntries.flatMap((action) => {
2577
+ if (action.value.kind !== "block")
2578
+ return [];
2579
+ return action.value.entries.flatMap((row) => {
2580
+ const spelling = clauseSpelling(row, actionClauseBySpelling);
2581
+ const definition = actionClauseBySpelling.get(spelling);
2582
+ return definition?.scope === "action"
2583
+ ? [{ definition, row, spelling }]
2584
+ : [];
2585
+ });
2586
+ });
2587
+ const sinks = clauses.filter(({ definition }) => "linearSink" in definition && definition.linearSink === true);
2588
+ for (const producer of clauses) {
2589
+ const names = linearOutputNames(producer.row, producer.definition);
2590
+ for (const name of names) {
2591
+ const uses = sinks.reduce((count, sink) => count + countIdentifier(sink.row.value, name), 0);
2592
+ if (uses === 0) {
2593
+ diagnostics.push({
2594
+ code: "HSX1202",
2595
+ fix: `route ${name} with moves, payout, distribution, a fee assignment, or an explicit remainder sink`,
2596
+ message: `money ${name} produced by ${producer.spelling} is never consumed`,
2597
+ severity: "error",
2598
+ span: producer.row.span,
2599
+ });
2600
+ }
2601
+ else if (uses > 1) {
2602
+ diagnostics.push({
2603
+ code: "HSX1201",
2604
+ fix: `leave exactly one sink for ${name}`,
2605
+ message: `money ${name} is consumed ${uses} times; each money value must be consumed exactly once`,
2606
+ severity: "error",
2607
+ span: producer.row.span,
2608
+ });
2609
+ }
2610
+ }
2611
+ }
2612
+ }
2613
+ function linearOutputNames(row, definition) {
2614
+ if (!("linearOutputs" in definition) || !definition.linearOutputs)
2615
+ return [];
2616
+ const spellingQualifierCount = definition.spelling.split(" ").length - 1;
2617
+ const explicitBinding = row.qualifiers.slice(spellingQualifierCount).at(-1);
2618
+ if (explicitBinding)
2619
+ return [explicitBinding.name];
2620
+ return definition.linearOutputs
2621
+ .flatMap((path) => {
2622
+ const value = exprAtPath(row.value, path.split("."));
2623
+ if (value?.kind === "ident")
2624
+ return [value.name];
2625
+ if (value?.kind === "string")
2626
+ return [value.value];
2627
+ if (value?.kind === "path")
2628
+ return [value.parts.at(-1)?.name ?? ""];
2629
+ return [];
2630
+ })
2631
+ .filter((name) => name.length > 0);
2632
+ }
2633
+ function exprAtPath(expr, path) {
2634
+ if (path.length === 0)
2635
+ return expr;
2636
+ const block = expr.kind === "decided_amount" ? expr.body : expr;
2637
+ if (block.kind !== "block")
2638
+ return undefined;
2639
+ const [head, ...tail] = path;
2640
+ const row = block.entries.find((candidate) => camel(candidate.key.name) === camel(head ?? ""));
2641
+ return row ? exprAtPath(row.value, tail) : undefined;
2642
+ }
2643
+ function countIdentifier(expr, name) {
2644
+ if (expr.kind === "ident")
2645
+ return expr.name === name ? 1 : 0;
2646
+ if (expr.kind === "string")
2647
+ return expr.value === name || expr.value.endsWith(`.${name}`) ? 1 : 0;
2648
+ if (expr.kind === "path")
2649
+ return expr.parts.at(-1)?.name === name ? 1 : 0;
2650
+ if (expr.kind === "settlement_ref")
2651
+ return expr.member.name === name ? 1 : 0;
2652
+ if (expr.kind === "list")
2653
+ return expr.items.reduce((sum, item) => sum + countIdentifier(item, name), 0);
2654
+ if (expr.kind === "block")
2655
+ return expr.entries.reduce((sum, row) => sum + countIdentifier(row.value, name), 0);
2656
+ if (expr.kind === "call" || expr.kind === "type_apply") {
2657
+ return expr.args.reduce((sum, item) => sum + countIdentifier(item, name), 0);
2658
+ }
2659
+ if (expr.kind === "apply") {
2660
+ return [...expr.args, ...expr.typeArgs].reduce((sum, item) => sum + countIdentifier(item, name), 0);
2661
+ }
2662
+ if (expr.kind === "binding")
2663
+ return countIdentifier(expr.type, name);
2664
+ if (expr.kind === "decided_amount")
2665
+ return countIdentifier(expr.body, name);
2666
+ return 0;
2667
+ }
2668
+ function clauseSpelling(row, vocabulary) {
2669
+ const key = row.key.name.replaceAll("_", " ");
2670
+ const first = row.qualifiers[0]?.name;
2671
+ if (!first)
2672
+ return key;
2673
+ const candidates = [
2674
+ `${key} ${first}`,
2675
+ `${key} ${first.replaceAll("_", " ")}`,
2676
+ ];
2677
+ return candidates.find((candidate) => vocabulary.has(candidate)) ?? key;
2678
+ }
2679
+ function setClause(target, path, cardinality, value, spelling, span, diagnostics) {
2680
+ const [head, tail] = path.split(".");
2681
+ if (!head)
2682
+ return;
2683
+ if (!tail) {
2684
+ const resolved = clauseValue(target[head], cardinality, value, spelling, span, diagnostics);
2685
+ if (resolved !== undefined)
2686
+ target[head] = resolved;
2687
+ return;
2688
+ }
2689
+ const nested = target[head] &&
2690
+ typeof target[head] === "object" &&
2691
+ !Array.isArray(target[head])
2692
+ ? { ...target[head] }
2693
+ : {};
2694
+ const resolved = clauseValue(nested[tail], cardinality, value, spelling, span, diagnostics);
2695
+ if (resolved !== undefined)
2696
+ nested[tail] = resolved;
2697
+ target[head] = nested;
2698
+ }
2699
+ function clauseValue(current, cardinality, value, spelling, span, diagnostics) {
2700
+ if (cardinality === "many") {
2701
+ const next = Array.isArray(value) ? value : [value];
2702
+ return Array.isArray(current) ? [...current, ...next] : next;
2703
+ }
2704
+ if (current === undefined)
2705
+ return value;
2706
+ diagnostics.push({
2707
+ code: "HSX1505",
2708
+ fix: `keep one ${spelling} clause`,
2709
+ message: `${spelling} is a single-valued UDL clause and appears more than once`,
2710
+ severity: "error",
2711
+ span,
2712
+ });
2713
+ return undefined;
2714
+ }
2715
+ function entry(block, ...names) {
2716
+ return block.entries.find((candidate) => names.includes(candidate.key.name));
2717
+ }
2718
+ function stringSlot(block, ...names) {
2719
+ const found = entry(block, ...names)?.value;
2720
+ if (found?.kind === "string")
2721
+ return found.value;
2722
+ if (found?.kind === "ident")
2723
+ return found.name;
2724
+ return undefined;
2725
+ }
2726
+ function blockToObject(block, substitutions, diagnostics) {
2727
+ if (!block)
2728
+ return {};
2729
+ const result = {};
2730
+ for (const row of block.entries) {
2731
+ const value = substituteExpr(row.value, substitutions);
2732
+ if (twoArgumentMoneyCall(value)) {
2733
+ diagnostics.push({
2734
+ code: "HSX1104",
2735
+ fix: "bind money(CUR, amount) to a declared field whose UDL schema can carry const",
2736
+ message: `UDL slot ${camel(row.key.name)} cannot carry a fixed money binding`,
2737
+ severity: "error",
2738
+ span: row.value.span,
2739
+ });
2740
+ continue;
2741
+ }
2742
+ result[row.key.quoted ? row.key.name : camel(row.key.name)] = exprToJson(value, diagnostics);
2743
+ }
2744
+ return result;
2745
+ }
2746
+ function exprToJsonForUdlSlot(expr, slot, diagnostics) {
2747
+ if (twoArgumentMoneyCall(expr)) {
2748
+ diagnostics.push({
2749
+ code: "HSX1104",
2750
+ fix: "bind money(CUR, amount) to a declared field whose UDL schema can carry const",
2751
+ message: `UDL slot ${slot} cannot carry a fixed money binding`,
2752
+ severity: "error",
2753
+ span: expr.span,
2754
+ });
2755
+ }
2756
+ if (slot === "callerParkedStates" && expr.kind === "block") {
2757
+ return Object.fromEntries(expr.entries.map((row) => [
2758
+ row.key.name,
2759
+ exprToJson(row.value, diagnostics),
2760
+ ]));
2761
+ }
2762
+ return exprToJson(expr, diagnostics);
2763
+ }
2764
+ function exprToJson(expr, diagnostics) {
2765
+ switch (expr.kind) {
2766
+ case "boolean":
2767
+ return expr.value;
2768
+ case "string":
2769
+ return expr.value;
2770
+ case "number": {
2771
+ if (expr.raw.includes(".")) {
2772
+ diagnostics.push({
2773
+ code: "HSX1103",
2774
+ fix: "use an integer, percent, bps, or currency-indexed money literal",
2775
+ message: `${expr.raw} is a free decimal; HSX has no float type`,
2776
+ severity: "error",
2777
+ span: expr.span,
2778
+ });
2779
+ return expr.raw;
2780
+ }
2781
+ return Number(expr.raw);
2782
+ }
2783
+ case "percent":
2784
+ return expr.bps;
2785
+ case "money": {
2786
+ const precision = minorUnits[expr.currency.name] ?? 2;
2787
+ const [whole = "0", fraction = ""] = expr.raw.split(".");
2788
+ if (fraction.length > precision) {
2789
+ diagnostics.push({
2790
+ code: "HSX1102",
2791
+ fix: `round the literal to ${precision} minor-unit digits`,
2792
+ message: `${expr.currency.name} ${expr.raw} is finer than ${expr.currency.name} minor units`,
2793
+ severity: "error",
2794
+ span: expr.span,
2795
+ });
2796
+ }
2797
+ return `${whole}${fraction.padEnd(precision, "0")}`.replace(/^0+(?=\d)/, "");
2798
+ }
2799
+ case "ident":
2800
+ return expr.name;
2801
+ case "path":
2802
+ return expr.parts.map((part) => part.name).join(".");
2803
+ case "settlement_ref":
2804
+ return `${expr.owner.name}.${expr.member.name}`;
2805
+ case "list":
2806
+ return expr.items.map((item) => exprToJson(item, diagnostics));
2807
+ case "block":
2808
+ return blockToObject(expr, new Map(), diagnostics);
2809
+ case "call":
2810
+ return `${expr.callee.name}(${expr.args.map((arg) => String(exprToJson(arg, diagnostics))).join(",")})`;
2811
+ case "type_apply":
2812
+ return `${expr.callee.name}<${expr.args.map((arg) => String(exprToJson(arg, diagnostics))).join(",")}>`;
2813
+ case "binding":
2814
+ return exprToJson(expr.type, diagnostics);
2815
+ case "decided_amount":
2816
+ return blockToObject(expr.body, new Map(), diagnostics);
2817
+ case "port_ref":
2818
+ return expr.name.name;
2819
+ case "apply":
2820
+ return `${expr.callee.parts.map((part) => part.name).join(".")}()`;
2821
+ }
2822
+ }
2823
+ function typeOf(expr, aliases = new Map(), seen = new Set()) {
2824
+ if (expr.kind === "binding")
2825
+ return typeOf(expr.type, aliases, seen);
2826
+ if (expr.kind === "money") {
2827
+ return { currency: expr.currency.name, kind: "money" };
2828
+ }
2829
+ if (expr.kind === "type_apply" || expr.kind === "call") {
2830
+ const kind = expr.callee.name;
2831
+ const argument = expr.args[0];
2832
+ const name = argument?.kind === "ident"
2833
+ ? argument.name
2834
+ : argument?.kind === "string"
2835
+ ? argument.value
2836
+ : undefined;
2837
+ if (kind === "money") {
2838
+ const amount = expr.args[1];
2839
+ const fixedAmount = expr.args.length === 2 &&
2840
+ amount?.kind === "number" &&
2841
+ /^[1-9][0-9]{0,17}$/.test(amount.raw) &&
2842
+ Number.isSafeInteger(Number(amount.raw))
2843
+ ? amount.raw
2844
+ : undefined;
2845
+ return {
2846
+ ...(name ? { currency: name } : {}),
2847
+ ...(fixedAmount ? { fixedAmount } : {}),
2848
+ kind: "money",
2849
+ };
2850
+ }
2851
+ if (kind === "account")
2852
+ return { ...(name ? { currency: name } : {}), kind: "account" };
2853
+ if (kind === "ref" || kind === "id")
2854
+ return { kind: "ref", ...(name ? { target: name } : {}) };
2855
+ }
2856
+ if (expr.kind === "ident") {
2857
+ const alias = aliases.get(expr.name);
2858
+ if (alias && !seen.has(expr.name)) {
2859
+ return typeOf(alias, aliases, new Set([...seen, expr.name]));
2860
+ }
2861
+ if (CURRENCY.test(expr.name))
2862
+ return { currency: expr.name, kind: "unknown" };
2863
+ if ([
2864
+ "boolean",
2865
+ "account",
2866
+ "bps",
2867
+ "condition",
2868
+ "date",
2869
+ "integer",
2870
+ "party",
2871
+ "percent",
2872
+ "ref",
2873
+ "text",
2874
+ ].includes(expr.name))
2875
+ return { kind: expr.name };
2876
+ }
2877
+ if (expr.kind === "percent")
2878
+ return { kind: "percent" };
2879
+ if (expr.kind === "boolean")
2880
+ return { kind: "boolean" };
2881
+ if (expr.kind === "number" && !expr.raw.includes("."))
2882
+ return { kind: "integer" };
2883
+ return { kind: "unknown" };
2884
+ }
2885
+ function schemaFor(type, required) {
2886
+ switch (type.kind) {
2887
+ case "money":
2888
+ return type.fixedAmount
2889
+ ? {
2890
+ const: type.fixedAmount,
2891
+ pattern: MONEY_PATTERN,
2892
+ type: "string",
2893
+ }
2894
+ : {
2895
+ pattern: required ? MONEY_PATTERN : OPTIONAL_MONEY_PATTERN,
2896
+ type: "string",
2897
+ };
2898
+ case "bps":
2899
+ case "percent":
2900
+ return { maximum: 10_000, minimum: 0, type: "integer" };
2901
+ case "integer":
2902
+ return { type: "integer" };
2903
+ case "boolean":
2904
+ return { type: "boolean" };
2905
+ case "date":
2906
+ return { format: "hyperscale-date-time", type: "string" };
2907
+ case "ref":
2908
+ return { type: "string" };
2909
+ default:
2910
+ return { type: "string" };
2911
+ }
2912
+ }
2913
+ function invalidFixedMoneyBinding(expr) {
2914
+ const candidate = twoArgumentMoneyCall(expr);
2915
+ if (!candidate)
2916
+ return undefined;
2917
+ const amount = candidate.args[1];
2918
+ if (candidate.args.length === 2 &&
2919
+ amount?.kind === "number" &&
2920
+ /^[1-9][0-9]{0,17}$/.test(amount.raw) &&
2921
+ Number.isSafeInteger(Number(amount.raw))) {
2922
+ return undefined;
2923
+ }
2924
+ return "fixes money to an invalid amount; this UDL field slot accepts one fixed minor-unit integer";
2925
+ }
2926
+ function twoArgumentMoneyCall(expr) {
2927
+ const candidate = expr.kind === "binding" ? expr.type : expr;
2928
+ return candidate.kind === "call" &&
2929
+ candidate.callee.name === "money" &&
2930
+ candidate.args.length >= 2
2931
+ ? candidate
2932
+ : undefined;
2933
+ }
2934
+ function typeWords(type) {
2935
+ return type.currency ? `${type.kind}<${type.currency}>` : type.kind;
2936
+ }
2937
+ function camel(value) {
2938
+ return value.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase());
2939
+ }
2940
+ function prefixFor(value) {
2941
+ const parts = value.split("_");
2942
+ return parts.length > 1
2943
+ ? parts
2944
+ .map((part) => part[0] ?? "")
2945
+ .join("")
2946
+ .slice(0, 8)
2947
+ : value.slice(0, 4);
2948
+ }
2949
+ function titleize(value) {
2950
+ return value
2951
+ .replaceAll("_", " ")
2952
+ .replace(/\b[a-z]/g, (letter) => letter.toUpperCase());
2953
+ }
2954
+ function sentenceCase(value) {
2955
+ const words = value.replaceAll("_", " ");
2956
+ return `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}`;
2957
+ }
2958
+ //# sourceMappingURL=typecheck.js.map