@hyperscale0/hsx 2.4.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +4 -406
  2. package/README.md +3 -92
  3. package/dist/src/ast.d.ts +99 -310
  4. package/dist/src/ast.d.ts.map +1 -1
  5. package/dist/src/ast.js +16 -69
  6. package/dist/src/ast.js.map +1 -1
  7. package/dist/src/cli.d.ts +6 -19
  8. package/dist/src/cli.d.ts.map +1 -1
  9. package/dist/src/cli.js +77 -315
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/compile.d.ts +23 -73
  12. package/dist/src/compile.d.ts.map +1 -1
  13. package/dist/src/compile.js +1113 -148
  14. package/dist/src/compile.js.map +1 -1
  15. package/dist/src/cost.d.ts +8 -77
  16. package/dist/src/cost.d.ts.map +1 -1
  17. package/dist/src/cost.js +13 -422
  18. package/dist/src/cost.js.map +1 -1
  19. package/dist/src/format.d.ts +8 -7
  20. package/dist/src/format.d.ts.map +1 -1
  21. package/dist/src/format.js +9 -175
  22. package/dist/src/format.js.map +1 -1
  23. package/dist/src/headers.d.ts +35 -0
  24. package/dist/src/headers.d.ts.map +1 -0
  25. package/dist/src/headers.js +108 -0
  26. package/dist/src/headers.js.map +1 -0
  27. package/dist/src/index.d.ts +6 -12
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +4 -9
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/lex.d.ts +8 -28
  32. package/dist/src/lex.d.ts.map +1 -1
  33. package/dist/src/lex.js +88 -146
  34. package/dist/src/lex.js.map +1 -1
  35. package/dist/src/parse.d.ts +4 -17
  36. package/dist/src/parse.d.ts.map +1 -1
  37. package/dist/src/parse.js +402 -1040
  38. package/dist/src/parse.js.map +1 -1
  39. package/dist/src/std-bundle.d.ts.map +1 -1
  40. package/dist/src/std-bundle.js +12 -20
  41. package/dist/src/std-bundle.js.map +1 -1
  42. package/dist/src/std-library.d.ts +1 -1
  43. package/dist/src/std-library.d.ts.map +1 -1
  44. package/dist/src/std-library.js +1 -5
  45. package/dist/src/std-library.js.map +1 -1
  46. package/dist/src/tunables.d.ts +7 -0
  47. package/dist/src/tunables.d.ts.map +1 -0
  48. package/dist/src/tunables.js +26 -0
  49. package/dist/src/tunables.js.map +1 -0
  50. package/dist/src/version.d.ts +2 -4
  51. package/dist/src/version.d.ts.map +1 -1
  52. package/dist/src/version.js +2 -4
  53. package/dist/src/version.js.map +1 -1
  54. package/docs/README.md +179 -41
  55. package/docs/headers.md +44 -0
  56. package/examples/cost-table.json +99 -751
  57. package/examples/library.hsx +59 -0
  58. package/package.json +9 -17
  59. package/src/ast.ts +82 -441
  60. package/src/cli.ts +78 -362
  61. package/src/compile.ts +1535 -247
  62. package/src/cost.ts +28 -671
  63. package/src/format.ts +12 -256
  64. package/src/headers.ts +126 -0
  65. package/src/index.ts +7 -39
  66. package/src/lex.ts +99 -195
  67. package/src/parse.ts +403 -1209
  68. package/src/std-bundle.ts +12 -20
  69. package/src/std-library.ts +2 -7
  70. package/src/tunables.ts +31 -0
  71. package/src/version.ts +2 -5
  72. package/std/approvals.hsx +17 -0
  73. package/std/cards.hsx +63 -0
  74. package/std/collections.hsx +31 -0
  75. package/std/escrow.hsx +43 -0
  76. package/std/financing.hsx +372 -0
  77. package/std/insurance.hsx +59 -0
  78. package/std/lending.hsx +115 -0
  79. package/std/marketplace.hsx +24 -0
  80. package/std/money.hsx +195 -0
  81. package/std/savings.hsx +44 -0
  82. package/std/travel.hsx +98 -0
  83. package/std/wallet.hsx +41 -0
  84. package/dist/src/diagnostics.d.ts +0 -13
  85. package/dist/src/diagnostics.d.ts.map +0 -1
  86. package/dist/src/diagnostics.js +0 -587
  87. package/dist/src/diagnostics.js.map +0 -1
  88. package/dist/src/emit.d.ts +0 -51
  89. package/dist/src/emit.d.ts.map +0 -1
  90. package/dist/src/emit.js +0 -192
  91. package/dist/src/emit.js.map +0 -1
  92. package/dist/src/entry-overrides.d.ts +0 -58
  93. package/dist/src/entry-overrides.d.ts.map +0 -1
  94. package/dist/src/entry-overrides.js +0 -284
  95. package/dist/src/entry-overrides.js.map +0 -1
  96. package/dist/src/ir.d.ts +0 -73
  97. package/dist/src/ir.d.ts.map +0 -1
  98. package/dist/src/ir.js +0 -15
  99. package/dist/src/ir.js.map +0 -1
  100. package/dist/src/limits.d.ts +0 -23
  101. package/dist/src/limits.d.ts.map +0 -1
  102. package/dist/src/limits.js +0 -23
  103. package/dist/src/limits.js.map +0 -1
  104. package/dist/src/lsp/server.d.ts +0 -32
  105. package/dist/src/lsp/server.d.ts.map +0 -1
  106. package/dist/src/lsp/server.js +0 -391
  107. package/dist/src/lsp/server.js.map +0 -1
  108. package/dist/src/modules.d.ts +0 -38
  109. package/dist/src/modules.d.ts.map +0 -1
  110. package/dist/src/modules.js +0 -372
  111. package/dist/src/modules.js.map +0 -1
  112. package/dist/src/typecheck.d.ts +0 -8
  113. package/dist/src/typecheck.d.ts.map +0 -1
  114. package/dist/src/typecheck.js +0 -4160
  115. package/dist/src/typecheck.js.map +0 -1
  116. package/docs/guide/01-first-program.md +0 -24
  117. package/docs/guide/02-money.md +0 -25
  118. package/docs/guide/03-instruments.md +0 -93
  119. package/docs/guide/04-lifecycles.md +0 -40
  120. package/docs/guide/05-fees-and-splits.md +0 -45
  121. package/docs/guide/06-schedules.md +0 -26
  122. package/docs/guide/07-composition.md +0 -52
  123. package/docs/guide/08-writing-a-module.md +0 -85
  124. package/docs/guide/09-cost.md +0 -24
  125. package/docs/guide/10-diagnostics.md +0 -27
  126. package/docs/llms-full.txt +0 -3684
  127. package/docs/llms.txt +0 -44
  128. package/docs/piece-plans.md +0 -120
  129. package/docs/playground.md +0 -55
  130. package/docs/reference/cli.md +0 -36
  131. package/docs/reference/diagnostics.md +0 -901
  132. package/docs/reference/grammar.md +0 -38
  133. package/docs/reference/std/advance.md +0 -112
  134. package/docs/reference/std/cancellable_booking.md +0 -159
  135. package/docs/reference/std/captured_payment.md +0 -115
  136. package/docs/reference/std/conditional_disbursement.md +0 -101
  137. package/docs/reference/std/credit_facility.md +0 -108
  138. package/docs/reference/std/held_payment.md +0 -173
  139. package/docs/reference/std/instant_transfer.md +0 -97
  140. package/docs/reference/std/metered.md +0 -89
  141. package/docs/reference/std/pooled_split.md +0 -95
  142. package/docs/reference/std/premium_forward.md +0 -119
  143. package/docs/reference/std/reconciled_payout.md +0 -103
  144. package/docs/reference/std/rotating_pool.md +0 -130
  145. package/docs/reference/std/scheduled.md +0 -136
  146. package/docs/reference/std/security_deposit.md +0 -138
  147. package/docs/reference/std/settlement_batch.md +0 -123
  148. package/docs/reference/std/swap.md +0 -133
  149. package/docs/reference/std/threshold_pool.md +0 -138
  150. package/docs/reference/std/weighted_distribution.md +0 -135
  151. package/docs/reference/types.md +0 -61
  152. package/docs/reference/udl-output.md +0 -13
  153. package/examples/01-first-program/README.md +0 -45
  154. package/examples/01-first-program/tip-jar.hsx +0 -17
  155. package/examples/02-imports-and-modules/README.md +0 -96
  156. package/examples/02-imports-and-modules/photo-booth.hsx +0 -37
  157. package/examples/03-diagnostics/README.md +0 -68
  158. package/examples/03-diagnostics/corner-shop-fixed.hsx +0 -22
  159. package/examples/03-diagnostics/corner-shop.hsx +0 -19
  160. package/examples/04-complete-product/README.md +0 -84
  161. package/examples/04-complete-product/study-hall.hsx +0 -67
  162. package/examples/05-authored-instrument/README.md +0 -5
  163. package/examples/05-authored-instrument/payment.hsx +0 -37
  164. package/examples/05-watch-club/README.md +0 -6
  165. package/examples/05-watch-club/watch-club.hsx +0 -123
  166. package/examples/README.md +0 -48
  167. package/examples/advance/README.md +0 -3
  168. package/examples/advance/advance.hsx +0 -13
  169. package/examples/advance/advance.udl +0 -367
  170. package/examples/cancellable_booking/README.md +0 -3
  171. package/examples/cancellable_booking/cancellable_booking.hsx +0 -14
  172. package/examples/cancellable_booking/cancellable_booking.udl +0 -413
  173. package/examples/captured_payment/README.md +0 -3
  174. package/examples/captured_payment/captured_payment.hsx +0 -17
  175. package/examples/captured_payment/captured_payment.udl +0 -686
  176. package/examples/conditional_disbursement/README.md +0 -3
  177. package/examples/conditional_disbursement/conditional_disbursement.hsx +0 -15
  178. package/examples/conditional_disbursement/conditional_disbursement.udl +0 -418
  179. package/examples/credit_facility/README.md +0 -3
  180. package/examples/credit_facility/credit_facility.hsx +0 -24
  181. package/examples/credit_facility/credit_facility.udl +0 -1396
  182. package/examples/held_payment/README.md +0 -3
  183. package/examples/held_payment/held_payment.hsx +0 -13
  184. package/examples/held_payment/held_payment.udl +0 -569
  185. package/examples/instant_transfer/README.md +0 -3
  186. package/examples/instant_transfer/instant_transfer.hsx +0 -16
  187. package/examples/instant_transfer/instant_transfer.udl +0 -376
  188. package/examples/metered/README.md +0 -3
  189. package/examples/metered/metered.hsx +0 -13
  190. package/examples/metered/metered.udl +0 -253
  191. package/examples/pooled_split/README.md +0 -3
  192. package/examples/pooled_split/pooled_split.hsx +0 -15
  193. package/examples/pooled_split/pooled_split.udl +0 -432
  194. package/examples/premium_forward/README.md +0 -3
  195. package/examples/premium_forward/premium_forward.hsx +0 -19
  196. package/examples/premium_forward/premium_forward.udl +0 -604
  197. package/examples/reconciled_payout/README.md +0 -3
  198. package/examples/reconciled_payout/reconciled_payout.hsx +0 -13
  199. package/examples/reconciled_payout/reconciled_payout.udl +0 -312
  200. package/examples/rotating_pool/README.md +0 -3
  201. package/examples/rotating_pool/rotating_pool.hsx +0 -18
  202. package/examples/rotating_pool/rotating_pool.udl +0 -4370
  203. package/examples/scheduled/README.md +0 -3
  204. package/examples/scheduled/scheduled.hsx +0 -12
  205. package/examples/scheduled/scheduled.udl +0 -355
  206. package/examples/security_deposit/README.md +0 -3
  207. package/examples/security_deposit/security_deposit.hsx +0 -21
  208. package/examples/security_deposit/security_deposit.udl +0 -320
  209. package/examples/settlement_batch/README.md +0 -3
  210. package/examples/settlement_batch/settlement_batch.hsx +0 -19
  211. package/examples/settlement_batch/settlement_batch.udl +0 -757
  212. package/examples/swap/README.md +0 -3
  213. package/examples/swap/swap.hsx +0 -19
  214. package/examples/swap/swap.udl +0 -892
  215. package/examples/threshold_pool/README.md +0 -3
  216. package/examples/threshold_pool/threshold_pool.hsx +0 -16
  217. package/examples/threshold_pool/threshold_pool.udl +0 -680
  218. package/examples/weighted_distribution/README.md +0 -3
  219. package/examples/weighted_distribution/weighted_distribution.hsx +0 -20
  220. package/examples/weighted_distribution/weighted_distribution.udl +0 -348
  221. package/skills/hsx/SKILL.md +0 -449
  222. package/src/diagnostics.ts +0 -620
  223. package/src/emit.ts +0 -282
  224. package/src/entry-overrides.ts +0 -428
  225. package/src/ir.ts +0 -113
  226. package/src/limits.ts +0 -22
  227. package/src/lsp/server.ts +0 -460
  228. package/src/modules.ts +0 -452
  229. package/src/typecheck.ts +0 -5515
  230. package/std/SEMANTICS.md +0 -44
  231. package/std/money_flows/advance.hsx +0 -235
  232. package/std/money_flows/cancellable_booking.hsx +0 -402
  233. package/std/money_flows/captured_payment.hsx +0 -220
  234. package/std/money_flows/conditional_disbursement.hsx +0 -226
  235. package/std/money_flows/credit_facility.hsx +0 -214
  236. package/std/money_flows/held_payment.hsx +0 -812
  237. package/std/money_flows/index.hsx +0 -4
  238. package/std/money_flows/instant_transfer.hsx +0 -231
  239. package/std/money_flows/metered.hsx +0 -104
  240. package/std/money_flows/pooled_split.hsx +0 -109
  241. package/std/money_flows/premium_forward.hsx +0 -307
  242. package/std/money_flows/reconciled_payout.hsx +0 -119
  243. package/std/money_flows/rotating_pool.hsx +0 -305
  244. package/std/money_flows/scheduled.hsx +0 -599
  245. package/std/money_flows/security_deposit.hsx +0 -248
  246. package/std/money_flows/settlement_batch.hsx +0 -173
  247. package/std/money_flows/swap.hsx +0 -1192
  248. package/std/money_flows/threshold_pool.hsx +0 -307
  249. package/std/money_flows/weighted_distribution.hsx +0 -237
@@ -1,4160 +0,0 @@
1
- import { fixedIsoDurationMs, referencedUdlInstrumentIds, deriveUdlActionEffects, resolveUdlActionPlans, udlDocumentSchema, udlClauseVocabulary, } from "@hyperscale0/udl";
2
- import { requiredFields, generalInstrumentValue, hasActionPlanClauses, lowerGeneralProgram, } from "./emit.js";
3
- import { hsxClauseDiagnosticCode } from "./diagnostics.js";
4
- import { HSX_LIMITS } from "./limits.js";
5
- const SNAKE_CASE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
6
- const CAMEL_CASE = /^[a-z][A-Za-z0-9]*$/;
7
- const CURRENCY = /^[A-Z]{3}$/;
8
- const MONEY_PATTERN = "^[1-9][0-9]{0,17}$";
9
- // Owner ruling 2026-09-16: amounts stay strictly positive unless the author
10
- // marks the money field with `allow_zero: true`; `optional` no longer implies zero.
11
- const ZERO_MONEY_PATTERN = "^(0|[1-9][0-9]{0,17})$";
12
- const ACCOUNT_PATTERN = "^acct_(sandbox|live)_[a-z0-9]{8,64}$";
13
- const NONE_SENTINEL = "__hsx_none__";
14
- const actionClauseBySpelling = new Map(udlClauseVocabulary
15
- .filter((definition) => definition.scope === "action")
16
- .map((definition) => [definition.spelling, definition]));
17
- const instrumentClauseBySpelling = new Map(udlClauseVocabulary
18
- .filter((definition) => definition.scope === "instrument")
19
- .map((definition) => [definition.spelling, definition]));
20
- const minorUnits = {
21
- BHD: 3,
22
- JPY: 0,
23
- KWD: 3,
24
- SAR: 2,
25
- USD: 2,
26
- };
27
- export function checkGeneralProgram(program, options = {}) {
28
- const diagnostics = [];
29
- const report = (code, span, message, fix, severity = "error") => {
30
- diagnostics.push({ code, fix, message, severity, span });
31
- };
32
- const headers = program.decls.filter((decl) => decl.kind === "program");
33
- const header = headers[0];
34
- const module = program.decls.find((decl) => decl.kind === "module");
35
- if (!header && !module) {
36
- report("HSX1000", program.span, "this file needs one program header or one module declaration", 'add `program product_name "Product title"`');
37
- }
38
- for (const extra of headers.slice(1)) {
39
- report("HSX1002", extra.span, "a file declares exactly one program", "delete the extra program declaration");
40
- }
41
- if (header && !SNAKE_CASE.test(header.name.name)) {
42
- report("HSX1003", header.name.span, `program ${header.name.name} is not snake_case`, "rename the program with lowercase words joined by underscores");
43
- }
44
- const parties = new Set(program.decls
45
- .filter((decl) => decl.kind === "party")
46
- .map((decl) => decl.name.name));
47
- const ports = new Map(program.decls
48
- .filter((decl) => decl.kind === "port")
49
- .map((decl) => [decl.name.name, decl]));
50
- const checkedPorts = new Set();
51
- const checkPortDeclarations = (declared, knownParties) => {
52
- for (const port of declared.values()) {
53
- if (checkedPorts.has(port))
54
- continue;
55
- checkedPorts.add(port);
56
- const allowed = entry(port.body, "allowed")?.value;
57
- for (const item of allowed?.kind === "list"
58
- ? allowed.items
59
- : allowed
60
- ? [allowed]
61
- : []) {
62
- const party = nameText(item);
63
- if (knownParties.has(party))
64
- continue;
65
- report("HSX1024", item.span, `decision port ${port.name.name} allows undeclared party ${party}; declare the party before using it`, `declare party ${party} with its party kind before using it in this port`);
66
- }
67
- }
68
- };
69
- checkPortDeclarations(ports, parties);
70
- const assetSubjects = program.decls
71
- .filter((decl) => decl.kind === "asset")
72
- .map((decl) => ({
73
- declaredValue: "optional",
74
- kind: decl.name.name,
75
- origin: decl.span,
76
- schema: { additionalProperties: false, properties: {}, type: "object" },
77
- title: sentenceCase(decl.name.name),
78
- version: 1,
79
- }));
80
- const subjects = [...assetSubjects];
81
- const subjectKinds = new Set(assetSubjects.map((subject) => subject.kind));
82
- for (const decl of program.decls.filter((candidate) => candidate.kind === "subject")) {
83
- if (subjectKinds.has(decl.name.name)) {
84
- report("HSX1008", decl.name.span, `subject ${decl.name.name} is declared twice`, "keep one subject declaration or rename one kind");
85
- continue;
86
- }
87
- const title = stringSlot(decl.body, "title");
88
- const declaredValue = stringSlot(decl.body, "declaredValue", "declared_value");
89
- const versionRow = entry(decl.body, "version");
90
- const version = versionRow?.value.kind === "number" && !versionRow.value.raw.includes(".")
91
- ? Number(versionRow.value.raw)
92
- : Number.NaN;
93
- const schemaRow = entry(decl.body, "schema");
94
- const schema = schemaRow?.value.kind === "block"
95
- ? blockToObject(schemaRow.value, new Map(), diagnostics)
96
- : undefined;
97
- if (!title ||
98
- !["none", "optional", "required"].includes(declaredValue ?? "") ||
99
- !Number.isSafeInteger(version) ||
100
- version < 1 ||
101
- !schema) {
102
- report("HSX1008", decl.span, `subject ${decl.name.name} needs title, positive integer version, declared_value, and schema`, "declare the complete UDL subject-kind document");
103
- continue;
104
- }
105
- subjectKinds.add(decl.name.name);
106
- subjects.push({
107
- declaredValue: declaredValue,
108
- kind: decl.name.name,
109
- origin: decl.span,
110
- schema,
111
- title,
112
- version,
113
- });
114
- }
115
- const compositionDeclarations = program.decls.filter((declaration) => declaration.kind === "use" || declaration.kind === "expose");
116
- if (!options.publishedCatalog) {
117
- for (const declaration of compositionDeclarations) {
118
- report("HSX1018", declaration.span, "this compiler host did not supply a published instrument catalog", "pass the canonical UDL catalog as the publishedCatalog compile option");
119
- }
120
- }
121
- const catalogById = options.publishedCatalog
122
- ? new Map(options.publishedCatalog.instruments.map((instrument) => [
123
- instrument.id,
124
- instrument,
125
- ]))
126
- : undefined;
127
- const aliases = new Map(program.decls
128
- .filter((decl) => decl.kind === "type")
129
- .map((decl) => [decl.name.name, decl.value]));
130
- checkConstants(program.decls, aliases, diagnostics);
131
- const templates = new Map();
132
- for (const decl of program.decls) {
133
- if (decl.kind !== "instrument")
134
- continue;
135
- const prior = templates.get(decl.name.name);
136
- if (prior) {
137
- report("HSX1004", decl.name.span, `instrument ${decl.name.name} is declared twice`, "keep one declaration or rename one instrument");
138
- }
139
- else
140
- templates.set(decl.name.name, decl);
141
- }
142
- const prefixByInstrument = new Map();
143
- if (options.publishedCatalog) {
144
- for (const inst of options.publishedCatalog.instruments) {
145
- if (inst.idPrefix) {
146
- prefixByInstrument.set(inst.id, inst.idPrefix);
147
- }
148
- }
149
- }
150
- for (const [name, decl] of templates) {
151
- const prefix = stringSlot(decl.body, "idPrefix", "id_prefix") ?? prefixFor(name);
152
- prefixByInstrument.set(name, prefix);
153
- }
154
- for (const decl of program.decls) {
155
- if (decl.kind === "instrument_apply" && decl.metadata) {
156
- const prefix = stringSlot(decl.metadata, "idPrefix", "id_prefix");
157
- if (prefix) {
158
- prefixByInstrument.set(decl.name.name, prefix);
159
- }
160
- }
161
- }
162
- const resolvePrefix = (target) => prefixByInstrument.get(target);
163
- const concrete = [];
164
- const reachedPorts = new Set();
165
- const droppedPorts = new Set();
166
- const scopedDeclarations = (declarationScope) => {
167
- const scopedAliases = new Map(aliases);
168
- const scopedConstants = resolveConstants([...program.decls, ...declarationScope], resolvePrefix);
169
- const scopedParties = new Set(parties);
170
- const scopedPorts = new Map(ports);
171
- const scopedTemplates = new Map(templates);
172
- for (const local of declarationScope) {
173
- if (local.kind === "type")
174
- scopedAliases.set(local.name.name, local.value);
175
- if (local.kind === "party")
176
- scopedParties.add(local.name.name);
177
- if (local.kind === "port")
178
- scopedPorts.set(local.name.name, local);
179
- if (local.kind === "instrument") {
180
- scopedTemplates.set(local.name.name, local);
181
- const prefix = stringSlot(local.body, "idPrefix", "id_prefix") ??
182
- prefixFor(local.name.name);
183
- prefixByInstrument.set(local.name.name, prefix);
184
- }
185
- }
186
- checkPortDeclarations(scopedPorts, scopedParties);
187
- checkConstants(declarationScope, scopedAliases, diagnostics);
188
- return {
189
- aliases: scopedAliases,
190
- constants: scopedConstants,
191
- parties: scopedParties,
192
- ports: scopedPorts,
193
- templates: scopedTemplates,
194
- };
195
- };
196
- for (const decl of program.decls) {
197
- if (decl.kind === "instrument" &&
198
- !decl.hasParameterList &&
199
- decl.typeParameters.length === 0) {
200
- const scoped = scopedDeclarations(decl.declarationScope ?? []);
201
- const values = new Map(scoped.constants);
202
- const dependencies = new Map();
203
- const shapes = new Map();
204
- for (const [name, port] of scoped.ports) {
205
- const reference = {
206
- kind: "port_ref",
207
- name: port.name,
208
- span: port.span,
209
- };
210
- values.set(name, reference);
211
- bindPortCompileValues(values, name, reference, scoped.ports, diagnostics, scoped.aliases, shapes, dependencies);
212
- }
213
- const selected = selectCompileTimeRows(decl.body, values, diagnostics, dependencies, [], resolvePrefix);
214
- const body = expandComprehensions(selected, diagnostics, values, dependencies, resolvePrefix);
215
- const constructed = constructedInstruments(decl.name, body, diagnostics, resolvePrefix);
216
- for (const candidate of constructed) {
217
- const prefix = stringSlot(candidate.body, "idPrefix", "id_prefix") ??
218
- prefixFor(candidate.name.name);
219
- prefixByInstrument.set(candidate.name.name, prefix);
220
- }
221
- concrete.push(...bindActionPorts(constructed, scoped.ports, reachedPorts).map((candidate) => ({
222
- ...candidate,
223
- aliases: scoped.aliases,
224
- callee: candidate.name.name,
225
- parties: scoped.parties,
226
- })));
227
- }
228
- if (decl.kind !== "instrument_apply")
229
- continue;
230
- const application = decl.application;
231
- const callee = application.callee.parts.at(-1)?.name ?? "";
232
- const globalTemplate = templates.get(callee);
233
- const declarationScope = [
234
- ...new Set([
235
- ...(globalTemplate?.declarationScope ?? []),
236
- ...(decl.declarationScope ?? []),
237
- ]),
238
- ];
239
- const scoped = scopedDeclarations(declarationScope);
240
- const template = scoped.templates.get(callee);
241
- if (!template) {
242
- 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`);
243
- continue;
244
- }
245
- const boundPorts = new Map();
246
- const body = instantiate(template, application, diagnostics, decl.name, scoped.ports, scoped.parties, scoped.aliases, scoped.constants, boundPorts, resolvePrefix);
247
- if (body) {
248
- const applicationPorts = new Set();
249
- const merged = decl.metadata
250
- ? mergeApplicationMetadata(body, decl.metadata, diagnostics)
251
- : body;
252
- const constructed = constructedInstruments(decl.name, merged, diagnostics, resolvePrefix);
253
- for (const candidate of constructed) {
254
- const prefix = stringSlot(candidate.body, "idPrefix", "id_prefix") ??
255
- prefixFor(candidate.name.name);
256
- prefixByInstrument.set(candidate.name.name, prefix);
257
- }
258
- concrete.push(...bindActionPorts(constructed, boundPorts, applicationPorts).map((candidate) => {
259
- let candidateCallee = callee;
260
- if (candidate.name.name !== decl.name.name &&
261
- candidate.name.name.startsWith(`${decl.name.name}_`)) {
262
- candidateCallee = `${callee}${candidate.name.name.slice(decl.name.name.length)}`;
263
- }
264
- return {
265
- ...candidate,
266
- aliases: scoped.aliases,
267
- callee: candidateCallee,
268
- parties: scoped.parties,
269
- };
270
- }));
271
- for (const port of boundPorts.values()) {
272
- if (applicationPorts.has(port))
273
- reachedPorts.add(port);
274
- else
275
- droppedPorts.add(port);
276
- }
277
- }
278
- }
279
- for (const port of checkedPorts) {
280
- if (reachedPorts.has(port) && !droppedPorts.has(port))
281
- continue;
282
- report("HSX1024", port.name.span, `wired decision port ${port.name.name} reaches no action`, `carry ${port.name.name}'s condition into the action it authorizes`);
283
- }
284
- const allocated = allocateGeneratedPrefixes(concrete);
285
- if (options.publishedCatalog) {
286
- checkPortCaptureTypes(allocated, options.publishedCatalog, aliases, diagnostics);
287
- }
288
- for (const candidate of allocated) {
289
- const prefix = stringSlot(candidate.body, "idPrefix", "id_prefix") ??
290
- prefixFor(candidate.name.name);
291
- prefixByInstrument.set(candidate.name.name, prefix);
292
- }
293
- const instruments = [];
294
- const ids = new Set();
295
- for (const candidate of allocated) {
296
- if (ids.has(candidate.name.name)) {
297
- report("HSX1004", candidate.name.span, `instrument ${candidate.name.name} is emitted twice`, "rename one instrument");
298
- continue;
299
- }
300
- ids.add(candidate.name.name);
301
- if (catalogById?.has(candidate.name.name)) {
302
- report("HSX1022", candidate.name.span, `authored instrument ${candidate.name.name} collides with published catalog instrument`, "rename the authored instrument to avoid colliding with the published catalog");
303
- continue;
304
- }
305
- const expandedBody = expandComprehensions(candidate.body, diagnostics, new Map(), new Map(), resolvePrefix);
306
- const checked = checkInstrument(candidate.name, expandedBody, candidate.parties, candidate.ports, candidate.aliases, diagnostics, resolvePrefix);
307
- if (checked)
308
- instruments.push(checked);
309
- }
310
- if (options.publishedCatalog) {
311
- return checkPublishedProgram(program, options.publishedCatalog, header, subjects, instruments, diagnostics, report);
312
- }
313
- crossInstrumentReferenceDiagnostics(instruments, diagnostics, new Set(templates.keys()));
314
- if (header &&
315
- instruments.length === 0 &&
316
- !diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
317
- report("HSX1502", header.span, `program ${header.name.name} emits no instruments`, "declare or instantiate at least one instrument");
318
- }
319
- if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
320
- return { diagnostics };
321
- }
322
- const name = header?.name.name ?? module?.name.parts.at(-1)?.name ?? "module";
323
- const typed = {
324
- instruments,
325
- kind: "typed_program",
326
- name,
327
- origin: program.span,
328
- subjects,
329
- title: header?.title?.value ?? sentenceCase(name),
330
- };
331
- return prepareActionPlanProgram(typed, diagnostics);
332
- }
333
- function checkPublishedProgram(program, catalog, header, authoredSubjects, authoredInstruments, diagnostics, report) {
334
- const compositionDeclarations = program.decls.filter((decl) => decl.kind === "use" || decl.kind === "expose");
335
- if (header &&
336
- (compositionDeclarations.length > 0 || authoredInstruments.length === 0)) {
337
- if (!header.title || header.title.value.length === 0) {
338
- 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"`');
339
- }
340
- if (header.name.name.length > 60) {
341
- 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");
342
- }
343
- if ((header.title?.value.length ?? 0) > 80) {
344
- 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");
345
- }
346
- }
347
- const uses = program.decls.filter((decl) => decl.kind === "use");
348
- if (header && uses.length === 0 && authoredInstruments.length === 0) {
349
- report("HSX1016", header.span, `composed program ${header.name.name} uses no published instruments`, "add at least one `use published_instrument` declaration");
350
- }
351
- const catalogById = new Map(catalog.instruments.map((instrument) => [instrument.id, instrument]));
352
- const useById = new Map();
353
- for (const use of uses) {
354
- const id = use.instrument.name;
355
- if (!SNAKE_CASE.test(id)) {
356
- report("HSX1018", use.instrument.span, `published instrument id ${id} is not snake_case`, "write the published catalog id exactly");
357
- continue;
358
- }
359
- if (useById.has(id)) {
360
- report("HSX1017", use.instrument.span, `published instrument ${id} is used twice`, "delete the duplicate use declaration");
361
- continue;
362
- }
363
- useById.set(id, use);
364
- if (!catalogById.has(id)) {
365
- report("HSX1018", use.instrument.span, `published instrument ${id} does not exist in this catalog`, "choose an instrument id from the supplied published catalog");
366
- }
367
- }
368
- const selected = publishedInstrumentClosure(uses, catalogById);
369
- const selectedById = new Map(selected.map((entry) => [entry.instrument.id, entry]));
370
- const exposures = program.decls.filter((decl) => decl.kind === "expose");
371
- const authoredById = new Map(authoredInstruments.map((instrument) => [instrument.id, instrument]));
372
- const publicNames = new Map();
373
- const exposedTargets = new Map();
374
- for (const exposure of exposures) {
375
- const target = `${exposure.instrument.name}.${exposure.action.name}`;
376
- const explicitlyUsed = useById.has(exposure.instrument.name);
377
- const instrument = selectedById.get(exposure.instrument.name)?.instrument;
378
- const action = instrument?.actions[exposure.action.name];
379
- const authoredInstrument = authoredById.get(exposure.instrument.name);
380
- const authoredAction = authoredInstrument?.actions.find((candidate) => candidate.name === exposure.action.name);
381
- if ((!explicitlyUsed || !instrument || !action) &&
382
- (!authoredInstrument || !authoredAction)) {
383
- report("HSX1021", exposure.span, `${target} is not an action on an explicitly used published instrument or authored instrument`, "add the matching use declaration and choose one of that instrument's actions");
384
- continue;
385
- }
386
- if (action?.engineOwned || authoredAction?.slots.engineOwned === true) {
387
- report("HSX1021", exposure.span, `${target} is engine-owned`, "expose a caller action instead");
388
- continue;
389
- }
390
- if (!CAMEL_CASE.test(exposure.publicName.name)) {
391
- report("HSX1020", exposure.publicName.span, `public action name ${exposure.publicName.name} is not camelCase`, "write a lowercase camelCase public action name");
392
- continue;
393
- }
394
- const priorName = publicNames.get(exposure.publicName.name);
395
- const priorTarget = exposedTargets.get(target);
396
- if (priorName || priorTarget) {
397
- report("HSX1020", exposure.span, priorTarget
398
- ? `${target} is exposed more than once`
399
- : `public action name ${exposure.publicName.name} is declared more than once`, "keep one public name for one instrument action");
400
- continue;
401
- }
402
- publicNames.set(exposure.publicName.name, exposure);
403
- exposedTargets.set(target, exposure);
404
- }
405
- const updatedAuthoredInstruments = authoredInstruments.map((authoredInstrument) => {
406
- const instrumentExposures = exposures.filter((exposure) => exposure.instrument.name === authoredInstrument.id);
407
- if (instrumentExposures.length === 0)
408
- return authoredInstrument;
409
- const exposureByAction = new Map(instrumentExposures.map((exposure) => [exposure.action.name, exposure]));
410
- return {
411
- ...authoredInstrument,
412
- actions: authoredInstrument.actions.map((action) => {
413
- const exposure = exposureByAction.get(action.name);
414
- if (!exposure)
415
- return action;
416
- return {
417
- ...action,
418
- origin: exposure.span,
419
- slots: {
420
- ...action.slots,
421
- publicAction: exposure.publicName.name,
422
- },
423
- };
424
- }),
425
- };
426
- });
427
- const publishedInstruments = selected.map(({ instrument, origin }) => {
428
- const projected = projectedPublishedInstrument(instrument, exposures);
429
- return typedPublishedInstrument(projected, origin, exposures.filter((exposure) => exposure.instrument.name === instrument.id), catalog.instruments);
430
- });
431
- const instruments = [...publishedInstruments, ...updatedAuthoredInstruments];
432
- crossInstrumentReferenceDiagnostics(instruments, diagnostics);
433
- if (header &&
434
- instruments.length === 0 &&
435
- !diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
436
- report("HSX1502", header.span, `program ${header.name.name} emits no instruments`, "declare or instantiate at least one instrument");
437
- }
438
- const referencedSubjectKinds = new Set(selected.flatMap(({ instrument }) => instrument.subject?.kinds ?? []));
439
- const defaultSpan = uses[0]?.span ?? header?.span ?? program.span;
440
- const catalogSubjects = catalog.subjects
441
- .filter((subject) => referencedSubjectKinds.has(subject.kind))
442
- .map((subject) => typedPublishedSubject(subject, defaultSpan));
443
- const catalogSubjectKinds = new Set(catalogSubjects.map((subject) => subject.kind));
444
- for (const subject of authoredSubjects) {
445
- if (!catalogSubjectKinds.has(subject.kind))
446
- continue;
447
- 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");
448
- }
449
- if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
450
- return { diagnostics };
451
- }
452
- const module = program.decls.find((decl) => decl.kind === "module");
453
- const name = header?.name.name ?? module?.name.parts.at(-1)?.name ?? "module";
454
- const typed = {
455
- instruments,
456
- kind: "typed_program",
457
- name,
458
- origin: program.span,
459
- subjects: [
460
- ...catalogSubjects,
461
- ...authoredSubjects.filter((subject) => !catalogSubjectKinds.has(subject.kind)),
462
- ],
463
- title: header?.title?.value ?? sentenceCase(name),
464
- };
465
- return prepareActionPlanProgram(typed, diagnostics);
466
- }
467
- function projectedPublishedInstrument(source, exposures) {
468
- const publicNameByAction = new Map(exposures
469
- .filter((exposure) => exposure.instrument.name === source.id)
470
- .map((exposure) => [exposure.action.name, exposure.publicName.name]));
471
- return {
472
- ...structuredClone(source),
473
- actions: Object.fromEntries(source.actionOrder.map((name) => {
474
- const { publicAction: _publicAction, ...privateAction } = source.actions[name];
475
- const publicName = publicNameByAction.get(name);
476
- return [
477
- name,
478
- publicName
479
- ? { ...privateAction, publicAction: publicName }
480
- : privateAction,
481
- ];
482
- })),
483
- };
484
- }
485
- function publishedInstrumentClosure(uses, catalogById) {
486
- const selected = [];
487
- const selectedIds = new Set();
488
- const queue = uses.map((use) => ({
489
- id: use.instrument.name,
490
- origin: use.span,
491
- }));
492
- const idByPrefix = new Map([...catalogById.values()].map((instrument) => [
493
- instrument.idPrefix,
494
- instrument.id,
495
- ]));
496
- while (queue.length > 0) {
497
- const next = queue.shift();
498
- if (!next || selectedIds.has(next.id))
499
- continue;
500
- const instrument = catalogById.get(next.id);
501
- if (!instrument)
502
- continue;
503
- selectedIds.add(next.id);
504
- selected.push({ instrument, origin: next.origin });
505
- for (const dependency of publishedInstrumentDependencies(instrument, idByPrefix)) {
506
- if (!selectedIds.has(dependency))
507
- queue.push({ id: dependency, origin: next.origin });
508
- }
509
- }
510
- return selected;
511
- }
512
- function publishedInstrumentDependencies(instrument, idByPrefix) {
513
- const found = new Set(referencedUdlInstrumentIds(instrument));
514
- for (const field of Object.values(instrument.fields)) {
515
- const prefix = typeof field.pattern === "string"
516
- ? /^\^([a-z]{2,8})_\(sandbox\|live\)_/.exec(field.pattern)?.[1]
517
- : undefined;
518
- const dependency = prefix ? idByPrefix.get(prefix) : undefined;
519
- if (dependency)
520
- found.add(dependency);
521
- }
522
- found.delete(instrument.id);
523
- return [...found];
524
- }
525
- function typedPublishedInstrument(source, origin, exposures, catalog) {
526
- const exposureByAction = new Map(exposures.map((exposure) => [exposure.action.name, exposure]));
527
- const { actionOrder, actions, fields, id: _id, ...slots } = structuredClone(source);
528
- const requiredFields = new Set(source.required);
529
- return {
530
- actions: actionOrder.map((name) => {
531
- const exposure = exposureByAction.get(name);
532
- const { publicAction: _publicAction, ...privateAction } = actions[name];
533
- return {
534
- effects: (privateAction.effects ?? {}),
535
- name,
536
- origin: exposure?.span ?? origin,
537
- slots: (exposure
538
- ? { ...privateAction, publicAction: exposure.publicName.name }
539
- : privateAction),
540
- };
541
- }),
542
- fields: Object.entries(fields).map(([name, schema]) => ({
543
- name,
544
- origin,
545
- required: requiredFields.has(name),
546
- schema,
547
- type: publishedFieldType(schema, catalog),
548
- })),
549
- id: source.id,
550
- origin,
551
- slots: {
552
- ...slots,
553
- required: [...source.required],
554
- },
555
- };
556
- }
557
- /** Recover the semantic types retained by UDL before checking mixed relations. */
558
- function publishedFieldType(schema, catalog) {
559
- if (schema.type === "integer")
560
- return { kind: "integer" };
561
- if (schema.type === "boolean")
562
- return { kind: "boolean" };
563
- if (schema.type !== "string")
564
- return { kind: "unknown" };
565
- if (schema.pattern === MONEY_PATTERN ||
566
- schema.pattern === ZERO_MONEY_PATTERN) {
567
- const currency = schema["x-hyperscale-currency"];
568
- return typeof currency === "string"
569
- ? { currency, kind: "money" }
570
- : { kind: "money" };
571
- }
572
- const target = catalog.find((instrument) => schema.pattern ===
573
- `^${instrument.idPrefix}_(sandbox|live)_[a-z0-9]{8,64}$`);
574
- return target ? { kind: "ref", target: target.id } : { kind: "text" };
575
- }
576
- const STRING_BACKED_PORT_KINDS = new Set([
577
- "account",
578
- "condition",
579
- "date",
580
- "party",
581
- "ref",
582
- "text",
583
- ]);
584
- const INTEGER_BACKED_PORT_KINDS = new Set([
585
- "bps",
586
- "integer",
587
- "percent",
588
- ]);
589
- /**
590
- * A published schema keeps less than the HSX type: dates and accounts publish
591
- * as plain strings, percents and bps as integers. Compare by the family the
592
- * schema can still express so the check never rejects a richer declared type.
593
- */
594
- function samePortType(declared, captured) {
595
- if (captured.kind === "unknown" || declared.kind === "unknown")
596
- return true;
597
- if (captured.kind === "text")
598
- return STRING_BACKED_PORT_KINDS.has(declared.kind);
599
- if (captured.kind === "integer") {
600
- return INTEGER_BACKED_PORT_KINDS.has(declared.kind);
601
- }
602
- if (declared.kind !== captured.kind)
603
- return false;
604
- if (captured.currency && declared.currency !== captured.currency) {
605
- return false;
606
- }
607
- if (captured.target && declared.target !== captured.target) {
608
- return false;
609
- }
610
- return true;
611
- }
612
- function portTypeWords(type) {
613
- if (type.kind === "ref" && type.target)
614
- return `ref<${type.target}>`;
615
- return typeWords(type);
616
- }
617
- function getFieldSchemaFromAction(action, fieldName) {
618
- if (!action.captureInput)
619
- return undefined;
620
- const properties = action.input?.properties;
621
- if (!properties)
622
- return undefined;
623
- for (const inputKey of Object.values(action.captureInput)) {
624
- if (inputKey !== fieldName && camel(inputKey) !== camel(fieldName)) {
625
- continue;
626
- }
627
- const schema = properties[inputKey] ?? properties[camel(inputKey)];
628
- if (schema && typeof schema === "object")
629
- return schema;
630
- }
631
- return undefined;
632
- }
633
- function findCapturedFieldSchema(catalogInstrument, actionName, fieldName) {
634
- const targetAction = catalogInstrument.actions[actionName];
635
- return targetAction
636
- ? getFieldSchemaFromAction(targetAction, fieldName)
637
- : undefined;
638
- }
639
- function checkPortCaptureTypes(candidates, publishedCatalog, topLevelAliases, diagnostics) {
640
- const catalogById = new Map(publishedCatalog.instruments.map((instrument) => [
641
- instrument.id,
642
- instrument,
643
- ]));
644
- for (const candidate of candidates) {
645
- const catalogInstrument = catalogById.get(candidate.name.name) ??
646
- (candidate.callee ? catalogById.get(candidate.callee) : undefined);
647
- if (!catalogInstrument)
648
- continue;
649
- const aliases = new Map([...topLevelAliases, ...candidate.aliases]);
650
- for (const [actionName, declaredPorts] of candidate.ports) {
651
- for (const port of declaredPorts) {
652
- const rawShape = entry(port.body, "shape")?.value;
653
- if (rawShape?.kind !== "block")
654
- continue;
655
- for (const fieldEntry of rawShape.entries) {
656
- const fieldName = fieldEntry.key.name;
657
- const capturedSchema = findCapturedFieldSchema(catalogInstrument, actionName, fieldName);
658
- if (!capturedSchema)
659
- continue;
660
- const capturedType = publishedFieldType(capturedSchema, publishedCatalog.instruments);
661
- const declaredType = typeOf(fieldEntry.value, aliases);
662
- if (!samePortType(declaredType, capturedType)) {
663
- const declaredWords = portTypeWords(declaredType);
664
- const capturedWords = portTypeWords(capturedType);
665
- const message = `decision port ${port.name.name} field ${fieldName} declares ${declaredWords} but instrument ${catalogInstrument.id} captures it as ${capturedWords}`;
666
- const fix = `declare ${fieldName} as ${capturedWords} in decision port ${port.name.name}`;
667
- if (!diagnostics.some((prior) => prior.code === "HSX1026" &&
668
- prior.span.start === fieldEntry.value.span.start &&
669
- prior.span.end === fieldEntry.value.span.end &&
670
- prior.message === message)) {
671
- diagnostics.push({
672
- code: "HSX1026",
673
- fix,
674
- message,
675
- severity: "error",
676
- span: fieldEntry.value.span,
677
- });
678
- }
679
- }
680
- }
681
- }
682
- }
683
- }
684
- }
685
- function typedPublishedSubject(subject, origin) {
686
- return {
687
- declaredValue: subject.declaredValue,
688
- kind: subject.kind,
689
- origin,
690
- schema: structuredClone(subject.schema),
691
- title: subject.title,
692
- version: subject.version,
693
- };
694
- }
695
- // Presentation only. An application may restate how a settlement reads to a
696
- // human or an agent; it may never restate what the settlement does. Both sets
697
- // carry agent_description for the same reason they carry description: it is
698
- // prose about the shape, and the catalog's voice is more concrete than the
699
- // parameterized template's interpolated one.
700
- const applicationMetadataKeys = new Set([
701
- "action",
702
- "agent_description",
703
- "description",
704
- "nav",
705
- "navigation",
706
- "summary",
707
- "surface_visibility",
708
- "template_id",
709
- "title",
710
- "visibility",
711
- ]);
712
- const applicationActionMetadataKeys = new Set([
713
- "agent_description",
714
- "description",
715
- "examples",
716
- "public",
717
- "public_action",
718
- "summary",
719
- ]);
720
- function applicationInstrumentMetadataKey(key) {
721
- if (key === "navigation")
722
- return "nav";
723
- if (key === "visibility")
724
- return "surface_visibility";
725
- return key;
726
- }
727
- function applicationActionMetadataKey(key) {
728
- return key === "public_action" ? "public" : key;
729
- }
730
- function mergeApplicationMetadata(body, metadata, diagnostics) {
731
- let entries = [...body.entries];
732
- for (const row of metadata.entries) {
733
- if (!applicationMetadataKeys.has(row.key.name)) {
734
- diagnostics.push({
735
- code: "HSX1507",
736
- fix: "move contract mechanics into the parameterized instrument",
737
- message: `application metadata cannot replace ${row.key.name}`,
738
- severity: "error",
739
- span: row.key.span,
740
- });
741
- continue;
742
- }
743
- if (row.key.name !== "action") {
744
- const overlayKey = applicationInstrumentMetadataKey(row.key.name);
745
- const normalizedRow = overlayKey === row.key.name
746
- ? row
747
- : { ...row, key: { ...row.key, name: overlayKey } };
748
- entries = [
749
- ...entries.filter((candidate) => applicationInstrumentMetadataKey(candidate.key.name) !== overlayKey),
750
- normalizedRow,
751
- ];
752
- continue;
753
- }
754
- const actionName = row.qualifiers[0]?.name;
755
- const targetIndex = entries.findIndex((candidate) => candidate.key.name === "action" &&
756
- candidate.qualifiers[0]?.name === actionName);
757
- const target = entries[targetIndex];
758
- if (!actionName ||
759
- row.value.kind !== "block" ||
760
- !target ||
761
- target.value.kind !== "block") {
762
- diagnostics.push({
763
- code: "HSX1507",
764
- fix: "name an action declared by the parameterized instrument",
765
- message: `application metadata action ${actionName ?? "<missing>"} does not exist`,
766
- severity: "error",
767
- span: row.span,
768
- });
769
- continue;
770
- }
771
- const invalid = row.value.entries.find((candidate) => !applicationActionMetadataKeys.has(candidate.key.name));
772
- if (invalid) {
773
- diagnostics.push({
774
- code: "HSX1507",
775
- fix: "keep only summary, description, agent_description, public, or public_action here",
776
- message: `application metadata cannot replace action clause ${invalid.key.name}`,
777
- severity: "error",
778
- span: invalid.span,
779
- });
780
- continue;
781
- }
782
- const overlayKeys = new Set(row.value.entries.map((entry) => applicationActionMetadataKey(entry.key.name)));
783
- entries[targetIndex] = {
784
- ...target,
785
- value: {
786
- ...target.value,
787
- entries: [
788
- ...target.value.entries.filter((candidate) => !overlayKeys.has(applicationActionMetadataKey(candidate.key.name))),
789
- ...row.value.entries,
790
- ],
791
- },
792
- };
793
- }
794
- return { ...body, entries };
795
- }
796
- function bindActionPorts(candidates, bindings, reached) {
797
- const declarations = new Map([...bindings.values()].map((port) => [port.name.name, port]));
798
- return candidates.map((candidate) => {
799
- const ports = new Map();
800
- const entries = candidate.body.entries.map((row) => {
801
- if (row.key.name !== "action" || row.value.kind !== "block")
802
- return row;
803
- const action = row.qualifiers[0]?.name;
804
- if (!action)
805
- return row;
806
- const declared = (row.conditionPorts ?? []).flatMap((name) => {
807
- const port = declarations.get(name);
808
- if (!port)
809
- return [];
810
- reached.add(port);
811
- return [port];
812
- });
813
- if (!declared.length)
814
- return row;
815
- ports.set(action, declared);
816
- if (entry(row.value, "port"))
817
- return row;
818
- const source = declared[0];
819
- const allowed = entry(source.body, "allowed")?.value;
820
- const span = source.span;
821
- return {
822
- ...row,
823
- value: {
824
- ...row.value,
825
- entries: [
826
- ...row.value.entries,
827
- {
828
- key: { kind: "ident", name: "port", span },
829
- qualifiers: [],
830
- span,
831
- value: {
832
- kind: "block",
833
- span,
834
- entries: [
835
- {
836
- key: {
837
- kind: "ident",
838
- name: "allowed_parties",
839
- span,
840
- },
841
- qualifiers: [],
842
- span,
843
- value: allowed?.kind === "list"
844
- ? allowed
845
- : {
846
- kind: "list",
847
- span,
848
- items: allowed ? [allowed] : [],
849
- },
850
- },
851
- ],
852
- },
853
- },
854
- ],
855
- },
856
- };
857
- });
858
- return { ...candidate, body: { ...candidate.body, entries }, ports };
859
- });
860
- }
861
- function constructedInstruments(rootName, body, diagnostics, resolvePrefix) {
862
- const expanded = expandComprehensions(body, diagnostics, new Map(), new Map(), resolvePrefix);
863
- const construction = entry(expanded, "instruments");
864
- const root = {
865
- body: {
866
- ...expanded,
867
- entries: expanded.entries.filter((row) => row !== construction),
868
- },
869
- generatedPrefix: false,
870
- name: rootName,
871
- };
872
- if (construction?.value.kind !== "block")
873
- return [root];
874
- const companions = construction.value.entries.flatMap((row) => {
875
- if (row.key.name !== "instrument" || row.value.kind !== "block") {
876
- diagnostics.push({
877
- code: "HSX1406",
878
- fix: 'write `instrument { id: concat(instrument, "_suffix"); ... }` inside the instruments block',
879
- message: "an instruments block contains only fixed instrument constructors",
880
- severity: "error",
881
- span: row.span,
882
- });
883
- return [];
884
- }
885
- const idRow = entry(row.value, "id");
886
- const id = idRow ? nameText(idRow.value) : "";
887
- if (!id || id === "invalid_compile_time_name") {
888
- diagnostics.push({
889
- code: "HSX1406",
890
- fix: "give the companion instrument a compile-time id",
891
- message: "a companion instrument id must resolve at compile time",
892
- severity: "error",
893
- span: idRow?.span ?? row.span,
894
- });
895
- return [];
896
- }
897
- const generated = entry(row.value, "generatedPrefix");
898
- return [
899
- {
900
- body: {
901
- ...row.value,
902
- entries: row.value.entries.filter((child) => child !== idRow && child !== generated),
903
- },
904
- generatedPrefix: generated?.value.kind === "boolean" && generated.value.value,
905
- name: {
906
- kind: "ident",
907
- name: id,
908
- span: idRow?.span ?? row.span,
909
- },
910
- },
911
- ];
912
- });
913
- return [root, ...companions];
914
- }
915
- function allocateGeneratedPrefixes(candidates) {
916
- const used = new Set(candidates
917
- .filter((candidate) => !candidate.generatedPrefix)
918
- .map((candidate) => stringSlot(candidate.body, "idPrefix", "id_prefix") ??
919
- prefixFor(candidate.name.name)));
920
- let ordinal = 0;
921
- const next = () => {
922
- while (true) {
923
- const high = String.fromCharCode(97 + Math.floor(ordinal / 26));
924
- const low = String.fromCharCode(97 + (ordinal % 26));
925
- ordinal += 1;
926
- const candidate = `zz${high}${low}`;
927
- if (!used.has(candidate)) {
928
- used.add(candidate);
929
- return candidate;
930
- }
931
- }
932
- };
933
- return candidates.map((candidate) => {
934
- if (!candidate.generatedPrefix)
935
- return candidate;
936
- const value = {
937
- kind: "string",
938
- span: candidate.name.span,
939
- value: next(),
940
- };
941
- return {
942
- ...candidate,
943
- body: {
944
- ...candidate.body,
945
- entries: [
946
- ...candidate.body.entries,
947
- {
948
- key: {
949
- kind: "ident",
950
- name: "id_prefix",
951
- span: candidate.name.span,
952
- },
953
- qualifiers: [],
954
- span: candidate.name.span,
955
- value,
956
- },
957
- ],
958
- },
959
- };
960
- });
961
- }
962
- function inferSettlementTypeArgument(template, rows, typeParameter) {
963
- for (const parameter of template.parameters) {
964
- const declaredType = parameter.type;
965
- const parameterType = declaredType.kind === "type_apply" &&
966
- declaredType.callee.name === "optional"
967
- ? (declaredType.args[0] ?? declaredType)
968
- : declaredType;
969
- if (parameterType.kind !== "type_apply" ||
970
- parameterType.callee.name !== "money" ||
971
- parameterType.args[0]?.kind !== "ident" ||
972
- parameterType.args[0].name !== typeParameter) {
973
- continue;
974
- }
975
- const value = rows.get(parameter.name.name);
976
- const valueType = value?.kind === "binding" ? value.type : value;
977
- if ((valueType?.kind === "call" || valueType?.kind === "type_apply") &&
978
- valueType.callee.name === "money") {
979
- return valueType.args[0];
980
- }
981
- if (valueType?.kind === "money")
982
- return valueType.currency;
983
- }
984
- return undefined;
985
- }
986
- function checkConstants(decls, aliases, diagnostics) {
987
- for (const decl of decls) {
988
- if (decl.kind !== "const" || !decl.type)
989
- continue;
990
- const expected = typeOf(decl.type, aliases);
991
- const actual = typeOf(decl.value, aliases);
992
- if (expected.kind === "money" &&
993
- actual.kind === "money" &&
994
- expected.currency !== actual.currency) {
995
- diagnostics.push({
996
- code: "HSX1101",
997
- fix: `write a ${expected.currency} literal or change ${decl.name.name}'s declared type`,
998
- message: `${decl.name.name} is money<${expected.currency}> but receives money<${actual.currency}>; currencies never coerce`,
999
- severity: "error",
1000
- span: decl.value.span,
1001
- });
1002
- }
1003
- if (decl.value.kind === "money")
1004
- validateMoneyLiteral(decl, diagnostics);
1005
- }
1006
- }
1007
- function resolveConstants(decls, resolvePrefix) {
1008
- const constants = new Map(decls
1009
- .filter((decl) => decl.kind === "const")
1010
- .map((decl) => [decl.name.name, decl.value]));
1011
- for (let pass = 0; pass < constants.size; pass += 1) {
1012
- for (const [name, value] of constants) {
1013
- constants.set(name, substituteExpr(value, constants, resolvePrefix));
1014
- }
1015
- }
1016
- return constants;
1017
- }
1018
- function validateMoneyLiteral(decl, diagnostics) {
1019
- if (decl.value.kind !== "money")
1020
- return;
1021
- const precision = minorUnits[decl.value.currency.name] ?? 2;
1022
- const fraction = decl.value.raw.split(".")[1] ?? "";
1023
- if (fraction.length <= precision)
1024
- return;
1025
- diagnostics.push({
1026
- code: "HSX1102",
1027
- fix: `round the literal to ${precision} minor-unit digit${precision === 1 ? "" : "s"}`,
1028
- message: `${decl.value.currency.name} ${decl.value.raw} has ${fraction.length} decimal places; ${decl.value.currency.name} settles to ${precision}`,
1029
- severity: "error",
1030
- span: decl.value.span,
1031
- });
1032
- }
1033
- function instantiate(template, application, diagnostics, instrumentName, ports, parties, aliases, constants, boundPorts, resolvePrefix) {
1034
- const loweredPortShapes = new Map();
1035
- const portDependencies = new Map();
1036
- const partyKeyedParameters = partyKeyedBlockParameters(template, aliases);
1037
- const named = new Map();
1038
- const positional = [];
1039
- const parameterNames = new Set(template.parameters.map((parameter) => parameter.name.name));
1040
- for (const arg of application.args) {
1041
- if (arg.kind !== "binding") {
1042
- positional.push(arg);
1043
- continue;
1044
- }
1045
- if (!parameterNames.has(arg.name.name)) {
1046
- diagnostics.push({
1047
- code: "HSX1012",
1048
- fix: `remove ${arg.name.name} or name one of ${[...parameterNames].join(", ")}`,
1049
- message: `${template.name.name} has no argument named ${arg.name.name}`,
1050
- severity: "error",
1051
- span: arg.name.span,
1052
- });
1053
- continue;
1054
- }
1055
- if (named.has(arg.name.name)) {
1056
- diagnostics.push({
1057
- code: "HSX1013",
1058
- fix: `pass ${arg.name.name} once`,
1059
- message: `${template.name.name} receives ${arg.name.name} more than once`,
1060
- severity: "error",
1061
- span: arg.name.span,
1062
- });
1063
- continue;
1064
- }
1065
- named.set(arg.name.name, arg.type);
1066
- }
1067
- const namedParameterCount = template.parameters.filter((parameter) => named.has(parameter.name.name)).length;
1068
- const maximumPositional = template.parameters.length - namedParameterCount;
1069
- if (positional.length > maximumPositional) {
1070
- diagnostics.push({
1071
- code: "HSX1012",
1072
- fix: `pass at most ${template.parameters.length} arguments`,
1073
- message: `${template.name.name} receives ${application.args.length} arguments but declares ${template.parameters.length}`,
1074
- severity: "error",
1075
- span: application.span,
1076
- });
1077
- }
1078
- const typeArgs = application.typeArgs.length > 0
1079
- ? application.typeArgs
1080
- : template.typeParameters.map((parameter) => inferSettlementTypeArgument(template, named, parameter.name.name) ??
1081
- parameter.name);
1082
- if (template.typeParameters.length !== typeArgs.length) {
1083
- diagnostics.push({
1084
- code: "HSX1010",
1085
- fix: `pass ${template.typeParameters.length} type argument${template.typeParameters.length === 1 ? "" : "s"}`,
1086
- message: `${template.name.name} expects ${template.typeParameters.length} type arguments but receives ${application.typeArgs.length}`,
1087
- severity: "error",
1088
- span: application.span,
1089
- });
1090
- return undefined;
1091
- }
1092
- const values = new Map(constants);
1093
- const missingOptional = new Map();
1094
- values.set("instrument", instrumentName);
1095
- const fieldBindings = new Map();
1096
- template.typeParameters.forEach((parameter, index) => {
1097
- const value = typeArgs[index];
1098
- if (value)
1099
- values.set(parameter.name.name, value);
1100
- });
1101
- for (const [name, value] of named)
1102
- values.set(name, value);
1103
- const resolvedArgs = [];
1104
- let position = 0;
1105
- for (const parameter of template.parameters) {
1106
- const value = named.get(parameter.name.name) ?? positional[position++];
1107
- resolvedArgs.push({ parameter, value });
1108
- if (!value) {
1109
- if (optionalInner(parameter.type)) {
1110
- values.set(parameter.name.name, booleanExpr(false, parameter.span));
1111
- missingOptional.set(parameter.name.name, parameter);
1112
- continue;
1113
- }
1114
- diagnostics.push({
1115
- code: "HSX1011",
1116
- fix: `pass ${parameter.name.name}: <${typeWords(typeOf(parameter.type, aliases))}>`,
1117
- message: `${template.name.name} needs argument ${parameter.name.name}`,
1118
- severity: "error",
1119
- span: application.span,
1120
- });
1121
- continue;
1122
- }
1123
- const expectedExpr = substituteExpr(optionalInner(parameter.type) ?? parameter.type, values, resolvePrefix);
1124
- const expected = typeOf(expectedExpr, aliases);
1125
- const actual = typeOf(value, aliases);
1126
- const nonPartyKeys = partyKeyedParameters.get(parameter.name.name);
1127
- if (nonPartyKeys && value.kind === "block") {
1128
- const declaredParties = [...parties].sort();
1129
- for (const row of value.entries) {
1130
- if (parties.has(row.key.name) || nonPartyKeys.has(row.key.name))
1131
- continue;
1132
- diagnostics.push({
1133
- code: "HSX1001",
1134
- fix: declaredParties.length > 0
1135
- ? `replace ${row.key.name} with one of ${declaredParties.join(", ")}`
1136
- : `declare the party named by ${row.key.name}`,
1137
- message: `${parameter.name.name} uses party key ${row.key.name}, which is not declared`,
1138
- severity: "error",
1139
- span: row.key.span,
1140
- });
1141
- }
1142
- }
1143
- if (expected.kind === "money" &&
1144
- actual.kind === "money" &&
1145
- expected.currency &&
1146
- actual.currency &&
1147
- expected.currency !== actual.currency) {
1148
- diagnostics.push({
1149
- code: "HSX1101",
1150
- fix: `pass money<${expected.currency}> to ${parameter.name.name}`,
1151
- message: `${parameter.name.name} needs money<${expected.currency}> but receives money<${actual.currency}>`,
1152
- severity: "error",
1153
- span: value.span,
1154
- });
1155
- }
1156
- if (!argumentMatches(expectedExpr, value, parties, ports, aliases)) {
1157
- diagnostics.push({
1158
- code: "HSX1104",
1159
- fix: `pass ${typeWords(expected)} to ${parameter.name.name}`,
1160
- message: `${parameter.name.name} needs ${typeWords(expected)} but receives ${argumentWords(value, aliases)}`,
1161
- severity: "error",
1162
- span: value.span,
1163
- });
1164
- }
1165
- if (value.kind === "binding") {
1166
- fieldBindings.set(parameter.name.name, {
1167
- name: value.name,
1168
- type: value.type,
1169
- });
1170
- values.set(parameter.name.name, value.name);
1171
- }
1172
- else if (expected.kind !== "party" && value.kind === "ident") {
1173
- fieldBindings.set(parameter.name.name, {
1174
- name: value,
1175
- type: substituteExpr(optionalInner(parameter.type) ?? parameter.type, values, resolvePrefix),
1176
- });
1177
- values.set(parameter.name.name, value);
1178
- }
1179
- else {
1180
- values.set(parameter.name.name, value);
1181
- }
1182
- }
1183
- for (const { parameter, value } of resolvedArgs) {
1184
- if (value?.kind === "port_ref") {
1185
- const port = ports.get(value.name.name);
1186
- if (port)
1187
- boundPorts.set(parameter.name.name, port);
1188
- bindPortCompileValues(values, parameter.name.name, value, ports, diagnostics, aliases, loweredPortShapes, portDependencies, resolvePrefix);
1189
- }
1190
- }
1191
- checkClaimAmountPortField(resolvedArgs, ports, aliases, diagnostics);
1192
- const selected = selectCompileTimeRows(template.body, values, diagnostics, portDependencies, [], resolvePrefix);
1193
- const dependencyDiagnosticStart = diagnostics.length;
1194
- for (const [name, parameter] of missingOptional) {
1195
- if (!referencesIdentifier(selected, name, values))
1196
- continue;
1197
- const inner = optionalInner(parameter.type);
1198
- if (!inner)
1199
- continue;
1200
- diagnostics.push({
1201
- code: "HSX1011",
1202
- fix: `pass ${name}: <${typeWords(typeOf(inner, aliases))}>`,
1203
- message: `${template.name.name} needs argument ${name} for the selected options`,
1204
- severity: "error",
1205
- span: application.span,
1206
- });
1207
- }
1208
- if (diagnostics.length > dependencyDiagnosticStart)
1209
- return undefined;
1210
- const body = expandComprehensions(selected, diagnostics, values, portDependencies, resolvePrefix);
1211
- const binding = {
1212
- key: { kind: "ident", name: "template_binding", span: application.span },
1213
- qualifiers: [],
1214
- span: application.span,
1215
- value: {
1216
- kind: "block",
1217
- span: application.span,
1218
- entries: [
1219
- {
1220
- key: { kind: "ident", name: "id", span: application.span },
1221
- qualifiers: [],
1222
- span: application.span,
1223
- value: {
1224
- kind: "string",
1225
- value: template.name.name,
1226
- span: application.span,
1227
- },
1228
- },
1229
- {
1230
- key: { kind: "ident", name: "parameters", span: application.span },
1231
- qualifiers: [],
1232
- span: application.span,
1233
- value: {
1234
- kind: "block",
1235
- span: application.span,
1236
- entries: resolvedArgs.flatMap(({ parameter }) => {
1237
- const value = values.get(parameter.name.name);
1238
- return value &&
1239
- ["string", "number", "boolean"].includes(value.kind)
1240
- ? [
1241
- {
1242
- key: parameter.name,
1243
- qualifiers: [],
1244
- span: parameter.span,
1245
- value,
1246
- },
1247
- ]
1248
- : [];
1249
- }),
1250
- },
1251
- },
1252
- ],
1253
- },
1254
- };
1255
- return {
1256
- ...body,
1257
- entries: [...body.entries, binding].map((row) => {
1258
- if (row.key.name !== "fields" || row.value.kind !== "block")
1259
- return row;
1260
- return {
1261
- ...row,
1262
- value: {
1263
- ...row.value,
1264
- entries: row.value.entries.map((field) => {
1265
- const binding = fieldBindings.get(field.key.name);
1266
- return binding
1267
- ? {
1268
- ...field,
1269
- key: binding.name,
1270
- value: field.value.kind === "block"
1271
- ? {
1272
- ...field.value,
1273
- entries: field.value.entries.map((row) => row.key.name === "type"
1274
- ? { ...row, value: binding.type }
1275
- : row),
1276
- }
1277
- : binding.type,
1278
- }
1279
- : field;
1280
- }),
1281
- },
1282
- };
1283
- }),
1284
- };
1285
- }
1286
- function referencesIdentifier(expr, name, values) {
1287
- if (expr.kind === "ident")
1288
- return expr.name === name;
1289
- if (expr.kind === "block") {
1290
- return expr.entries.some((row) => row.key.name !== "let" && referencesIdentifier(row.value, name, values));
1291
- }
1292
- if (expr.kind === "call") {
1293
- if (expr.callee.name === "if_eq") {
1294
- const [left, right, whenEqual, whenDifferent] = expr.args;
1295
- const actualLeft = left ? substituteCompileArgument(left, values) : left;
1296
- const actualRight = right
1297
- ? substituteCompileArgument(right, values)
1298
- : right;
1299
- const selectedBranch = actualLeft &&
1300
- actualRight &&
1301
- nameText(actualLeft) === nameText(actualRight)
1302
- ? whenEqual
1303
- : whenDifferent;
1304
- return selectedBranch
1305
- ? referencesIdentifier(selectedBranch, name, values)
1306
- : false;
1307
- }
1308
- return expr.args.some((argument) => referencesIdentifier(argument, name, values));
1309
- }
1310
- if (expr.kind === "type_apply") {
1311
- return expr.args.some((argument) => referencesIdentifier(argument, name, values));
1312
- }
1313
- if (expr.kind === "apply") {
1314
- return (expr.args.some((argument) => referencesIdentifier(argument, name, values)) ||
1315
- expr.typeArgs.some((argument) => referencesIdentifier(argument, name, values)));
1316
- }
1317
- if (expr.kind === "list") {
1318
- return expr.items.some((item) => referencesIdentifier(item, name, values));
1319
- }
1320
- if (expr.kind === "binding")
1321
- return referencesIdentifier(expr.type, name, values);
1322
- if (expr.kind === "decided_amount") {
1323
- return referencesIdentifier(expr.body, name, values);
1324
- }
1325
- return false;
1326
- }
1327
- function partyKeyedBlockParameters(template, aliases) {
1328
- const partyParameters = new Set(template.parameters
1329
- .filter((parameter) => typeOf(optionalInner(parameter.type) ?? parameter.type, aliases)
1330
- .kind === "party")
1331
- .map((parameter) => parameter.name.name));
1332
- const partyListParameters = new Set(template.parameters
1333
- .filter((parameter) => {
1334
- const type = resolveAlias(optionalInner(parameter.type) ?? parameter.type, aliases);
1335
- const item = type.kind === "type_apply" ? type.args[0] : undefined;
1336
- return (type.kind === "type_apply" &&
1337
- type.callee.name === "list" &&
1338
- item !== undefined &&
1339
- typeOf(item, aliases).kind === "party");
1340
- })
1341
- .map((parameter) => parameter.name.name));
1342
- const blockParameters = new Set(template.parameters
1343
- .filter((parameter) => {
1344
- const type = resolveAlias(optionalInner(parameter.type) ?? parameter.type, aliases);
1345
- return type.kind === "ident" && type.name === "block";
1346
- })
1347
- .map((parameter) => parameter.name.name));
1348
- const keyed = new Set();
1349
- const nonPartyKeys = new Map();
1350
- const partyBindings = new Set(partyParameters);
1351
- const visit = (expr) => {
1352
- if (expr.kind === "call") {
1353
- const [owner, key] = expr.args;
1354
- if (expr.callee.name === "get" &&
1355
- owner?.kind === "ident" &&
1356
- key?.kind === "ident" &&
1357
- blockParameters.has(owner.name) &&
1358
- partyBindings.has(key.name)) {
1359
- keyed.add(owner.name);
1360
- }
1361
- expr.args.forEach(visit);
1362
- return;
1363
- }
1364
- if (expr.kind === "type_apply") {
1365
- expr.args.forEach(visit);
1366
- return;
1367
- }
1368
- if (expr.kind === "apply") {
1369
- expr.args.forEach(visit);
1370
- expr.typeArgs.forEach(visit);
1371
- return;
1372
- }
1373
- if (expr.kind === "list") {
1374
- expr.items.forEach(visit);
1375
- return;
1376
- }
1377
- if (expr.kind === "block") {
1378
- for (const row of expr.entries) {
1379
- if (row.key.name === "let") {
1380
- const binding = row.qualifiers[0];
1381
- const value = row.value;
1382
- const aliasesParty = value.kind === "ident" && partyBindings.has(value.name);
1383
- const aliasesPartyList = value.kind === "ident" && partyListParameters.has(value.name);
1384
- const derivesBlockKeys = value.kind === "call" &&
1385
- value.callee.name === "keys_except" &&
1386
- value.args[0]?.kind === "ident" &&
1387
- blockParameters.has(value.args[0].name);
1388
- const indexesPartyList = value.kind === "call" &&
1389
- value.callee.name === "at" &&
1390
- value.args[0]?.kind === "ident" &&
1391
- partyListParameters.has(value.args[0].name);
1392
- if (binding && (aliasesPartyList || derivesBlockKeys)) {
1393
- partyListParameters.add(binding.name);
1394
- }
1395
- if (derivesBlockKeys) {
1396
- const owner = value.args[0];
1397
- if (owner?.kind === "ident") {
1398
- const omitted = nonPartyKeys.get(owner.name) ?? new Set();
1399
- for (const argument of value.args.slice(1)) {
1400
- omitted.add(nameText(argument));
1401
- }
1402
- nonPartyKeys.set(owner.name, omitted);
1403
- }
1404
- }
1405
- if (binding && (aliasesParty || indexesPartyList)) {
1406
- partyBindings.add(binding.name);
1407
- }
1408
- }
1409
- visit(row.value);
1410
- }
1411
- return;
1412
- }
1413
- if (expr.kind === "decided_amount") {
1414
- visit(expr.body);
1415
- return;
1416
- }
1417
- if (expr.kind === "binding")
1418
- visit(expr.type);
1419
- };
1420
- visit(template.body);
1421
- return new Map([...keyed].map((parameter) => [
1422
- parameter,
1423
- nonPartyKeys.get(parameter) ?? new Set(),
1424
- ]));
1425
- }
1426
- function shapeTypeText(expr) {
1427
- if (expr.kind === "ident")
1428
- return expr.name;
1429
- if (expr.kind === "block")
1430
- return "block";
1431
- if (expr.kind === "list")
1432
- return "list";
1433
- if (expr.kind === "number")
1434
- return expr.raw;
1435
- if (expr.kind === "string")
1436
- return `"${expr.value}"`;
1437
- if (expr.kind === "boolean")
1438
- return String(expr.value);
1439
- if (expr.kind === "call") {
1440
- const args = expr.args.map(shapeTypeText).join(", ");
1441
- return `${expr.callee.name}(${args})`;
1442
- }
1443
- if (expr.kind === "type_apply") {
1444
- const args = expr.args.map(shapeTypeText).join(", ");
1445
- return `${expr.callee.name}<${args}>`;
1446
- }
1447
- return nameText(expr);
1448
- }
1449
- function lowerPortShapeField(port, row, aliases, diagnostics, resolvePrefix) {
1450
- const type = typeOf(row.value, aliases);
1451
- if (![
1452
- "text",
1453
- "money",
1454
- "date",
1455
- "integer",
1456
- "boolean",
1457
- "account",
1458
- "ref",
1459
- "bps",
1460
- "percent",
1461
- ].includes(type.kind) ||
1462
- ((type.kind === "money" || type.kind === "account") &&
1463
- (!type.currency || !CURRENCY.test(type.currency)))) {
1464
- diagnostics.push({
1465
- code: "HSX1025",
1466
- fix: `declare ${row.key.name} as text, money(CUR), date, integer, boolean, account(CUR), ref<instrument_id>, id(instrument_id), bps, or percent`,
1467
- message: `decision port ${port.name.name} shape field ${row.key.name} has unknown type ${shapeTypeText(row.value)}`,
1468
- severity: "error",
1469
- span: row.value.span,
1470
- });
1471
- return undefined;
1472
- }
1473
- const extras = {};
1474
- if (type.kind === "text") {
1475
- extras.description =
1476
- "Required decision reference retained in the operation receipt";
1477
- extras.max_length = 180;
1478
- extras.min_length = 1;
1479
- }
1480
- else if (type.kind === "money") {
1481
- extras.description = `Decided amount in ${type.currency} minor units`;
1482
- }
1483
- else if (type.kind === "ref") {
1484
- extras.max_length = 180;
1485
- extras.min_length = 1;
1486
- if (type.target)
1487
- extras.pattern = `^${resolvePrefix?.(type.target) ?? prefixFor(type.target)}_(sandbox|live)_[a-z0-9]{8,64}$`;
1488
- }
1489
- else if (type.kind === "account") {
1490
- extras.pattern = ACCOUNT_PATTERN;
1491
- }
1492
- const scalarEntries = (values) => Object.entries(values).map(([name, value]) => ({
1493
- key: { kind: "ident", name, span: row.span },
1494
- qualifiers: [],
1495
- span: row.span,
1496
- value: typeof value === "number"
1497
- ? { kind: "number", raw: String(value), span: row.span }
1498
- : { kind: "string", value: String(value), span: row.span },
1499
- }));
1500
- // Port inputs require every key; a caller-decided amount may still be zero.
1501
- const field = lowerField({
1502
- ...row,
1503
- value: {
1504
- kind: "block",
1505
- span: row.span,
1506
- entries: [
1507
- {
1508
- key: { kind: "ident", name: "type", span: row.span },
1509
- qualifiers: [],
1510
- span: row.span,
1511
- value: row.value,
1512
- },
1513
- ...(type.kind === "money"
1514
- ? [
1515
- {
1516
- key: { kind: "ident", name: "allow_zero", span: row.span },
1517
- qualifiers: [],
1518
- span: row.span,
1519
- value: { kind: "boolean", value: true, span: row.span },
1520
- },
1521
- ]
1522
- : []),
1523
- ...scalarEntries(extras),
1524
- ],
1525
- },
1526
- }, aliases, diagnostics);
1527
- const schema = type.kind === "text"
1528
- ? {
1529
- type: field.schema.type,
1530
- description: field.schema.description,
1531
- ...field.schema,
1532
- }
1533
- : field.schema;
1534
- return {
1535
- key: row.key,
1536
- qualifiers: [],
1537
- span: row.span,
1538
- value: {
1539
- kind: "block",
1540
- span: row.span,
1541
- entries: scalarEntries(schema),
1542
- },
1543
- };
1544
- }
1545
- function lowerPortShape(port, rawShape, aliases, diagnostics, resolvePrefix) {
1546
- if (rawShape.kind !== "block") {
1547
- diagnostics.push({
1548
- code: "HSX1025",
1549
- fix: "declare shape as a block of fields, like shape { note: text; }",
1550
- message: `decision port ${port.name.name} shape must be a block`,
1551
- severity: "error",
1552
- span: rawShape.span,
1553
- });
1554
- return { entries: [], kind: "block", span: rawShape.span };
1555
- }
1556
- const entries = [];
1557
- const names = new Set();
1558
- for (const row of rawShape.entries) {
1559
- if (names.has(row.key.name)) {
1560
- diagnostics.push({
1561
- code: "HSX1004",
1562
- fix: `keep one ${row.key.name} field in decision port ${port.name.name}`,
1563
- message: `decision port ${port.name.name} shape field ${row.key.name} is declared twice`,
1564
- severity: "error",
1565
- span: row.key.span,
1566
- });
1567
- continue;
1568
- }
1569
- names.add(row.key.name);
1570
- const lowered = lowerPortShapeField(port, row, aliases, diagnostics, resolvePrefix);
1571
- if (lowered)
1572
- entries.push(lowered);
1573
- }
1574
- return {
1575
- entries,
1576
- kind: "block",
1577
- span: rawShape.span,
1578
- };
1579
- }
1580
- function checkClaimAmountPortField(resolvedArgs, ports, aliases, diagnostics) {
1581
- for (const { value } of resolvedArgs) {
1582
- if (value?.kind !== "block")
1583
- continue;
1584
- const fieldEntry = entry(value, "field");
1585
- const boundEntry = entry(value, "bound");
1586
- if (!fieldEntry || !boundEntry)
1587
- continue;
1588
- const claimFieldName = fieldEntry.value.kind === "ident"
1589
- ? fieldEntry.value.name
1590
- : fieldEntry.value.kind === "string"
1591
- ? fieldEntry.value.value
1592
- : undefined;
1593
- const boundFieldName = boundEntry.value.kind === "ident"
1594
- ? boundEntry.value.name
1595
- : boundEntry.value.kind === "string"
1596
- ? boundEntry.value.value
1597
- : undefined;
1598
- if (!claimFieldName || !boundFieldName)
1599
- continue;
1600
- const boundArg = resolvedArgs.find((arg) => (arg.value?.kind === "binding" &&
1601
- arg.value.name.name === boundFieldName) ||
1602
- arg.parameter.name.name === boundFieldName);
1603
- const boundTypeExpr = boundArg?.value?.kind === "binding"
1604
- ? boundArg.value.type
1605
- : boundArg?.value;
1606
- if (!boundTypeExpr)
1607
- continue;
1608
- const boundType = typeOf(boundTypeExpr, aliases);
1609
- if (boundType.kind !== "money")
1610
- continue;
1611
- const portArg = resolvedArgs.find((arg) => arg.parameter.name.name === "claim");
1612
- if (portArg?.value?.kind !== "port_ref")
1613
- continue;
1614
- const portDecl = ports.get(portArg.value.name.name);
1615
- if (!portDecl)
1616
- continue;
1617
- const shapeExpr = entry(portDecl.body, "shape")?.value;
1618
- if (shapeExpr?.kind !== "block")
1619
- continue;
1620
- const shapeField = entry(shapeExpr, claimFieldName);
1621
- if (!shapeField) {
1622
- diagnostics.push({
1623
- code: "HSX1011",
1624
- fix: `add ${claimFieldName}: money(${boundType.currency ?? "CUR"}) to the shape of decision port ${portDecl.name.name}`,
1625
- message: `decision port ${portDecl.name.name} shape lacks claim field ${claimFieldName}`,
1626
- severity: "error",
1627
- span: portArg.value.span,
1628
- });
1629
- continue;
1630
- }
1631
- const actualType = typeOf(shapeField.value, aliases);
1632
- if (actualType.kind !== "money") {
1633
- diagnostics.push({
1634
- code: "HSX1104",
1635
- fix: `pass money<${boundType.currency ?? "CUR"}> to ${shapeField.key.name}`,
1636
- message: `${shapeField.key.name} needs money<${boundType.currency ?? "CUR"}> but receives ${argumentWords(shapeField.value, aliases)}`,
1637
- severity: "error",
1638
- span: shapeField.value.span,
1639
- });
1640
- }
1641
- else if (actualType.currency &&
1642
- boundType.currency &&
1643
- actualType.currency !== boundType.currency) {
1644
- diagnostics.push({
1645
- code: "HSX1101",
1646
- fix: `pass money<${boundType.currency}> to ${shapeField.key.name}`,
1647
- message: `${shapeField.key.name} needs money<${boundType.currency}> but receives money<${actualType.currency}>`,
1648
- severity: "error",
1649
- span: shapeField.value.span,
1650
- });
1651
- }
1652
- }
1653
- }
1654
- function bindPortCompileValues(values, binding, value, ports, diagnostics, aliases = new Map(), loweredPortShapes = new Map(), dependencies = new Map(), resolvePrefix) {
1655
- const port = ports.get(value.name.name);
1656
- if (!port) {
1657
- diagnostics.push({
1658
- code: "HSX1008",
1659
- fix: `declare port ${value.name.name} { allowed: [...]; } or correct the port name`,
1660
- message: `decision port ${value.name.name} is not declared`,
1661
- severity: "error",
1662
- span: value.name.span,
1663
- });
1664
- return;
1665
- }
1666
- for (const key of [binding, `${binding}_allowed`, `${binding}_fields`]) {
1667
- dependencies.set(key, [port.name.name]);
1668
- }
1669
- const allowed = entry(port.body, "allowed")?.value;
1670
- let shape = loweredPortShapes.get(value.name.name);
1671
- if (!shape) {
1672
- const rawShape = entry(port.body, "shape")?.value ?? {
1673
- entries: [],
1674
- kind: "block",
1675
- span: port.body.span,
1676
- };
1677
- const shapeDiagnostics = [];
1678
- shape = lowerPortShape(port, rawShape, aliases, shapeDiagnostics, resolvePrefix);
1679
- for (const diagnostic of shapeDiagnostics) {
1680
- if (!diagnostics.some((prior) => prior.code === diagnostic.code &&
1681
- prior.span === diagnostic.span &&
1682
- prior.message === diagnostic.message))
1683
- diagnostics.push(diagnostic);
1684
- }
1685
- loweredPortShapes.set(value.name.name, shape);
1686
- }
1687
- const normalizedAllowed = allowed?.kind === "ident"
1688
- ? {
1689
- items: [allowed],
1690
- kind: "list",
1691
- span: allowed.span,
1692
- }
1693
- : allowed;
1694
- if (normalizedAllowed)
1695
- values.set(`${binding}_allowed`, normalizedAllowed);
1696
- values.set(`${binding}_fields`, shape);
1697
- if (value.within)
1698
- values.set(`${binding}_within`, value.within);
1699
- if (value.deadline)
1700
- values.set(`${binding}_deadline`, value.deadline);
1701
- }
1702
- function substituteExpr(expr, values, resolvePrefix) {
1703
- if (expr.kind === "ident") {
1704
- return values.get(expr.name) ?? substituteName(expr, values);
1705
- }
1706
- if (expr.kind === "string") {
1707
- return {
1708
- ...expr,
1709
- value: expr.value.replaceAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g, (_, name) => {
1710
- const value = values.get(String(name));
1711
- if (value?.kind === "ident")
1712
- return value.name;
1713
- if (value?.kind === "number")
1714
- return value.raw;
1715
- if (value?.kind === "string")
1716
- return value.value;
1717
- if (value?.kind === "percent")
1718
- return `${value.raw}%`;
1719
- if (value?.kind === "binding")
1720
- return value.name.name;
1721
- if (value?.kind === "port_ref")
1722
- return value.name.name;
1723
- return `{${String(name)}}`;
1724
- }),
1725
- };
1726
- }
1727
- if (expr.kind === "block") {
1728
- return {
1729
- ...expr,
1730
- entries: expr.entries.map((entry) => {
1731
- const nestedValues = entry.iteration
1732
- ? new Map([...values].filter(([name]) => name !== entry.iteration?.binding.name))
1733
- : values;
1734
- return {
1735
- ...entry,
1736
- ...(entry.iteration
1737
- ? {
1738
- iteration: {
1739
- ...entry.iteration,
1740
- bound: substituteExpr(entry.iteration.bound, values, resolvePrefix),
1741
- },
1742
- }
1743
- : {}),
1744
- key: substituteName(entry.key, nestedValues),
1745
- qualifiers: entry.qualifiers.flatMap((qualifier, index) => {
1746
- const value = nestedValues.get(qualifier.name);
1747
- const fixed = fixedQualifier(entry.key.name, qualifier.name, index);
1748
- if (entry.key.name === "on" &&
1749
- !fixed &&
1750
- index > 0 &&
1751
- value?.kind === "list")
1752
- return value.items.map((item) => nameFromValue(qualifier, item));
1753
- return !fixed && value
1754
- ? nameFromValue(qualifier, value)
1755
- : substituteName(qualifier, nestedValues);
1756
- }),
1757
- value: substituteExpr(entry.value, nestedValues, resolvePrefix),
1758
- };
1759
- }),
1760
- };
1761
- }
1762
- if (expr.kind === "list") {
1763
- return {
1764
- ...expr,
1765
- items: expr.items.map((item) => substituteExpr(item, values, resolvePrefix)),
1766
- };
1767
- }
1768
- if (expr.kind === "type_apply") {
1769
- return {
1770
- ...expr,
1771
- args: expr.args.map((arg) => substituteExpr(arg, values, resolvePrefix)),
1772
- };
1773
- }
1774
- if (expr.kind === "call") {
1775
- const hasUnbound = expr.args.some((arg) => hasUnboundCompileIdentifier(arg, values));
1776
- const substituted = {
1777
- ...expr,
1778
- args: expr.args.map((arg) => substituteCompileArgument(arg, values, resolvePrefix)),
1779
- };
1780
- const structural = new Set([
1781
- "at",
1782
- "get",
1783
- "keys",
1784
- "keys_except",
1785
- "kind",
1786
- "len",
1787
- "prefix",
1788
- "values",
1789
- ]).has(expr.callee.name);
1790
- return hasUnbound && !structural
1791
- ? substituted
1792
- : (evaluateCompileTimeCall(substituted, !hasUnbound, resolvePrefix) ??
1793
- substituted);
1794
- }
1795
- if (expr.kind === "apply") {
1796
- return {
1797
- ...expr,
1798
- args: expr.args.map((arg) => substituteExpr(arg, values, resolvePrefix)),
1799
- typeArgs: expr.typeArgs.map((arg) => substituteExpr(arg, values, resolvePrefix)),
1800
- };
1801
- }
1802
- if (expr.kind === "binding") {
1803
- return { ...expr, type: substituteExpr(expr.type, values, resolvePrefix) };
1804
- }
1805
- if (expr.kind === "decided_amount") {
1806
- return {
1807
- ...expr,
1808
- body: substituteExpr(expr.body, values, resolvePrefix),
1809
- };
1810
- }
1811
- return expr;
1812
- }
1813
- function substituteCompileArgument(expr, values, resolvePrefix) {
1814
- if (expr.kind !== "ident")
1815
- return substituteExpr(expr, values, resolvePrefix);
1816
- const value = values.get(expr.name);
1817
- if (!value)
1818
- return expr;
1819
- if (value.kind === "ident" || value.kind === "port_ref") {
1820
- return { kind: "string", span: value.span, value: nameText(value) };
1821
- }
1822
- return value;
1823
- }
1824
- function hasUnboundCompileIdentifier(expr, values) {
1825
- if (expr.kind === "ident")
1826
- return !values.has(expr.name);
1827
- if (expr.kind === "call" || expr.kind === "type_apply") {
1828
- return expr.args.some((arg) => hasUnboundCompileIdentifier(arg, values));
1829
- }
1830
- if (expr.kind === "list") {
1831
- return expr.items.some((item) => hasUnboundCompileIdentifier(item, values));
1832
- }
1833
- return false;
1834
- }
1835
- function optionalInner(type) {
1836
- return type.kind === "type_apply" &&
1837
- type.callee.name === "optional" &&
1838
- type.args.length === 1
1839
- ? type.args[0]
1840
- : undefined;
1841
- }
1842
- function resolveAlias(expr, aliases, seen = new Set()) {
1843
- if (expr.kind !== "ident")
1844
- return expr;
1845
- const alias = aliases.get(expr.name);
1846
- if (!alias || seen.has(expr.name))
1847
- return expr;
1848
- return resolveAlias(alias, aliases, new Set([...seen, expr.name]));
1849
- }
1850
- function argumentMatches(expectedExpression, actualExpression, parties, ports, aliases) {
1851
- const expected = resolveAlias(expectedExpression, aliases);
1852
- const actual = actualExpression.kind === "binding"
1853
- ? resolveAlias(actualExpression.type, aliases)
1854
- : actualExpression;
1855
- if (expected.kind === "type_apply") {
1856
- if (expected.callee.name === "list") {
1857
- const itemType = expected.args[0];
1858
- return Boolean(itemType &&
1859
- actual.kind === "list" &&
1860
- actual.items.every((item) => argumentMatches(itemType, item, parties, ports, aliases)));
1861
- }
1862
- if (expected.callee.name === "optional") {
1863
- const inner = expected.args[0];
1864
- return Boolean(inner && argumentMatches(inner, actual, parties, ports, aliases));
1865
- }
1866
- }
1867
- const expectedType = typeOf(expected, aliases);
1868
- const actualType = typeOf(actual, aliases);
1869
- switch (expectedType.kind) {
1870
- case "money":
1871
- return actualType.kind === "money";
1872
- case "party":
1873
- return actual.kind === "ident";
1874
- case "condition":
1875
- return actual.kind === "port_ref";
1876
- case "text":
1877
- return actual.kind === "string" || actual.kind === "ident";
1878
- case "date":
1879
- return actualType.kind === "date" || actual.kind === "ident";
1880
- case "bps":
1881
- return actualType.kind === "bps" || actual.kind === "percent";
1882
- case "percent":
1883
- return actual.kind === "percent";
1884
- case "integer":
1885
- case "boolean":
1886
- case "account":
1887
- return actualType.kind === expectedType.kind;
1888
- case "ref":
1889
- return (actualType.kind === "ref" || actualExpression.kind === "settlement_ref");
1890
- case "unknown":
1891
- if (expected.kind !== "ident")
1892
- return false;
1893
- if (expected.name === "unknown" || expected.name === "json")
1894
- return true;
1895
- return (expected.name === "block" &&
1896
- (actual.kind === "block" || actual.kind === "decided_amount"));
1897
- }
1898
- }
1899
- function argumentWords(expression, aliases) {
1900
- if (expression.kind === "list")
1901
- return "list";
1902
- if (expression.kind === "block")
1903
- return "block";
1904
- if (expression.kind === "port_ref")
1905
- return "decision port";
1906
- return typeWords(typeOf(expression, aliases));
1907
- }
1908
- function booleanExpr(value, span) {
1909
- return { kind: "boolean", span, value };
1910
- }
1911
- function selectedControls(value) {
1912
- if (value.kind !== "block")
1913
- return [];
1914
- return value.entries.flatMap((row) => [
1915
- ...(row.conditionPorts ?? []),
1916
- ...selectedControls(row.value),
1917
- ]);
1918
- }
1919
- // Retain dependencies before compile-time evaluation erases identifiers.
1920
- function expressionPorts(value, dependencies) {
1921
- if (!value || typeof value !== "object")
1922
- return [];
1923
- if (Array.isArray(value))
1924
- return [
1925
- ...new Set(value.flatMap((item) => expressionPorts(item, dependencies))),
1926
- ];
1927
- const node = value;
1928
- const found = new Set(node.conditionPorts ?? []);
1929
- if (node.kind === "ident" && typeof node.name === "string") {
1930
- for (const port of dependencies.get(node.name.replace(/^\[|\]$/g, "")) ??
1931
- [])
1932
- found.add(port);
1933
- }
1934
- if (node.kind === "string" && typeof node.value === "string") {
1935
- for (const match of node.value.matchAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g)) {
1936
- for (const port of dependencies.get(match[1]) ?? [])
1937
- found.add(port);
1938
- }
1939
- }
1940
- for (const [key, child] of Object.entries(node)) {
1941
- if (key === "span" || key === "conditionPorts")
1942
- continue;
1943
- for (const port of expressionPorts(child, dependencies))
1944
- found.add(port);
1945
- }
1946
- return [...found];
1947
- }
1948
- function selectCompileTimeRows(block, values, diagnostics, dependencies = new Map(), controls = [], resolvePrefix) {
1949
- const entries = [];
1950
- const locals = new Map(values);
1951
- const localDependencies = new Map(dependencies);
1952
- for (const row of block.entries) {
1953
- if (row.key.name === "let") {
1954
- const binding = row.qualifiers[0];
1955
- if (binding) {
1956
- localDependencies.set(binding.name, expressionPorts(row.value, localDependencies));
1957
- locals.set(binding.name, substituteExpr(row.value, locals, resolvePrefix));
1958
- }
1959
- entries.push(row);
1960
- continue;
1961
- }
1962
- if (row.key.name === "unsupported") {
1963
- const value = substituteExpr(row.value, locals, resolvePrefix);
1964
- if (value.kind !== "block") {
1965
- diagnostics.push({
1966
- code: "HSX1405",
1967
- fix: "give unsupported a block with code, message, and fix rows",
1968
- message: "unsupported needs a fixed diagnostic block",
1969
- severity: "error",
1970
- span: row.span,
1971
- });
1972
- continue;
1973
- }
1974
- const diagnosticValue = (name) => {
1975
- const entry = value.entries.find((candidate) => candidate.key.name === name);
1976
- return entry ? nameText(entry.value) : undefined;
1977
- };
1978
- diagnostics.push({
1979
- code: diagnosticValue("code") ?? "HSX1110",
1980
- fix: diagnosticValue("fix") ??
1981
- "choose a supported compile-time parameter combination",
1982
- message: diagnosticValue("message") ??
1983
- "this compile-time parameter combination is not supported",
1984
- severity: "error",
1985
- span: row.span,
1986
- });
1987
- continue;
1988
- }
1989
- if (row.key.name !== "when" &&
1990
- row.key.name !== "when_not" &&
1991
- row.key.name !== "when_eq") {
1992
- const value = row.value.kind === "block" && !row.iteration
1993
- ? selectCompileTimeRows(row.value, locals, diagnostics, localDependencies, controls, resolvePrefix)
1994
- : row.value;
1995
- const conditionPorts = row.key.name === "action"
1996
- ? [
1997
- ...new Set([
1998
- ...(value.kind === "block" && entry(value, "due")
1999
- ? []
2000
- : controls),
2001
- ...row.qualifiers
2002
- .filter((qualifier) => qualifier.name.startsWith("["))
2003
- .flatMap((qualifier) => expressionPorts(qualifier, localDependencies)),
2004
- ...expressionPorts(value.kind === "block"
2005
- ? entry(value, "port")?.value
2006
- : undefined, localDependencies),
2007
- ...expressionPorts(value.kind === "block"
2008
- ? entry(value, "input")?.value
2009
- : undefined, localDependencies),
2010
- ...expressionPorts(value.kind === "block"
2011
- ? entry(value, "capture_input", "captureInput")?.value
2012
- : undefined, localDependencies),
2013
- ...(value.kind === "block" && entry(value, "due")
2014
- ? []
2015
- : selectedControls(value)),
2016
- ]),
2017
- ]
2018
- : controls;
2019
- entries.push({ ...row, conditionPorts, value });
2020
- continue;
2021
- }
2022
- const subject = row.qualifiers[0];
2023
- const actual = subject ? locals.get(subject.name) : undefined;
2024
- let include = false;
2025
- if (row.key.name === "when_eq") {
2026
- const expected = row.qualifiers[1];
2027
- include = Boolean(actual && expected && nameText(actual) === expected.name);
2028
- }
2029
- else {
2030
- include = compileTimeTruthy(actual);
2031
- if (row.key.name === "when_not")
2032
- include = !include;
2033
- }
2034
- if (row.value.kind !== "block") {
2035
- diagnostics.push({
2036
- code: "HSX1405",
2037
- fix: "give the compile-time condition a body in braces",
2038
- message: `${row.key.name} needs a fixed block to select`,
2039
- severity: "error",
2040
- span: row.span,
2041
- });
2042
- continue;
2043
- }
2044
- if (include) {
2045
- entries.push(...selectCompileTimeRows(row.value, locals, diagnostics, localDependencies, [
2046
- ...new Set([
2047
- ...controls,
2048
- ...(subject ? expressionPorts(subject, localDependencies) : []),
2049
- ]),
2050
- ], resolvePrefix).entries);
2051
- }
2052
- }
2053
- return { ...block, entries };
2054
- }
2055
- function compileTimeTruthy(value) {
2056
- if (!value)
2057
- return false;
2058
- if (value.kind === "string" && value.value === NONE_SENTINEL)
2059
- return false;
2060
- if (value.kind === "boolean")
2061
- return value.value;
2062
- if (value.kind === "number")
2063
- return Number(value.raw) !== 0;
2064
- if (value.kind === "list")
2065
- return value.items.length > 0;
2066
- if (value.kind === "block")
2067
- return value.entries.length > 0;
2068
- return true;
2069
- }
2070
- function evaluateCompileTimeCall(expr, resolveMissing = true, resolvePrefix) {
2071
- const [first, second, third, fourth] = expr.args;
2072
- const number = (value) => {
2073
- if (value?.kind === "percent")
2074
- return value.bps;
2075
- if (value?.kind !== "number" || value.raw.includes("."))
2076
- return undefined;
2077
- const parsed = Number(value.raw);
2078
- return Number.isSafeInteger(parsed) ? parsed : undefined;
2079
- };
2080
- const numeric = (value) => ({
2081
- kind: "number",
2082
- raw: String(value),
2083
- span: expr.span,
2084
- });
2085
- const text = (value) => ({
2086
- kind: "string",
2087
- span: expr.span,
2088
- value,
2089
- });
2090
- const a = number(first);
2091
- const b = number(second);
2092
- switch (expr.callee.name) {
2093
- case "add":
2094
- return a === undefined || b === undefined ? undefined : numeric(a + b);
2095
- case "sub":
2096
- return a === undefined || b === undefined ? undefined : numeric(a - b);
2097
- case "mul":
2098
- return a === undefined || b === undefined ? undefined : numeric(a * b);
2099
- case "div_floor":
2100
- return a === undefined || b === undefined || b === 0
2101
- ? undefined
2102
- : numeric(Math.floor(a / b));
2103
- case "mod":
2104
- return a === undefined || b === undefined || b === 0
2105
- ? undefined
2106
- : numeric(a % b);
2107
- case "min":
2108
- return a === undefined || b === undefined
2109
- ? undefined
2110
- : numeric(Math.min(a, b));
2111
- case "lt":
2112
- return a === undefined || b === undefined
2113
- ? undefined
2114
- : numeric(a < b ? 1 : 0);
2115
- case "len":
2116
- return first?.kind === "list"
2117
- ? numeric(first.items.length)
2118
- : first?.kind === "block"
2119
- ? numeric(first.entries.length)
2120
- : first?.kind === "boolean" && !first.value
2121
- ? numeric(0)
2122
- : undefined;
2123
- case "at": {
2124
- const index = number(second);
2125
- if (index === undefined)
2126
- return undefined;
2127
- if (first?.kind === "list")
2128
- return first.items[index - 1];
2129
- if (first?.kind === "block")
2130
- return first.entries[index - 1]?.value;
2131
- return undefined;
2132
- }
2133
- case "get": {
2134
- const key = second ? nameText(second) : "";
2135
- if (!key || key === "invalid_compile_time_name")
2136
- return undefined;
2137
- const body = first?.kind === "decided_amount" ? first.body : first;
2138
- if (body?.kind !== "block") {
2139
- return resolveMissing
2140
- ? { kind: "string", span: expr.span, value: NONE_SENTINEL }
2141
- : undefined;
2142
- }
2143
- const value = body.entries.find((row) => row.key.name === key)?.value;
2144
- if (value || !resolveMissing)
2145
- return value;
2146
- return { kind: "string", span: expr.span, value: NONE_SENTINEL };
2147
- }
2148
- case "keys":
2149
- return first?.kind === "block" || first?.kind === "decided_amount"
2150
- ? {
2151
- items: (first.kind === "decided_amount"
2152
- ? first.body
2153
- : first).entries.map((row) => ({ ...row.key })),
2154
- kind: "list",
2155
- span: expr.span,
2156
- }
2157
- : undefined;
2158
- case "values":
2159
- return first?.kind === "block"
2160
- ? {
2161
- items: first.entries.map((row) => row.value),
2162
- kind: "list",
2163
- span: expr.span,
2164
- }
2165
- : undefined;
2166
- case "keys_except": {
2167
- if (first?.kind !== "block")
2168
- return undefined;
2169
- const omitted = new Set(expr.args.slice(1).map(nameText));
2170
- return {
2171
- items: first.entries
2172
- .filter((row) => !omitted.has(row.key.name))
2173
- .map((row) => ({ ...row.key })),
2174
- kind: "list",
2175
- span: expr.span,
2176
- };
2177
- }
2178
- case "kind":
2179
- return first
2180
- ? text(first.kind === "string" && first.value === NONE_SENTINEL
2181
- ? "none"
2182
- : first.kind)
2183
- : undefined;
2184
- case "basis_points":
2185
- return first?.kind === "percent" ? numeric(first.bps) : undefined;
2186
- case "shift_date": {
2187
- const instant = first ? Date.parse(nameText(first)) : NaN;
2188
- const offset = second ? fixedIsoDurationMs(nameText(second)) : null;
2189
- if (!Number.isFinite(instant) || offset === null)
2190
- return text(NONE_SENTINEL);
2191
- const direction = third && nameText(third) === "before" ? -1 : 1;
2192
- const shifted = new Date(instant + direction * offset);
2193
- return Number.isFinite(shifted.getTime())
2194
- ? text(shifted.toISOString())
2195
- : text(NONE_SENTINEL);
2196
- }
2197
- case "names": {
2198
- const count = number(second);
2199
- if (!first || count === undefined || count < 0 || !third)
2200
- return undefined;
2201
- return {
2202
- items: Array.from({ length: count }, (_, index) => text(`${nameText(first)}${index + 1}${nameText(third)}`)),
2203
- kind: "list",
2204
- span: expr.span,
2205
- };
2206
- }
2207
- case "suffix_each":
2208
- return first?.kind === "list" && second
2209
- ? {
2210
- items: first.items.map((item) => text(`${camel(nameText(item))}${nameText(second)}`)),
2211
- kind: "list",
2212
- span: expr.span,
2213
- }
2214
- : undefined;
2215
- case "without":
2216
- return first?.kind === "list" && second
2217
- ? {
2218
- ...first,
2219
- items: first.items.filter((item) => nameText(item) !== nameText(second)),
2220
- }
2221
- : undefined;
2222
- case "concat_lists":
2223
- return expr.args.every((arg) => arg.kind === "list")
2224
- ? {
2225
- items: expr.args.flatMap((arg) => arg.kind === "list" ? arg.items : []),
2226
- kind: "list",
2227
- span: expr.span,
2228
- }
2229
- : undefined;
2230
- case "concat":
2231
- return text(expr.args.map(nameText).join(""));
2232
- case "camel":
2233
- return first ? text(camel(nameText(first))) : undefined;
2234
- case "title":
2235
- return first ? text(titleize(nameText(first))) : undefined;
2236
- case "sentence":
2237
- return first ? text(sentenceCase(nameText(first))) : undefined;
2238
- case "percent_text": {
2239
- const basisPoints = number(first);
2240
- if (basisPoints === undefined)
2241
- return undefined;
2242
- const whole = Math.trunc(basisPoints / 100);
2243
- const fraction = basisPoints % 100;
2244
- const decimal = fraction === 0
2245
- ? ""
2246
- : fraction % 10 === 0
2247
- ? `.${fraction / 10}`
2248
- : `.${String(fraction).padStart(2, "0")}`;
2249
- return text(`${whole}${decimal}%`);
2250
- }
2251
- case "words":
2252
- return first ? text(nameText(first).replaceAll("_", " ")) : undefined;
2253
- case "prefix":
2254
- return first
2255
- ? text(resolvePrefix?.(nameText(first)) ?? prefixFor(nameText(first)))
2256
- : undefined;
2257
- case "owner":
2258
- return first?.kind === "settlement_ref"
2259
- ? text(first.owner.name)
2260
- : undefined;
2261
- case "member":
2262
- return first?.kind === "settlement_ref"
2263
- ? text(first.member.name)
2264
- : undefined;
2265
- case "scale_duration": {
2266
- if (!first || b === undefined)
2267
- return undefined;
2268
- const match = /^P([0-9]+)([DWMY])$/.exec(nameText(first));
2269
- if (!match)
2270
- return undefined;
2271
- const amount = Number(match[1]) * b;
2272
- return match[2] === "W"
2273
- ? text(`P${amount * 7}D`)
2274
- : text(`P${amount}${match[2]}`);
2275
- }
2276
- case "if_eq":
2277
- return first && second && nameText(first) === nameText(second)
2278
- ? third
2279
- : fourth;
2280
- default:
2281
- return undefined;
2282
- }
2283
- }
2284
- function substituteName(name, values) {
2285
- return {
2286
- ...name,
2287
- name: name.name.replaceAll(/\[([A-Za-z][A-Za-z0-9_]*)\]/g, (whole, key) => {
2288
- const value = values.get(String(key));
2289
- return value ? nameText(value) : String(whole);
2290
- }),
2291
- };
2292
- }
2293
- function fixedQualifier(key, qualifier, index) {
2294
- if (["action", "when", "when_not", "when_eq"].includes(key))
2295
- return true;
2296
- if (key === "on" && index === 0)
2297
- return true;
2298
- const normalizedKey = key.replaceAll("_", " ");
2299
- return udlClauseVocabulary.some((definition) => definition.spelling === `${normalizedKey} ${qualifier}` ||
2300
- definition.spelling ===
2301
- `${normalizedKey} ${qualifier.replaceAll("_", " ")}`);
2302
- }
2303
- function nameFromValue(origin, value) {
2304
- return { ...origin, name: nameText(value) };
2305
- }
2306
- function nameText(value) {
2307
- if (value.kind === "binding")
2308
- return value.name.name;
2309
- if (value.kind === "ident")
2310
- return value.name;
2311
- if (value.kind === "number")
2312
- return value.raw;
2313
- if (value.kind === "string")
2314
- return value.value;
2315
- if (value.kind === "percent")
2316
- return `${value.raw}%`;
2317
- if (value.kind === "port_ref")
2318
- return value.name.name;
2319
- if (value.kind === "settlement_ref")
2320
- return `${value.owner.name}.${value.member.name}`;
2321
- return "invalid_compile_time_name";
2322
- }
2323
- function expandComprehensions(block, diagnostics, values = new Map(), dependencies = new Map(), resolvePrefix) {
2324
- const budget = { used: 0 };
2325
- const expand = (current, inherited = new Map(), inheritedDependencies = dependencies) => {
2326
- const entries = [];
2327
- const locals = new Map(inherited);
2328
- const localDependencies = new Map(inheritedDependencies);
2329
- for (const row of current.entries) {
2330
- if (row.key.name === "let") {
2331
- const binding = row.qualifiers[0];
2332
- if (!binding) {
2333
- diagnostics.push({
2334
- code: "HSX1405",
2335
- fix: "write `let(name): <compile-time value>`",
2336
- message: "a compile-time binding needs one name",
2337
- severity: "error",
2338
- span: row.span,
2339
- });
2340
- continue;
2341
- }
2342
- localDependencies.set(binding.name, expressionPorts(row.value, localDependencies));
2343
- locals.set(binding.name, substituteExpr(row.value, locals, resolvePrefix));
2344
- continue;
2345
- }
2346
- if (row.key.name === "when" ||
2347
- row.key.name === "when_not" ||
2348
- row.key.name === "when_eq") {
2349
- const subject = row.qualifiers[0];
2350
- const actual = subject ? locals.get(subject.name) : undefined;
2351
- const expected = row.qualifiers[1];
2352
- let include = row.key.name === "when_eq"
2353
- ? Boolean(actual && expected && nameText(actual) === expected.name)
2354
- : compileTimeTruthy(actual);
2355
- if (row.key.name === "when_not")
2356
- include = !include;
2357
- if (include && row.value.kind === "block") {
2358
- entries.push(...expand(row.value, locals, localDependencies).entries);
2359
- }
2360
- continue;
2361
- }
2362
- if (!row.iteration) {
2363
- const substituted = {
2364
- ...row,
2365
- key: substituteName(row.key, locals),
2366
- qualifiers: row.qualifiers.flatMap((qualifier, index) => {
2367
- const value = locals.get(qualifier.name);
2368
- const fixed = fixedQualifier(row.key.name, qualifier.name, index);
2369
- if (row.key.name === "on" &&
2370
- !fixed &&
2371
- index > 0 &&
2372
- value?.kind === "list")
2373
- return value.items.map((item) => nameFromValue(qualifier, item));
2374
- return !fixed && value
2375
- ? nameFromValue(qualifier, value)
2376
- : substituteName(qualifier, locals);
2377
- }),
2378
- value: substituteExpr(row.value, locals, resolvePrefix),
2379
- };
2380
- entries.push({
2381
- ...substituted,
2382
- value: row.value.kind === "block"
2383
- ? expand(row.value, locals, localDependencies)
2384
- : substituted.value,
2385
- });
2386
- continue;
2387
- }
2388
- if (row.value.kind !== "block")
2389
- continue;
2390
- const bound = substituteExpr(row.iteration.bound, locals, resolvePrefix);
2391
- const values = finiteIterationValues(bound, resolvePrefix);
2392
- if (!values) {
2393
- diagnostics.push({
2394
- code: "HSX1403",
2395
- fix: "pass an integer literal or a literal finite list as the comprehension bound",
2396
- message: `comprehension bound for ${row.iteration.binding.name} is not known at compile time`,
2397
- severity: "error",
2398
- span: bound.span,
2399
- });
2400
- continue;
2401
- }
2402
- if (budget.used + values.length > HSX_LIMITS.maxExpansions) {
2403
- diagnostics.push({
2404
- code: "HSX1404",
2405
- fix: `reduce the fixed expansion to at most ${HSX_LIMITS.maxExpansions} rows`,
2406
- message: `bounded comprehensions expand past the ${HSX_LIMITS.maxExpansions}-row compiler limit`,
2407
- severity: "error",
2408
- span: bound.span,
2409
- });
2410
- continue;
2411
- }
2412
- budget.used += values.length;
2413
- for (const value of values) {
2414
- const substitutions = new Map([
2415
- ...locals,
2416
- [row.iteration.binding.name, value],
2417
- ]);
2418
- const body = selectCompileTimeRows(row.value, substitutions, diagnostics, localDependencies, row.conditionPorts, resolvePrefix);
2419
- entries.push(...expand(body, substitutions, localDependencies).entries);
2420
- }
2421
- }
2422
- return { ...current, entries };
2423
- };
2424
- return expand(block, values);
2425
- }
2426
- function finiteIterationValues(bound, resolvePrefix) {
2427
- if (bound.kind === "call") {
2428
- const evaluated = evaluateCompileTimeCall(bound, true, resolvePrefix);
2429
- return evaluated
2430
- ? finiteIterationValues(evaluated, resolvePrefix)
2431
- : undefined;
2432
- }
2433
- if (bound.kind === "list")
2434
- return bound.items;
2435
- if (bound.kind !== "number" || bound.raw.includes("."))
2436
- return undefined;
2437
- const count = Number(bound.raw);
2438
- if (!Number.isSafeInteger(count) || count < 0)
2439
- return undefined;
2440
- return Array.from({ length: count }, (_, index) => ({
2441
- kind: "number",
2442
- raw: String(index + 1),
2443
- span: bound.span,
2444
- }));
2445
- }
2446
- function checkInstrument(name, body, parties, ports, aliases, diagnostics, resolvePrefix) {
2447
- if (!SNAKE_CASE.test(name.name)) {
2448
- diagnostics.push({
2449
- code: "HSX1003",
2450
- fix: "rename the instrument with lowercase words joined by underscores",
2451
- message: `instrument ${name.name} is not snake_case`,
2452
- severity: "error",
2453
- span: name.span,
2454
- });
2455
- }
2456
- const fieldsEntry = entry(body, "fields");
2457
- const fields = fieldsEntry?.value.kind === "block"
2458
- ? checkFields(fieldsEntry.value, aliases, diagnostics, resolvePrefix)
2459
- : [];
2460
- if (!fieldsEntry) {
2461
- diagnostics.push({
2462
- code: "HSX1503",
2463
- fix: "add a fields { ... } block, even when it is empty",
2464
- message: `instrument ${name.name} has no fields block`,
2465
- severity: "error",
2466
- span: name.span,
2467
- });
2468
- }
2469
- const fieldNames = new Set(fields.map((field) => field.name));
2470
- if (fields.some((field) => field.type.kind === "money") &&
2471
- !fieldNames.has("currency")) {
2472
- fields.push({
2473
- name: "currency",
2474
- origin: fieldsEntry?.span ?? name.span,
2475
- required: true,
2476
- schema: {
2477
- description: "ISO 4217 currency code",
2478
- maxLength: 3,
2479
- minLength: 3,
2480
- pattern: "^[A-Z]{3}$",
2481
- type: "string",
2482
- },
2483
- type: { kind: "text" },
2484
- });
2485
- fieldNames.add("currency");
2486
- }
2487
- const endpointFields = new Map();
2488
- for (const field of fields) {
2489
- if (field.type.kind === "account")
2490
- endpointFields.set(field.name, field.name);
2491
- }
2492
- const slots = {};
2493
- const partiesEntry = entry(body, "parties");
2494
- const roleObject = blockToObject(partiesEntry?.value.kind === "block" ? partiesEntry.value : undefined, new Map(), diagnostics);
2495
- const partyAccounts = new Map([...parties].map((party) => [party, `${camel(party)}AccountId`]));
2496
- for (const [role, binding] of Object.entries(roleObject)) {
2497
- if (!isJsonObject(binding))
2498
- continue;
2499
- if (typeof binding.party !== "string" ||
2500
- !parties.has(binding.party) ||
2501
- typeof binding.account !== "string" ||
2502
- !endpointFields.has(binding.account)) {
2503
- diagnostics.push({
2504
- code: "HSX1024",
2505
- fix: "bind a declared party to a declared account field",
2506
- message: `party role ${role} has an invalid party/account binding`,
2507
- severity: "error",
2508
- span: partiesEntry?.span ?? name.span,
2509
- });
2510
- continue;
2511
- }
2512
- partyAccounts.set(binding.party, binding.account);
2513
- roleObject[role] = binding.account;
2514
- }
2515
- const decisionRoles = new Map();
2516
- const inferredDecisionFields = new Set();
2517
- // Decision authority needs an identity binding, even when no money moves
2518
- // through that party. Reuse the same account binding as economic roles.
2519
- for (const action of body.entries) {
2520
- if (action.key.name !== "action" || action.value.kind !== "block")
2521
- continue;
2522
- const port = entry(action.value, "port")?.value;
2523
- if (port?.kind !== "block")
2524
- continue;
2525
- const allowed = entry(port, "allowed_parties", "allowedParties")?.value;
2526
- if (allowed?.kind !== "list")
2527
- continue;
2528
- for (const item of allowed.items) {
2529
- const party = nameText(item);
2530
- if (!parties.has(party))
2531
- continue;
2532
- const accountField = partyAccounts.get(party);
2533
- const existing = Object.entries(roleObject).find(([, value]) => value === party || value === accountField);
2534
- let role = existing?.[0] ?? party;
2535
- for (let suffix = 2; !existing && Object.hasOwn(roleObject, role); suffix++)
2536
- role = `${party}_${suffix}`;
2537
- if (!existing) {
2538
- roleObject[role] = party;
2539
- inferredDecisionFields.add(accountField);
2540
- }
2541
- decisionRoles.set(party, role);
2542
- }
2543
- }
2544
- if (Object.keys(roleObject).length > 0) {
2545
- for (const [role, value] of Object.entries(roleObject)) {
2546
- if (typeof value !== "string")
2547
- continue;
2548
- if (fieldNames.has(value)) {
2549
- endpointFields.set(role, value);
2550
- endpointFields.set(value, value);
2551
- continue;
2552
- }
2553
- if (parties.has(value)) {
2554
- const field = `${camel(value)}AccountId`;
2555
- roleObject[role] = field;
2556
- endpointFields.set(role, field);
2557
- endpointFields.set(value, field);
2558
- if (!fieldNames.has(field)) {
2559
- fields.push({
2560
- name: field,
2561
- origin: partiesEntry?.span ?? name.span,
2562
- required: true,
2563
- schema: {
2564
- description: `The ${value.replaceAll("_", " ")} account`,
2565
- pattern: ACCOUNT_PATTERN,
2566
- type: "string",
2567
- "x-hyperscale-reference-filter": {
2568
- column: "role",
2569
- values: ["customer_balance"],
2570
- },
2571
- },
2572
- type: { kind: "account" },
2573
- });
2574
- fieldNames.add(field);
2575
- }
2576
- continue;
2577
- }
2578
- diagnostics.push({
2579
- code: "HSX1001",
2580
- fix: "bind the role to a declared party or field",
2581
- message: `party role refers to ${String(value)}, which is not declared`,
2582
- severity: "error",
2583
- span: (partiesEntry?.value.kind === "block"
2584
- ? partiesEntry.value.entries
2585
- : []).find((entry) => entry.key.name === role)?.value.span ??
2586
- partiesEntry?.span ??
2587
- name.span,
2588
- });
2589
- }
2590
- slots.parties = roleObject;
2591
- }
2592
- const actionEntries = body.entries.filter((candidate) => candidate.key.name === "action" && candidate.value.kind === "block");
2593
- const actionNames = new Set(actionEntries.map((candidate) => candidate.qualifiers[0]?.name ?? ""));
2594
- const holdDestinations = new Map();
2595
- for (const actionEntry of actionEntries) {
2596
- if (actionEntry.value.kind !== "block")
2597
- continue;
2598
- for (const move of friendlyMoveRows(actionEntry.value)) {
2599
- if (move.operation === "post" && move.from && move.to) {
2600
- const destination = endpointFields.get(move.to);
2601
- if (destination)
2602
- holdDestinations.set(move.from, destination);
2603
- }
2604
- }
2605
- }
2606
- for (const actionEntry of actionEntries) {
2607
- if (actionEntry.value.kind !== "block")
2608
- continue;
2609
- for (const move of friendlyMoveRows(actionEntry.value)) {
2610
- for (const endpoint of [move.from, move.to]) {
2611
- if (!endpoint ||
2612
- endpoint.startsWith("refs.") ||
2613
- endpointFields.has(endpoint) ||
2614
- holdDestinations.has(endpoint)) {
2615
- continue;
2616
- }
2617
- const field = `${camel(endpoint)}AccountId`;
2618
- endpointFields.set(endpoint, field);
2619
- if (!fieldNames.has(field)) {
2620
- fields.push({
2621
- name: field,
2622
- origin: actionEntry.span,
2623
- required: true,
2624
- schema: {
2625
- pattern: ACCOUNT_PATTERN,
2626
- type: "string",
2627
- },
2628
- type: { kind: "account" },
2629
- });
2630
- fieldNames.add(field);
2631
- }
2632
- }
2633
- }
2634
- }
2635
- const actions = actionEntries
2636
- .flatMap((candidate) => {
2637
- const actionName = candidate.qualifiers[0];
2638
- if (!actionName || candidate.value.kind !== "block")
2639
- return [];
2640
- return [
2641
- checkAction(actionName, candidate.value, fields, name.name, endpointFields, holdDestinations, diagnostics, resolvePrefix),
2642
- ];
2643
- })
2644
- .map((action) => {
2645
- const port = action.slots.port;
2646
- if (port === undefined ||
2647
- !isJsonObject(port) ||
2648
- !Array.isArray(port.allowedParties))
2649
- return action;
2650
- return {
2651
- ...action,
2652
- slots: {
2653
- ...action.slots,
2654
- port: {
2655
- ...port,
2656
- allowedParties: port.allowedParties.map((party) => typeof party === "string"
2657
- ? (decisionRoles.get(party) ?? party)
2658
- : party),
2659
- },
2660
- },
2661
- };
2662
- })
2663
- .map((action) => pinFixedCurrencyBindings(action, fields, diagnostics));
2664
- for (const action of actions) {
2665
- for (const declared of ports.get(action.name) ?? []) {
2666
- const allowed = entry(declared.body, "allowed")?.value;
2667
- const allowedItems = allowed?.kind === "list" ? allowed.items : allowed ? [allowed] : [];
2668
- if (allowedItems.length === 0) {
2669
- diagnostics.push({
2670
- code: "HSX1024",
2671
- fix: `declare a nonempty allowed list of declared parties on port ${declared.name.name}`,
2672
- message: `wired decision port ${declared.name.name} reaches action ${action.name} without declared allowed parties`,
2673
- severity: "error",
2674
- span: declared.name.span,
2675
- });
2676
- continue;
2677
- }
2678
- const port = action.slots.port;
2679
- if (port === undefined ||
2680
- !isJsonObject(port) ||
2681
- !Array.isArray(port.allowedParties)) {
2682
- diagnostics.push({
2683
- code: "HSX1024",
2684
- fix: `emit the allowed parties of ${declared.name.name} in action ${action.name}'s port`,
2685
- message: `wired decision port ${declared.name.name} reaches action ${action.name} without a decision port`,
2686
- severity: "error",
2687
- span: action.origin,
2688
- });
2689
- continue;
2690
- }
2691
- const allowedFields = new Set();
2692
- for (const item of allowedItems) {
2693
- const party = nameText(item);
2694
- const role = decisionRoles.get(party) ??
2695
- Object.keys(roleObject).find((role) => roleObject[role] ===
2696
- (partyAccounts.get(party) ?? `${camel(party)}AccountId`));
2697
- const field = role === undefined ? undefined : roleObject[role];
2698
- if (typeof field === "string") {
2699
- allowedFields.add(field);
2700
- continue;
2701
- }
2702
- if (!parties.has(party))
2703
- continue;
2704
- diagnostics.push({
2705
- code: "HSX1024",
2706
- fix: `bind ${party} to an account field and use that binding in action ${action.name}'s port`,
2707
- message: `decision port ${action.name} allows party ${party}, but settlement ${name.name} does not bind that party to the decision action`,
2708
- severity: "error",
2709
- span: item.span,
2710
- });
2711
- }
2712
- if (port.allowedParties.length === 0 ||
2713
- allowedFields.size !==
2714
- new Set(port.allowedParties.map((role) => String(roleObject[String(role)]))).size ||
2715
- port.allowedParties.some((role) => typeof role !== "string" ||
2716
- !allowedFields.has(String(roleObject[role])))) {
2717
- diagnostics.push({
2718
- code: "HSX1024",
2719
- fix: `use only the account bindings allowed by ${declared.name.name}`,
2720
- message: `action ${action.name} does not preserve the allowed parties of decision port ${declared.name.name}`,
2721
- severity: "error",
2722
- span: action.origin,
2723
- });
2724
- }
2725
- }
2726
- }
2727
- if (!actionNames.has("create")) {
2728
- diagnostics.push({
2729
- code: "HSX1504",
2730
- fix: 'add action create { summary: "Create ..."; steps: [] }',
2731
- message: `instrument ${name.name} has no create action`,
2732
- severity: "error",
2733
- span: name.span,
2734
- });
2735
- }
2736
- const lifecycleEntry = entry(body, "lifecycle");
2737
- const lifecycle = lifecycleEntry?.value.kind === "block"
2738
- ? checkLifecycle(name.name, lifecycleEntry.value, actionNames, diagnostics)
2739
- : undefined;
2740
- if (!lifecycle) {
2741
- diagnostics.push({
2742
- code: "HSX1402",
2743
- fix: "add lifecycle { states ...; initial ...; on ...; }",
2744
- message: `instrument ${name.name} has no valid lifecycle`,
2745
- severity: "error",
2746
- span: name.span,
2747
- });
2748
- }
2749
- for (const row of body.entries) {
2750
- if (row.key.name === "fields" ||
2751
- row.key.name === "lifecycle" ||
2752
- row.key.name === "parties" ||
2753
- row.key.name === "action")
2754
- continue;
2755
- if (row.key.name === "required") {
2756
- const explicit = exprToJsonForUdlSlot(row.value, "required", diagnostics);
2757
- const checked = checkRequiredFields(explicit, fields, row.span, diagnostics);
2758
- if (checked)
2759
- slots.required = checked;
2760
- continue;
2761
- }
2762
- const spelling = clauseSpelling(row, instrumentClauseBySpelling);
2763
- const definition = instrumentClauseBySpelling.get(spelling);
2764
- if (definition?.scope === "instrument") {
2765
- setClause(slots, definition.target, definition.cardinality, exprToJsonForUdlSlot(row.value, definition.target, diagnostics), definition.spelling, row.span, diagnostics);
2766
- continue;
2767
- }
2768
- if (!definition && row.key.name !== "required") {
2769
- diagnostics.push({
2770
- code: "HSX1501",
2771
- fix: "use a clause exported by the targeted UDL definition set",
2772
- message: `instrument clause ${spelling} does not exist in the targeted UDL vocabulary`,
2773
- severity: "error",
2774
- span: row.key.span,
2775
- });
2776
- }
2777
- }
2778
- if (lifecycle) {
2779
- slots.lifecycle = lifecycle.value;
2780
- if (Object.keys(lifecycle.parked).length > 0) {
2781
- slots.callerParkedStates = lifecycle.parked;
2782
- }
2783
- }
2784
- slots.idPrefix ??=
2785
- stringSlot(body, "idPrefix", "id_prefix") ?? prefixFor(name.name);
2786
- slots.summary ??= `${titleize(name.name)} instrument`;
2787
- slots.title ??= titleize(name.name);
2788
- linearityDiagnostics(actionEntries, diagnostics);
2789
- agentDescriptionDiagnostics(name, slots, actions, diagnostics);
2790
- if (Array.isArray(slots.required)) {
2791
- slots.required = [
2792
- ...new Set([...slots.required, ...inferredDecisionFields]),
2793
- ];
2794
- }
2795
- return derivePieceInputs({ actions, fields, id: name.name, origin: name.span, slots }, diagnostics);
2796
- }
2797
- // Mirrors the catalog law agent_description_required: every action a caller
2798
- // can reach becomes an agent tool whose description is authored, never
2799
- // synthesized from the human-facing summary. System actions with a due clause
2800
- // are exempt because no caller can reach them.
2801
- function agentDescriptionDiagnostics(name, slots, actions, diagnostics) {
2802
- const callable = actions.filter((action) => action.slots.due === undefined);
2803
- if (callable.length === 0)
2804
- return;
2805
- if (slots.agentDescription === undefined) {
2806
- diagnostics.push({
2807
- code: "HSX1509",
2808
- fix: 'add agent_description: "..." telling an agent when to reach for this instrument',
2809
- message: `instrument ${name.name} exposes callable actions without an agent_description`,
2810
- severity: "error",
2811
- span: name.span,
2812
- });
2813
- }
2814
- for (const action of callable) {
2815
- if (action.slots.agentDescription !== undefined)
2816
- continue;
2817
- diagnostics.push({
2818
- code: "HSX1509",
2819
- fix: 'add agent_description: "..." telling an agent when to call this action',
2820
- message: `action ${action.name} of ${name.name} is callable without an agent_description`,
2821
- severity: "error",
2822
- span: action.origin,
2823
- });
2824
- }
2825
- }
2826
- function checkRequiredFields(value, fields, span, diagnostics) {
2827
- const report = (message, fix) => {
2828
- diagnostics.push({
2829
- code: "HSX1023",
2830
- fix,
2831
- message,
2832
- severity: "error",
2833
- span,
2834
- });
2835
- };
2836
- if (!Array.isArray(value)) {
2837
- report("required must be a list of field names", "write required as a list, such as [amount, currency]");
2838
- return undefined;
2839
- }
2840
- const fieldByName = new Map(fields.map((field) => [field.name, field]));
2841
- const seen = new Set();
2842
- let valid = true;
2843
- for (const item of value) {
2844
- if (typeof item !== "string") {
2845
- valid = false;
2846
- report("required contains a value that is not a field name", "keep only declared non-optional field names in required");
2847
- continue;
2848
- }
2849
- if (seen.has(item)) {
2850
- valid = false;
2851
- report(`required names ${item} more than once`, `keep one ${item} entry in required`);
2852
- continue;
2853
- }
2854
- seen.add(item);
2855
- const field = fieldByName.get(item);
2856
- if (!field) {
2857
- valid = false;
2858
- report(`required names unknown field ${item}`, "name a declared non-optional field in required");
2859
- continue;
2860
- }
2861
- if (!field.required) {
2862
- valid = false;
2863
- report(`required names optional field ${item}`, `remove ${item} from required or make the field non-optional`);
2864
- }
2865
- }
2866
- return valid ? value : undefined;
2867
- }
2868
- function checkFields(block, aliases, diagnostics, resolvePrefix) {
2869
- const result = [];
2870
- const names = new Set();
2871
- for (const row of block.entries) {
2872
- const name = camel(row.key.name);
2873
- if (!CAMEL_CASE.test(name)) {
2874
- diagnostics.push({
2875
- code: "HSX1005",
2876
- fix: "rename the field in lower camelCase",
2877
- message: `field ${row.key.name} is not lower camelCase`,
2878
- severity: "error",
2879
- span: row.key.span,
2880
- });
2881
- }
2882
- if (names.has(name)) {
2883
- diagnostics.push({
2884
- code: "HSX1004",
2885
- fix: `keep one ${name} field`,
2886
- message: `field ${name} is declared twice`,
2887
- severity: "error",
2888
- span: row.key.span,
2889
- });
2890
- continue;
2891
- }
2892
- names.add(name);
2893
- result.push(lowerField(row, aliases, diagnostics, resolvePrefix));
2894
- }
2895
- return result;
2896
- }
2897
- function lowerField(row, aliases, diagnostics, resolvePrefix) {
2898
- const name = camel(row.key.name);
2899
- let typeExpr = row.value;
2900
- let description;
2901
- let required = true;
2902
- let extra = {};
2903
- let allowZero = false;
2904
- if (row.value.kind === "block") {
2905
- const typeRow = entry(row.value, "type");
2906
- if (typeRow)
2907
- typeExpr = typeRow.value;
2908
- const desc = entry(row.value, "description") ?? entry(row.value, "desc");
2909
- if (desc?.value.kind === "string")
2910
- description = desc.value.value;
2911
- const optional = entry(row.value, "optional");
2912
- if (optional?.value.kind === "boolean")
2913
- required = !optional.value.value;
2914
- const zero = entry(row.value, "allow_zero");
2915
- if (zero?.value.kind === "boolean")
2916
- allowZero = zero.value.value;
2917
- extra = blockToObject({
2918
- ...row.value,
2919
- entries: row.value.entries.filter((item) => !["type", "description", "desc", "optional", "allow_zero"].includes(item.key.name)),
2920
- }, new Map(), diagnostics, false, resolvePrefix);
2921
- }
2922
- const type = typeOf(typeExpr, aliases);
2923
- // Diagnostics point at the field's own type expression, not at an alias
2924
- // declaration that may live in an imported module.
2925
- const declaredSpan = typeExpr.span;
2926
- if (allowZero && type.kind !== "money") {
2927
- diagnostics.push({
2928
- code: "HSX1105",
2929
- fix: `declare ${name} as money(CUR) or remove allow_zero`,
2930
- message: `field ${name} sets allow_zero but is ${typeWords(type)}, not money`,
2931
- severity: "error",
2932
- span: declaredSpan,
2933
- });
2934
- }
2935
- const seen = new Set();
2936
- while (typeExpr.kind === "ident" &&
2937
- aliases.has(typeExpr.name) &&
2938
- !seen.has(typeExpr.name)) {
2939
- seen.add(typeExpr.name);
2940
- typeExpr = aliases.get(typeExpr.name);
2941
- }
2942
- const fixedMoneyIssue = invalidFixedMoneyBinding(typeExpr);
2943
- if (fixedMoneyIssue) {
2944
- diagnostics.push({
2945
- code: "HSX1103",
2946
- fix: `write ${name} as money(SAR, 2500) with a positive safe integer in minor units`,
2947
- message: `field ${name} ${fixedMoneyIssue}`,
2948
- severity: "error",
2949
- span: declaredSpan,
2950
- });
2951
- }
2952
- if ((typeExpr.kind === "ident" && typeExpr.name === "ref") ||
2953
- (type.kind === "ref" && !type.target)) {
2954
- diagnostics.push({
2955
- code: "HSX1007",
2956
- fix: "declare the target instrument as ref<instrument_id>",
2957
- message: `field ${name} declares ref without an instrument target`,
2958
- severity: "error",
2959
- span: declaredSpan,
2960
- });
2961
- }
2962
- else if (type.kind === "ref" && type.target) {
2963
- const prefix = resolvePrefix?.(type.target);
2964
- if (!extra.pattern) {
2965
- if (prefix) {
2966
- extra.pattern = `^${prefix}_(sandbox|live)_[a-z0-9]{8,64}$`;
2967
- }
2968
- else if (resolvePrefix !== undefined) {
2969
- diagnostics.push({
2970
- code: "HSX1007",
2971
- fix: "reference an instrument declared in this program, imported from std, or published in the catalogue",
2972
- message: `field ${name} references unknown instrument ${type.target}`,
2973
- severity: "error",
2974
- span: declaredSpan,
2975
- });
2976
- }
2977
- }
2978
- }
2979
- const structuralType = Object.hasOwn(extra, "items")
2980
- ? "array"
2981
- : Object.hasOwn(extra, "properties") ||
2982
- Object.hasOwn(extra, "additionalProperties")
2983
- ? "object"
2984
- : undefined;
2985
- const schema = {
2986
- ...schemaFor(type, allowZero),
2987
- ...extra,
2988
- ...(structuralType ? { type: structuralType } : {}),
2989
- ...(description ? { description } : {}),
2990
- };
2991
- return { name, origin: row.span, required, schema, type };
2992
- }
2993
- function checkLifecycle(instrument, block, actions, diagnostics) {
2994
- const statesRows = block.entries.filter((row) => row.key.name === "states");
2995
- const initialRow = entry(block, "initial");
2996
- const states = statesRows.flatMap((row) => row.value.kind === "list"
2997
- ? row.value.items.flatMap((item) => item.kind === "ident"
2998
- ? [item.name]
2999
- : item.kind === "string"
3000
- ? [item.value]
3001
- : [])
3002
- : []);
3003
- const initial = initialRow?.value.kind === "ident"
3004
- ? initialRow.value.name
3005
- : initialRow?.value.kind === "string"
3006
- ? initialRow.value.value
3007
- : "";
3008
- if (states.length === 0 || !initial)
3009
- return undefined;
3010
- const stateSet = new Set(states);
3011
- if (!stateSet.has(initial)) {
3012
- diagnostics.push({
3013
- code: "HSX1001",
3014
- fix: `add ${initial} to the states row or choose a declared initial state`,
3015
- message: `lifecycle initial state ${initial} is not declared by ${instrument}`,
3016
- severity: "error",
3017
- span: initialRow?.span ?? block.span,
3018
- });
3019
- }
3020
- const transitions = {};
3021
- const parked = {};
3022
- for (const row of block.entries) {
3023
- if (row.key.name === "parked") {
3024
- const state = row.qualifiers[0]?.name;
3025
- if (state && row.value.kind === "string")
3026
- parked[state] = row.value.value;
3027
- continue;
3028
- }
3029
- if (row.key.name !== "on")
3030
- continue;
3031
- const action = row.qualifiers[0]?.name ?? "";
3032
- const from = row.qualifiers.slice(1).map((item) => item.name);
3033
- const to = row.value.kind === "ident"
3034
- ? row.value.name
3035
- : row.value.kind === "string"
3036
- ? row.value.value
3037
- : "";
3038
- if (!actions.has(action)) {
3039
- diagnostics.push({
3040
- code: "HSX1001",
3041
- fix: `declare action ${action} { ... } or remove its transition`,
3042
- message: `lifecycle transition names action ${action}, which ${instrument} does not declare`,
3043
- severity: "error",
3044
- span: row.span,
3045
- });
3046
- }
3047
- for (const state of [...from, to]) {
3048
- if (stateSet.has(state))
3049
- continue;
3050
- diagnostics.push({
3051
- code: "HSX1001",
3052
- fix: `add ${state} to states or correct the transition`,
3053
- message: `lifecycle transition ${action} refers to undeclared state ${state}`,
3054
- severity: "error",
3055
- span: row.span,
3056
- });
3057
- }
3058
- transitions[action] = { from, to };
3059
- }
3060
- return { parked, value: { initial, states, transitions } };
3061
- }
3062
- function checkAction(name, body, fields, instrumentId, endpointFields, holdDestinations, diagnostics, resolvePrefix) {
3063
- const slots = {};
3064
- let omitsPublicAction = false;
3065
- const fieldNames = new Set(fields.map((field) => field.name));
3066
- for (const row of body.entries) {
3067
- if (["for", "while", "loop", "recurse"].includes(row.key.name)) {
3068
- diagnostics.push({
3069
- code: "HSX1401",
3070
- fix: "use a bounded comprehension over list<T, N> or a declared finite schedule",
3071
- message: `${row.key.name} is unbounded; every HSX action must terminate at compile time`,
3072
- severity: "error",
3073
- span: row.key.span,
3074
- });
3075
- continue;
3076
- }
3077
- if (row.key.name === "public" &&
3078
- row.qualifiers.length === 0 &&
3079
- row.value.kind === "ident" &&
3080
- row.value.name === "none") {
3081
- omitsPublicAction = true;
3082
- continue;
3083
- }
3084
- const spelling = clauseSpelling(row, actionClauseBySpelling);
3085
- const definition = actionClauseBySpelling.get(spelling);
3086
- if (definition?.scope === "action" && spelling === "notify") {
3087
- const role = row.qualifiers[0]?.name;
3088
- const channel = row.qualifiers[1]?.name;
3089
- if (role && channel) {
3090
- const notification = { channel, role };
3091
- setClause(slots, definition.target, definition.cardinality, notification, definition.spelling, row.span, diagnostics);
3092
- }
3093
- continue;
3094
- }
3095
- if (definition?.scope === "action") {
3096
- if (definition.target === "port" && row.value.kind === "block") {
3097
- const allowedEntry = entry(row.value, "allowed");
3098
- if (allowedEntry) {
3099
- diagnostics.push({
3100
- code: "HSX1508",
3101
- fix: "Use allowed_parties: [...] inside an action-level port clause; allowed: is for top-level port declarations.",
3102
- message: "action-level port clause uses allowed: instead of allowed_parties:",
3103
- severity: "error",
3104
- span: allowedEntry.key.span,
3105
- });
3106
- }
3107
- }
3108
- const rawValue = exprToJsonForUdlSlot(row.value, definition.target, diagnostics, resolvePrefix);
3109
- const moveOffset = Array.isArray(slots.moves) ? slots.moves.length : 0;
3110
- const value = definition.target === "moves"
3111
- ? normalizeMoves(rawValue, name.name, instrumentId, fields, endpointFields, holdDestinations, diagnostics, row.span, moveOffset)
3112
- : rawValue;
3113
- setClause(slots, definition.target, definition.cardinality, value, definition.spelling, row.span, diagnostics);
3114
- continue;
3115
- }
3116
- diagnostics.push({
3117
- code: "HSX1501",
3118
- fix: "use a clause exported by the targeted UDL definition set",
3119
- message: `action clause ${spelling} does not exist in the targeted UDL vocabulary`,
3120
- severity: "error",
3121
- span: row.key.span,
3122
- });
3123
- }
3124
- slots.summary ??= `${titleize(name.name)} ${fields.length === 1 ? (fields[0]?.name ?? "instrument") : "instrument"}`;
3125
- slots.steps ??= [];
3126
- slots.moves ??= [];
3127
- if (omitsPublicAction && slots.publicAction !== undefined) {
3128
- diagnostics.push({
3129
- code: "HSX1506",
3130
- fix: "keep either `public: none` or `public action: actionName`",
3131
- message: `action ${name.name} both omits and names its public action`,
3132
- severity: "error",
3133
- span: name.span,
3134
- });
3135
- }
3136
- if (!omitsPublicAction &&
3137
- slots.publicAction === undefined &&
3138
- slots.due === undefined &&
3139
- slots.reconcile === undefined) {
3140
- slots.publicAction = camel(`${name.name}_${instrumentId}`);
3141
- }
3142
- bindFieldReferences(slots, fieldNames, name, diagnostics);
3143
- const effects = deriveUdlActionEffects(slots, udlClauseVocabulary);
3144
- if (Object.keys(effects).length > 0) {
3145
- slots.effects = effects;
3146
- }
3147
- else {
3148
- delete slots.effects;
3149
- }
3150
- return { effects, name: name.name, origin: name.span, slots };
3151
- }
3152
- function normalizeMoves(value, action, instrumentId, fields, endpointFields, holdDestinations, diagnostics, span, offset = 0) {
3153
- if (!Array.isArray(value))
3154
- return value;
3155
- const fieldByName = new Map(fields.map((field) => [field.name, field]));
3156
- return value.map((candidate, index) => {
3157
- if (!isJsonObject(candidate) || candidate.bind !== undefined)
3158
- return candidate;
3159
- const amount = typeof candidate.amount === "string" ? candidate.amount : undefined;
3160
- const from = typeof candidate.from === "string" ? candidate.from : undefined;
3161
- const to = typeof candidate.to === "string" ? candidate.to : undefined;
3162
- const operationName = typeof candidate.operation === "string" ? candidate.operation : "create";
3163
- if (!amount || !from || !to)
3164
- return candidate;
3165
- const operation = `internal_transfer.${operationName}`;
3166
- const key = typeof candidate.key === "string"
3167
- ? candidate.key
3168
- : `${action}_${offset + index + 1}`;
3169
- if (operationName === "post" || operationName === "void") {
3170
- return {
3171
- bind: {
3172
- transferId: { from: "instance", path: "refs.reservationId" },
3173
- },
3174
- key,
3175
- operation,
3176
- };
3177
- }
3178
- const sourcePath = from.startsWith("refs.")
3179
- ? from
3180
- : endpointFields.get(from)
3181
- ? `fields.${endpointFields.get(from)}`
3182
- : undefined;
3183
- const destinationEndpoint = operationName === "reserve"
3184
- ? (holdDestinations.get(to) ?? endpointFields.get(to))
3185
- : endpointFields.get(to);
3186
- const destinationPath = to.startsWith("refs.")
3187
- ? to
3188
- : destinationEndpoint
3189
- ? `fields.${destinationEndpoint}`
3190
- : undefined;
3191
- if (!sourcePath || !destinationPath) {
3192
- const missing = !sourcePath ? from : to;
3193
- diagnostics.push({
3194
- code: "HSX1001",
3195
- fix: `bind ${missing} in the instrument parties block`,
3196
- message: `action ${action} moves money through ${missing}, which has no bound account field`,
3197
- severity: "error",
3198
- span,
3199
- });
3200
- return candidate;
3201
- }
3202
- const amountPath = amount.includes(".") ? amount : `fields.${amount}`;
3203
- const move = {
3204
- bind: {
3205
- amount: { from: "instance", path: amountPath },
3206
- currency: { from: "instance", path: "fields.currency" },
3207
- destinationAccountId: {
3208
- from: "instance",
3209
- path: destinationPath,
3210
- },
3211
- sourceAccountId: {
3212
- from: "instance",
3213
- path: sourcePath,
3214
- },
3215
- "metadata.instrumentId": { from: "const", value: instrumentId },
3216
- "metadata.instrumentInstanceId": {
3217
- from: "instance",
3218
- path: "instrumentInstanceId",
3219
- },
3220
- "metadata.phase": { from: "const", value: action },
3221
- productId: { from: "instance", path: "productId" },
3222
- },
3223
- key,
3224
- operation,
3225
- };
3226
- if (operationName === "reserve") {
3227
- move.capture = {
3228
- [camel(`${action}_${key}_transfer_id`)]: "transferId",
3229
- };
3230
- }
3231
- else if (operationName === "create") {
3232
- move.capture = {
3233
- [camel(`${action}_${key}_transfer_id`)]: "transferId",
3234
- };
3235
- }
3236
- if (!amountPath.startsWith("refs.") &&
3237
- !fieldByName.has(amountPath.replace(/^fields\./, ""))) {
3238
- diagnostics.push({
3239
- code: "HSX1001",
3240
- fix: `declare the money field named by ${amount}`,
3241
- message: `action ${action} moves ${amount}, which is not a declared money field`,
3242
- severity: "error",
3243
- span,
3244
- });
3245
- }
3246
- return move;
3247
- });
3248
- }
3249
- function friendlyMoveRows(body) {
3250
- const moves = entry(body, "moves")?.value;
3251
- if (moves?.kind !== "list")
3252
- return [];
3253
- return moves.items.flatMap((item) => {
3254
- if (item.kind !== "block")
3255
- return [];
3256
- const from = stringSlot(item, "from");
3257
- const operation = stringSlot(item, "operation");
3258
- const to = stringSlot(item, "to");
3259
- return [
3260
- {
3261
- ...(from ? { from } : {}),
3262
- ...(operation ? { operation } : {}),
3263
- ...(to ? { to } : {}),
3264
- },
3265
- ];
3266
- });
3267
- }
3268
- function isJsonObject(value) {
3269
- return value !== null && typeof value === "object" && !Array.isArray(value);
3270
- }
3271
- /**
3272
- * A money<SAR> field moves SAR, whatever the caller wrote in the instance
3273
- * currency field. Every move of a fixed-currency amount, and every account
3274
- * step of an instrument whose money is fixed, binds that currency as a
3275
- * constant: a binding that reads one of the instrument's own text fields is
3276
- * pinned here, and one the compiler cannot pin (an input, a ref, or a
3277
- * constant naming another currency) is refused as HSX1306.
3278
- */
3279
- function pinFixedCurrencyBindings(action, fields, diagnostics) {
3280
- const fieldByName = new Map(fields.map((field) => [field.name, field]));
3281
- const fixedCurrencyOf = (field) => field?.type.kind === "money" &&
3282
- field.type.currency &&
3283
- CURRENCY.test(field.type.currency)
3284
- ? field.type.currency
3285
- : undefined;
3286
- const fixed = new Set(fields.flatMap((field) => fixedCurrencyOf(field) ?? []));
3287
- // Two fixed currencies is HSX1305's refusal; one decides here, none skips.
3288
- const ledger = fixed.size === 1 ? [...fixed][0] : undefined;
3289
- if (!ledger)
3290
- return action;
3291
- const instanceField = (binding) => binding !== undefined &&
3292
- isJsonObject(binding) &&
3293
- binding.from === "instance" &&
3294
- typeof binding.path === "string" &&
3295
- binding.path.startsWith("fields.")
3296
- ? fieldByName.get(binding.path.slice("fields.".length))
3297
- : undefined;
3298
- const pin = (entry, currency, subject) => {
3299
- if (!isJsonObject(entry))
3300
- return entry;
3301
- const bind = entry.bind;
3302
- if (bind === undefined || !isJsonObject(bind))
3303
- return entry;
3304
- const binding = bind.currency;
3305
- if (binding === undefined)
3306
- return entry;
3307
- const bound = isJsonObject(binding) && binding.from === "const"
3308
- ? String(binding.value)
3309
- : undefined;
3310
- if (bound === currency)
3311
- return entry;
3312
- if (bound === undefined && instanceField(binding)?.type.kind === "text") {
3313
- return {
3314
- ...entry,
3315
- bind: { ...bind, currency: { from: "const", value: currency } },
3316
- };
3317
- }
3318
- diagnostics.push({
3319
- code: "HSX1306",
3320
- fix: `bind currency as { from: const; value: ${currency}; }`,
3321
- message: `action ${action.name} binds the currency of ${subject} to ${bound ?? "a caller-supplied value"}; a money<${currency}> amount moves in ${currency}`,
3322
- severity: "error",
3323
- span: action.origin,
3324
- });
3325
- return entry;
3326
- };
3327
- const moves = action.slots.moves ?? [];
3328
- const steps = action.slots.steps ?? [];
3329
- return {
3330
- ...action,
3331
- slots: {
3332
- ...action.slots,
3333
- moves: Array.isArray(moves)
3334
- ? moves.map((move) => {
3335
- if (!isJsonObject(move))
3336
- return move;
3337
- // A post settles a reservation; the ledger refuses a post whose
3338
- // currency differs from the reserve, which is pinned here.
3339
- if (move.operation === "internal_transfer.post")
3340
- return move;
3341
- const bind = move.bind;
3342
- const amount = bind !== undefined && isJsonObject(bind)
3343
- ? instanceField(bind.amount)
3344
- : undefined;
3345
- return pin(move, fixedCurrencyOf(amount) ?? ledger, amount ? `fields.${amount.name}` : "the moved amount");
3346
- })
3347
- : moves,
3348
- steps: Array.isArray(steps)
3349
- ? steps.map((step) => isJsonObject(step) &&
3350
- typeof step.operation === "string" &&
3351
- step.operation.startsWith("account.")
3352
- ? pin(step, ledger, `the ${step.operation} account`)
3353
- : step)
3354
- : steps,
3355
- },
3356
- };
3357
- }
3358
- function bindFieldReferences(slots, fields, action, diagnostics) {
3359
- const externalFieldSlot = (path) => {
3360
- const key = path.at(-1);
3361
- const input = slots.input !== undefined && isJsonObject(slots.input)
3362
- ? slots.input
3363
- : undefined;
3364
- const allocation = slots.allocate !== undefined && isJsonObject(slots.allocate)
3365
- ? slots.allocate
3366
- : undefined;
3367
- const allocationOperand = path.includes("allocate") &&
3368
- ["amountField", "sourceAccountField", "paymentIdentityField"].includes(key ?? "");
3369
- return ((allocationOperand &&
3370
- Array.isArray(input?.required) &&
3371
- allocation !== undefined &&
3372
- input.required.includes(allocation[key])) ||
3373
- (path.includes("funding") &&
3374
- [
3375
- "ticketRefField",
3376
- "ticketAmountField",
3377
- "ticketInvestorField",
3378
- "ticketAccountField",
3379
- ].includes(key ?? "")) ||
3380
- (path.includes("requiresExposure") && key === "minimumField") ||
3381
- (path.includes("requiresAggregate") && key === "refField") ||
3382
- (path.includes("requiresAggregate") &&
3383
- ["amountField", "dateField", "positionField"].includes(key ?? "")) ||
3384
- (path.includes("requiresAggregate") &&
3385
- path.includes("dueBefore") &&
3386
- key === "field") ||
3387
- (path.includes("signedSum") && key === "refField") ||
3388
- (path.includes("signedSum") && key === "amountField") ||
3389
- (path.includes("remainder") && key === "refField") ||
3390
- (path.includes("remainder") && key === "amountField") ||
3391
- (path.includes("requiresExposure") && key === "capField") ||
3392
- (path.includes("reconcile") &&
3393
- path.includes("exception") &&
3394
- ["amountField", "reasonField", "refField"].includes(key ?? "")) ||
3395
- (path.includes("cascade") &&
3396
- ["inputField", "refField"].includes(key ?? "")));
3397
- };
3398
- const visit = (value, path = []) => {
3399
- if (Array.isArray(value)) {
3400
- value.forEach((item, index) => visit(item, [...path, String(index)]));
3401
- return;
3402
- }
3403
- if (value && typeof value === "object") {
3404
- Object.entries(value).forEach(([childKey, child]) => visit(child, [...path, childKey]));
3405
- return;
3406
- }
3407
- const key = path.at(-1) ?? "";
3408
- if (typeof value === "string" &&
3409
- key.endsWith("Field") &&
3410
- !externalFieldSlot(path) &&
3411
- !fields.has(value)) {
3412
- diagnostics.push({
3413
- code: "HSX1001",
3414
- fix: `declare field ${value} or correct ${key}`,
3415
- message: `action ${action.name} refers to field ${value}, which does not exist`,
3416
- severity: "error",
3417
- span: action.span,
3418
- });
3419
- }
3420
- };
3421
- visit(slots);
3422
- }
3423
- function crossInstrumentReferenceDiagnostics(instruments, diagnostics, knownTargets) {
3424
- const byId = new Map(instruments.map((instrument) => [instrument.id, instrument]));
3425
- const report = (owner, span, message, fix) => {
3426
- diagnostics.push({
3427
- code: "HSX1007",
3428
- fix,
3429
- message: `${owner.id}: ${message}`,
3430
- severity: "error",
3431
- span,
3432
- });
3433
- };
3434
- const fieldIn = (instrument, name) => instrument.fields.find((field) => field.name === name);
3435
- const statesOf = (instrument) => {
3436
- const lifecycle = jsonObject(instrument.slots.lifecycle);
3437
- return new Set(Array.isArray(lifecycle?.states)
3438
- ? lifecycle.states.filter((state) => typeof state === "string")
3439
- : []);
3440
- };
3441
- for (const owner of instruments) {
3442
- for (const field of owner.fields) {
3443
- if (field.type.kind !== "ref" || !field.type.target)
3444
- continue;
3445
- if (!byId.has(field.type.target) &&
3446
- !knownTargets?.has(field.type.target)) {
3447
- 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`);
3448
- }
3449
- }
3450
- for (const invariant of jsonObjects(owner.slots.aggregateInvariants)) {
3451
- const childId = invariant.childInstrumentId;
3452
- if (typeof childId !== "string")
3453
- continue;
3454
- const child = byId.get(childId);
3455
- if (!child) {
3456
- report(owner, owner.origin, `aggregate invariant refers to instrument ${childId}, which does not exist`, `declare instrument ${childId} or correct childInstrumentId`);
3457
- continue;
3458
- }
3459
- const childRefField = invariant.childRefField;
3460
- if (typeof childRefField === "string") {
3461
- const field = fieldIn(child, childRefField);
3462
- if (!field ||
3463
- field.type.kind !== "ref" ||
3464
- (field.type.target !== undefined && field.type.target !== owner.id)) {
3465
- report(owner, owner.origin, `${childId}.${childRefField} is not ref<${owner.id}>`, `declare ${childId}.${childRefField} as ref<${owner.id}> or correct childRefField`);
3466
- }
3467
- }
3468
- const parentField = invariant.parentField;
3469
- const parent = typeof parentField === "string"
3470
- ? fieldIn(owner, parentField)
3471
- : undefined;
3472
- const childField = invariant.childField;
3473
- const childFieldDef = typeof childField === "string" ? fieldIn(child, childField) : undefined;
3474
- const isIntegerSum = invariant.count !== true &&
3475
- parent?.type.kind === "integer" &&
3476
- childFieldDef?.type.kind === "integer";
3477
- if (typeof childField === "string" &&
3478
- !isIntegerSum &&
3479
- childFieldDef?.type.kind !== "money") {
3480
- report(owner, owner.origin, `${childId}.${childField} is not declared money`, `declare ${childId}.${childField} as money<C> or correct childField`);
3481
- }
3482
- const childStates = statesOf(child);
3483
- for (const status of Array.isArray(invariant.childStatuses)
3484
- ? invariant.childStatuses
3485
- : []) {
3486
- if (typeof status === "string" && !childStates.has(status)) {
3487
- report(owner, owner.origin, `${childId} has no lifecycle state ${status}`, `add ${status} to ${childId} or correct childStatuses`);
3488
- }
3489
- }
3490
- if (typeof parentField === "string" &&
3491
- (!parent ||
3492
- (invariant.count === true || isIntegerSum
3493
- ? parent.type.kind !== "integer"
3494
- : parent.type.kind !== "money"))) {
3495
- const expected = invariant.count === true || isIntegerSum ? "integer" : "money<C>";
3496
- report(owner, owner.origin, `${owner.id}.${parentField} is not declared ${expected}`, `declare ${owner.id}.${parentField} as ${expected} or correct parentField`);
3497
- }
3498
- }
3499
- const checkRelation = (action, relation, targetKey, checkAmountField = true) => {
3500
- const targetId = relation[targetKey];
3501
- if (typeof targetId !== "string")
3502
- return;
3503
- const target = byId.get(targetId);
3504
- if (!target) {
3505
- report(owner, action.origin, `action ${action.name} refers to instrument ${targetId}, which does not exist`, `declare instrument ${targetId} or correct ${targetKey}`);
3506
- return;
3507
- }
3508
- const refField = relation.refField;
3509
- const anchorName = relation.anchorField ??
3510
- (relation.over === "siblings" ? refField : undefined);
3511
- const anchor = typeof anchorName === "string" ? fieldIn(owner, anchorName) : undefined;
3512
- const expectedTarget = anchor?.type.kind === "ref" ? anchor.type.target : owner.id;
3513
- if (typeof anchorName === "string" &&
3514
- (!anchor || anchor.type.kind !== "ref"))
3515
- report(owner, action.origin, `aggregate anchor ${anchorName} is not a reference`, "declare the anchor as a typed parent reference");
3516
- if (typeof refField === "string") {
3517
- const field = fieldIn(target, refField);
3518
- if (!field) {
3519
- report(owner, action.origin, `${targetId}.${refField} does not exist`, `declare ${refField} on ${targetId} as ref<${expectedTarget}> or correct the clause`);
3520
- }
3521
- else if (field.type.kind !== "ref" ||
3522
- (field.type.target !== undefined &&
3523
- field.type.target !== expectedTarget)) {
3524
- report(owner, action.origin, `${targetId}.${refField} has ${typeWords(field.type)}; this relation needs ref<${expectedTarget}>`, `change ${targetId}.${refField} to ref<${expectedTarget}>`);
3525
- }
3526
- }
3527
- const check = jsonObject(relation.check);
3528
- const amountField = relation.amountField ?? check?.amountField;
3529
- if (checkAmountField &&
3530
- typeof amountField === "string" &&
3531
- relation.path !== "refs") {
3532
- const field = fieldIn(target, amountField);
3533
- if (!field || field.type.kind !== "money") {
3534
- report(owner, action.origin, `${targetId}.${amountField} is not declared money`, `declare ${targetId}.${amountField} as money<C> or correct the clause`);
3535
- }
3536
- }
3537
- const targetField = check?.targetField;
3538
- if (typeof targetField === "string" &&
3539
- fieldIn(owner, targetField)?.type.kind !==
3540
- (check?.kind === "count_at_least" ? "integer" : "money")) {
3541
- report(owner, action.origin, `${owner.id}.${targetField} is not declared ${check?.kind === "count_at_least" ? "integer" : "money"}`, `declare ${owner.id}.${targetField} as money<C> or correct the aggregate target`);
3542
- }
3543
- const statuses = relation.statuses;
3544
- if (Array.isArray(statuses)) {
3545
- const targetStates = statesOf(target);
3546
- for (const status of statuses) {
3547
- if (typeof status === "string" && !targetStates.has(status)) {
3548
- report(owner, action.origin, `${targetId} has no lifecycle state ${status}`, `add ${status} to ${targetId} or correct the clause status`);
3549
- }
3550
- }
3551
- }
3552
- };
3553
- for (const action of owner.actions) {
3554
- for (const gate of jsonObjects(action.slots.requiresRefs)) {
3555
- const fieldName = gate.field;
3556
- if (typeof fieldName !== "string")
3557
- continue;
3558
- const field = fieldIn(owner, fieldName);
3559
- if (!field || field.type.kind !== "ref" || !field.type.target)
3560
- continue;
3561
- const target = byId.get(field.type.target);
3562
- if (!target)
3563
- continue;
3564
- const statuses = Array.isArray(gate.statuses) ? gate.statuses : [];
3565
- const targetStates = statesOf(target);
3566
- for (const status of statuses) {
3567
- if (typeof status === "string" && !targetStates.has(status)) {
3568
- report(owner, action.origin, `${field.type.target} has no lifecycle state ${status}`, `correct requires refs statuses for ${fieldName}`);
3569
- }
3570
- }
3571
- }
3572
- for (const relation of jsonObjects(action.slots.requiresAggregate)) {
3573
- checkRelation(action, relation, "instrumentId");
3574
- }
3575
- for (const relation of jsonObjects(action.slots.requiresExposure)) {
3576
- const childId = relation.childInstrumentId;
3577
- if (typeof childId === "string" && childId !== owner.id) {
3578
- report(owner, action.origin, `requires exposure names child ${childId}, but the action belongs to ${owner.id}`, `set childInstrumentId to ${owner.id}`);
3579
- }
3580
- const amountField = relation.amountField;
3581
- if (typeof amountField === "string" &&
3582
- fieldIn(owner, amountField)?.type.kind !== "money") {
3583
- report(owner, action.origin, `${owner.id}.${amountField} is not declared money`, `declare ${amountField} as money<C> or correct requires exposure`);
3584
- }
3585
- const anchorField = relation.anchorField;
3586
- const anchor = typeof anchorField === "string"
3587
- ? fieldIn(owner, anchorField)
3588
- : undefined;
3589
- if (typeof anchorField === "string" &&
3590
- (!anchor || anchor.type.kind !== "ref" || !anchor.type.target)) {
3591
- report(owner, action.origin, `${owner.id}.${anchorField} is not a typed cross-instrument ref`, `declare ${anchorField} as ref<parent_instrument>`);
3592
- }
3593
- else if (anchor?.type.target) {
3594
- const parent = byId.get(anchor.type.target);
3595
- const capField = relation.capField;
3596
- if (parent &&
3597
- typeof capField === "string" &&
3598
- fieldIn(parent, capField)?.type.kind !== "money") {
3599
- report(owner, action.origin, `${parent.id}.${capField} is not declared money`, `declare ${parent.id}.${capField} as money<C> or correct requires exposure`);
3600
- }
3601
- }
3602
- }
3603
- const signedSum = jsonObject(action.slots.signedSum);
3604
- for (const relation of jsonObjects(signedSum?.sources)) {
3605
- checkRelation(action, relation, "instrumentId");
3606
- }
3607
- const remainder = jsonObject(action.slots.remainder);
3608
- for (const relation of jsonObjects(remainder?.collected)) {
3609
- checkRelation(action, relation, "instrumentId");
3610
- }
3611
- for (const reconcile of jsonObjects(action.slots.reconcile)) {
3612
- const exception = jsonObject(reconcile.exception);
3613
- if (!exception)
3614
- continue;
3615
- checkRelation(action, exception, "childInstrumentId", false);
3616
- const childId = exception.childInstrumentId;
3617
- if (typeof childId !== "string")
3618
- continue;
3619
- const child = byId.get(childId);
3620
- if (!child)
3621
- continue;
3622
- const required = new Set(requiredFields(child));
3623
- for (const [key, expectedKind, expectedWords] of [
3624
- ["amountField", "money", "money<C>"],
3625
- ["reasonField", "text", "text"],
3626
- ]) {
3627
- const fieldName = exception[key];
3628
- if (typeof fieldName !== "string")
3629
- continue;
3630
- const field = fieldIn(child, fieldName);
3631
- if (!field) {
3632
- report(owner, action.origin, `reconcile exception ${key} names ${childId}.${fieldName}, which does not exist`, `declare ${childId}.${fieldName} as ${expectedWords} or correct ${key}`);
3633
- }
3634
- else if (field.type.kind !== expectedKind) {
3635
- 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}`);
3636
- }
3637
- else if (!field.required || !required.has(fieldName)) {
3638
- 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}`);
3639
- }
3640
- else if (key === "reasonField") {
3641
- const constraint = field.schema.pattern !== undefined
3642
- ? "pattern"
3643
- : field.schema.format !== undefined
3644
- ? "format"
3645
- : field.schema.enum !== undefined
3646
- ? "enum"
3647
- : field.schema.const !== undefined
3648
- ? "const"
3649
- : undefined;
3650
- if (constraint) {
3651
- const article = constraint === "enum" ? "an" : "a";
3652
- 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`);
3653
- }
3654
- }
3655
- }
3656
- }
3657
- }
3658
- }
3659
- }
3660
- function jsonObject(value) {
3661
- return value !== undefined && isJsonObject(value) ? value : undefined;
3662
- }
3663
- function jsonObjects(value) {
3664
- if (Array.isArray(value))
3665
- return value.filter(isJsonObject);
3666
- const object = jsonObject(value);
3667
- return object ? [object] : [];
3668
- }
3669
- function linearityDiagnostics(actionEntries, diagnostics) {
3670
- const clauses = actionEntries.flatMap((action) => {
3671
- if (action.value.kind !== "block")
3672
- return [];
3673
- return action.value.entries.flatMap((row) => {
3674
- const spelling = clauseSpelling(row, actionClauseBySpelling);
3675
- const definition = actionClauseBySpelling.get(spelling);
3676
- return definition?.scope === "action"
3677
- ? [{ definition, row, spelling }]
3678
- : [];
3679
- });
3680
- });
3681
- const sinks = clauses.filter(({ definition }) => "linearSink" in definition && definition.linearSink === true);
3682
- for (const producer of clauses) {
3683
- const names = linearOutputNames(producer.row, producer.definition);
3684
- for (const name of names) {
3685
- const uses = sinks.reduce((count, sink) => count + countIdentifier(sink.row.value, name), 0);
3686
- if (uses === 0) {
3687
- diagnostics.push({
3688
- code: "HSX1202",
3689
- fix: `route ${name} with moves, payout, distribution, a fee assignment, or an explicit remainder sink`,
3690
- message: `money ${name} produced by ${producer.spelling} is never consumed`,
3691
- severity: "error",
3692
- span: producer.row.span,
3693
- });
3694
- }
3695
- else if (uses > 1) {
3696
- diagnostics.push({
3697
- code: "HSX1201",
3698
- fix: `leave exactly one sink for ${name}`,
3699
- message: `money ${name} is consumed ${uses} times; each money value must be consumed exactly once`,
3700
- severity: "error",
3701
- span: producer.row.span,
3702
- });
3703
- }
3704
- }
3705
- }
3706
- }
3707
- function linearOutputNames(row, definition) {
3708
- if (!("linearOutputs" in definition) || !definition.linearOutputs)
3709
- return [];
3710
- const spellingQualifierCount = definition.spelling.split(" ").length - 1;
3711
- const explicitBinding = row.qualifiers.slice(spellingQualifierCount).at(-1);
3712
- if (explicitBinding)
3713
- return [explicitBinding.name];
3714
- return definition.linearOutputs
3715
- .flatMap((path) => {
3716
- const value = exprAtPath(row.value, path.split("."));
3717
- if (value?.kind === "ident")
3718
- return [value.name];
3719
- if (value?.kind === "string")
3720
- return [value.value];
3721
- if (value?.kind === "path")
3722
- return [value.parts.at(-1)?.name ?? ""];
3723
- return [];
3724
- })
3725
- .filter((name) => name.length > 0);
3726
- }
3727
- function exprAtPath(expr, path) {
3728
- if (path.length === 0)
3729
- return expr;
3730
- const block = expr.kind === "decided_amount" ? expr.body : expr;
3731
- if (block.kind !== "block")
3732
- return undefined;
3733
- const [head, ...tail] = path;
3734
- const row = block.entries.find((candidate) => camel(candidate.key.name) === camel(head ?? ""));
3735
- return row ? exprAtPath(row.value, tail) : undefined;
3736
- }
3737
- function countIdentifier(expr, name) {
3738
- if (expr.kind === "ident")
3739
- return expr.name === name ? 1 : 0;
3740
- if (expr.kind === "string")
3741
- return expr.value === name || expr.value.endsWith(`.${name}`) ? 1 : 0;
3742
- if (expr.kind === "path")
3743
- return expr.parts.at(-1)?.name === name ? 1 : 0;
3744
- if (expr.kind === "settlement_ref")
3745
- return expr.member.name === name ? 1 : 0;
3746
- if (expr.kind === "list")
3747
- return expr.items.reduce((sum, item) => sum + countIdentifier(item, name), 0);
3748
- if (expr.kind === "block")
3749
- return expr.entries.reduce((sum, row) => sum + countIdentifier(row.value, name), 0);
3750
- if (expr.kind === "call" || expr.kind === "type_apply") {
3751
- return expr.args.reduce((sum, item) => sum + countIdentifier(item, name), 0);
3752
- }
3753
- if (expr.kind === "apply") {
3754
- return [...expr.args, ...expr.typeArgs].reduce((sum, item) => sum + countIdentifier(item, name), 0);
3755
- }
3756
- if (expr.kind === "binding")
3757
- return countIdentifier(expr.type, name);
3758
- if (expr.kind === "decided_amount")
3759
- return countIdentifier(expr.body, name);
3760
- return 0;
3761
- }
3762
- function clauseSpelling(row, vocabulary) {
3763
- const key = row.key.name.replaceAll("_", " ");
3764
- const first = row.qualifiers[0]?.name;
3765
- if (!first)
3766
- return key;
3767
- const candidates = [
3768
- `${key} ${first}`,
3769
- `${key} ${first.replaceAll("_", " ")}`,
3770
- ];
3771
- return candidates.find((candidate) => vocabulary.has(candidate)) ?? key;
3772
- }
3773
- function setClause(target, path, cardinality, value, spelling, span, diagnostics) {
3774
- const [head, tail] = path.split(".");
3775
- if (!head)
3776
- return;
3777
- if (!tail) {
3778
- const resolved = clauseValue(target[head], cardinality, value, spelling, span, diagnostics);
3779
- if (resolved !== undefined)
3780
- target[head] = resolved;
3781
- return;
3782
- }
3783
- const nested = target[head] &&
3784
- typeof target[head] === "object" &&
3785
- !Array.isArray(target[head])
3786
- ? { ...target[head] }
3787
- : {};
3788
- const resolved = clauseValue(nested[tail], cardinality, value, spelling, span, diagnostics);
3789
- if (resolved !== undefined)
3790
- nested[tail] = resolved;
3791
- target[head] = nested;
3792
- }
3793
- function clauseValue(current, cardinality, value, spelling, span, diagnostics) {
3794
- if (cardinality === "many") {
3795
- const next = Array.isArray(value) ? value : [value];
3796
- return Array.isArray(current) ? [...current, ...next] : next;
3797
- }
3798
- if (current === undefined)
3799
- return value;
3800
- diagnostics.push({
3801
- code: "HSX1505",
3802
- fix: `keep one ${spelling} clause`,
3803
- message: `${spelling} is a single-valued UDL clause and appears more than once`,
3804
- severity: "error",
3805
- span,
3806
- });
3807
- return undefined;
3808
- }
3809
- function entry(block, ...names) {
3810
- return block.entries.find((candidate) => names.includes(candidate.key.name));
3811
- }
3812
- function stringSlot(block, ...names) {
3813
- const found = entry(block, ...names)?.value;
3814
- if (found?.kind === "string")
3815
- return found.value;
3816
- if (found?.kind === "ident")
3817
- return found.name;
3818
- return undefined;
3819
- }
3820
- function blockToObject(block, substitutions, diagnostics, literalKeys = false, resolvePrefix) {
3821
- if (!block)
3822
- return {};
3823
- const result = {};
3824
- for (const row of block.entries) {
3825
- const value = substituteExpr(row.value, substitutions, resolvePrefix);
3826
- if (twoArgumentMoneyCall(value)) {
3827
- diagnostics.push({
3828
- code: "HSX1104",
3829
- fix: "bind money(CUR, amount) to a declared field whose UDL schema can carry const",
3830
- message: `UDL slot ${camel(row.key.name)} cannot carry a fixed money binding`,
3831
- severity: "error",
3832
- span: row.value.span,
3833
- });
3834
- continue;
3835
- }
3836
- result[literalKeys || row.key.quoted ? row.key.name : camel(row.key.name)] =
3837
- exprToJson(value, diagnostics, literalKeys, resolvePrefix);
3838
- }
3839
- return result;
3840
- }
3841
- function exprToJsonForUdlSlot(expr, slot, diagnostics, resolvePrefix) {
3842
- if (twoArgumentMoneyCall(expr)) {
3843
- diagnostics.push({
3844
- code: "HSX1104",
3845
- fix: "bind money(CUR, amount) to a declared field whose UDL schema can carry const",
3846
- message: `UDL slot ${slot} cannot carry a fixed money binding`,
3847
- severity: "error",
3848
- span: expr.span,
3849
- });
3850
- }
3851
- if (slot === "callerParkedStates" && expr.kind === "block") {
3852
- return Object.fromEntries(expr.entries.map((row) => [
3853
- row.key.name,
3854
- exprToJson(row.value, diagnostics, false, resolvePrefix),
3855
- ]));
3856
- }
3857
- // Declaration maps retain their authored keys; field-spelling aliases still normalize.
3858
- const literalKeys = udlClauseVocabulary.some((clause) => clause.target === slot &&
3859
- "literalKeys" in clause &&
3860
- clause.literalKeys === true);
3861
- return exprToJson(expr, diagnostics, literalKeys, resolvePrefix);
3862
- }
3863
- function exprToJson(expr, diagnostics, literalKeys = false, resolvePrefix) {
3864
- switch (expr.kind) {
3865
- case "boolean":
3866
- return expr.value;
3867
- case "string":
3868
- return expr.value;
3869
- case "number": {
3870
- if (expr.raw.includes(".")) {
3871
- diagnostics.push({
3872
- code: "HSX1103",
3873
- fix: "use an integer, percent, bps, or currency-indexed money literal",
3874
- message: `${expr.raw} is a free decimal; HSX has no float type`,
3875
- severity: "error",
3876
- span: expr.span,
3877
- });
3878
- return expr.raw;
3879
- }
3880
- return Number(expr.raw);
3881
- }
3882
- case "percent":
3883
- return expr.bps;
3884
- case "money": {
3885
- const precision = minorUnits[expr.currency.name] ?? 2;
3886
- const [whole = "0", fraction = ""] = expr.raw.split(".");
3887
- if (fraction.length > precision) {
3888
- diagnostics.push({
3889
- code: "HSX1102",
3890
- fix: `round the literal to ${precision} minor-unit digits`,
3891
- message: `${expr.currency.name} ${expr.raw} is finer than ${expr.currency.name} minor units`,
3892
- severity: "error",
3893
- span: expr.span,
3894
- });
3895
- }
3896
- return `${whole}${fraction.padEnd(precision, "0")}`.replace(/^0+(?=\d)/, "");
3897
- }
3898
- case "ident":
3899
- return expr.name;
3900
- case "path":
3901
- return expr.parts.map((part) => part.name).join(".");
3902
- case "settlement_ref":
3903
- return `${expr.owner.name}.${expr.member.name}`;
3904
- case "list":
3905
- return expr.items.map((item) => exprToJson(item, diagnostics, literalKeys, resolvePrefix));
3906
- case "block":
3907
- return blockToObject(expr, new Map(), diagnostics, literalKeys, resolvePrefix);
3908
- case "call":
3909
- return `${expr.callee.name}(${expr.args.map((arg) => String(exprToJson(arg, diagnostics, false, resolvePrefix))).join(",")})`;
3910
- case "type_apply":
3911
- return `${expr.callee.name}<${expr.args.map((arg) => String(exprToJson(arg, diagnostics, false, resolvePrefix))).join(",")}>`;
3912
- case "binding":
3913
- return exprToJson(expr.type, diagnostics, false, resolvePrefix);
3914
- case "decided_amount":
3915
- return blockToObject(expr.body, new Map(), diagnostics, false, resolvePrefix);
3916
- case "port_ref":
3917
- return expr.name.name;
3918
- case "apply":
3919
- return `${expr.callee.parts.map((part) => part.name).join(".")}()`;
3920
- }
3921
- }
3922
- function typeOf(expr, aliases = new Map(), seen = new Set()) {
3923
- if (expr.kind === "binding")
3924
- return typeOf(expr.type, aliases, seen);
3925
- if (expr.kind === "money") {
3926
- return { currency: expr.currency.name, kind: "money" };
3927
- }
3928
- if (expr.kind === "type_apply" || expr.kind === "call") {
3929
- const kind = expr.callee.name;
3930
- const argument = expr.args[0];
3931
- const name = argument?.kind === "ident"
3932
- ? argument.name
3933
- : argument?.kind === "string"
3934
- ? argument.value
3935
- : undefined;
3936
- if (kind === "money") {
3937
- const amount = expr.args[1];
3938
- const fixedAmount = expr.args.length === 2 &&
3939
- amount?.kind === "number" &&
3940
- /^[1-9][0-9]{0,17}$/.test(amount.raw) &&
3941
- Number.isSafeInteger(Number(amount.raw))
3942
- ? amount.raw
3943
- : undefined;
3944
- return {
3945
- ...(name ? { currency: name } : {}),
3946
- ...(fixedAmount ? { fixedAmount } : {}),
3947
- kind: "money",
3948
- };
3949
- }
3950
- if (kind === "account")
3951
- return { ...(name ? { currency: name } : {}), kind: "account" };
3952
- if (kind === "ref" || kind === "id")
3953
- return { kind: "ref", ...(name ? { target: name } : {}) };
3954
- }
3955
- if (expr.kind === "ident") {
3956
- const alias = aliases.get(expr.name);
3957
- if (alias && !seen.has(expr.name)) {
3958
- return typeOf(alias, aliases, new Set([...seen, expr.name]));
3959
- }
3960
- if (CURRENCY.test(expr.name))
3961
- return { currency: expr.name, kind: "unknown" };
3962
- if ([
3963
- "boolean",
3964
- "account",
3965
- "bps",
3966
- "condition",
3967
- "date",
3968
- "integer",
3969
- "party",
3970
- "percent",
3971
- "ref",
3972
- "text",
3973
- ].includes(expr.name))
3974
- return { kind: expr.name };
3975
- }
3976
- if (expr.kind === "percent")
3977
- return { kind: "percent" };
3978
- if (expr.kind === "boolean")
3979
- return { kind: "boolean" };
3980
- if (expr.kind === "number" && !expr.raw.includes("."))
3981
- return { kind: "integer" };
3982
- return { kind: "unknown" };
3983
- }
3984
- function schemaFor(type, allowZero) {
3985
- switch (type.kind) {
3986
- case "money":
3987
- return {
3988
- ...(type.fixedAmount
3989
- ? { const: type.fixedAmount, pattern: MONEY_PATTERN }
3990
- : { pattern: allowZero ? ZERO_MONEY_PATTERN : MONEY_PATTERN }),
3991
- type: "string",
3992
- ...(type.currency && CURRENCY.test(type.currency)
3993
- ? { "x-hyperscale-currency": type.currency }
3994
- : {}),
3995
- };
3996
- case "bps":
3997
- case "percent":
3998
- return { maximum: 10_000, minimum: 0, type: "integer" };
3999
- case "integer":
4000
- return { type: "integer" };
4001
- case "boolean":
4002
- return { type: "boolean" };
4003
- case "date":
4004
- return { format: "hyperscale-date-time", type: "string" };
4005
- case "ref":
4006
- return { type: "string" };
4007
- case "account":
4008
- return { pattern: ACCOUNT_PATTERN, type: "string" };
4009
- default:
4010
- return { type: "string" };
4011
- }
4012
- }
4013
- function invalidFixedMoneyBinding(expr) {
4014
- const candidate = twoArgumentMoneyCall(expr);
4015
- if (!candidate)
4016
- return undefined;
4017
- const amount = candidate.args[1];
4018
- if (candidate.args.length === 2 &&
4019
- amount?.kind === "number" &&
4020
- /^[1-9][0-9]{0,17}$/.test(amount.raw) &&
4021
- Number.isSafeInteger(Number(amount.raw))) {
4022
- return undefined;
4023
- }
4024
- return "fixes money to an invalid amount; this UDL field slot accepts one fixed minor-unit integer";
4025
- }
4026
- function twoArgumentMoneyCall(expr) {
4027
- const candidate = expr.kind === "binding" ? expr.type : expr;
4028
- return candidate.kind === "call" &&
4029
- candidate.callee.name === "money" &&
4030
- candidate.args.length >= 2
4031
- ? candidate
4032
- : undefined;
4033
- }
4034
- function typeWords(type) {
4035
- return type.currency ? `${type.kind}<${type.currency}>` : type.kind;
4036
- }
4037
- function camel(value) {
4038
- return value.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase());
4039
- }
4040
- function prefixFor(value) {
4041
- const parts = value.split("_");
4042
- return parts.length > 1
4043
- ? parts
4044
- .map((part) => part[0] ?? "")
4045
- .join("")
4046
- .slice(0, 8)
4047
- : value.slice(0, 4);
4048
- }
4049
- function titleize(value) {
4050
- return value
4051
- .replaceAll("_", " ")
4052
- .replace(/\b[a-z]/g, (letter) => letter.toUpperCase());
4053
- }
4054
- function sentenceCase(value) {
4055
- const words = value.replaceAll("_", " ");
4056
- return `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}`;
4057
- }
4058
- function prepareActionPlanProgram(program, diagnostics) {
4059
- if (!program.instruments.some(hasActionPlanClauses))
4060
- return { diagnostics, program };
4061
- const instruments = program.instruments.map((instrument) => {
4062
- if (!hasActionPlanClauses(instrument))
4063
- return instrument;
4064
- const parsed = udlDocumentSchema.shape.instruments.element.safeParse(generalInstrumentValue(instrument));
4065
- if (!parsed.success) {
4066
- for (const issue of parsed.error.issues)
4067
- diagnostics.push({
4068
- code: "HSX1601",
4069
- severity: "error",
4070
- span: instrument.origin,
4071
- message: `${instrument.id}.${issue.path.join(".")}: ${issue.message}`,
4072
- fix: "match the typed UDL clause declaration",
4073
- });
4074
- return instrument;
4075
- }
4076
- const resolved = resolveUdlActionPlans(parsed.data);
4077
- for (const issue of resolved.issues)
4078
- diagnostics.push({
4079
- code: hsxClauseDiagnosticCode(issue.code),
4080
- severity: "error",
4081
- span: instrument.origin,
4082
- message: `${instrument.id}${issue.path}: ${issue.message}`,
4083
- udlCode: issue.code,
4084
- path: issue.path,
4085
- fix: issue.fix,
4086
- });
4087
- return {
4088
- ...instrument,
4089
- actionPlans: resolved.plans,
4090
- actions: instrument.actions.map((action) => {
4091
- if (!action.slots.calls)
4092
- return action;
4093
- const plan = resolved.plans.find((plan) => plan.action === action.name);
4094
- if (!plan)
4095
- return action;
4096
- const effects = plan.effects;
4097
- return {
4098
- ...action,
4099
- effects,
4100
- slots: { ...action.slots, effects: effects },
4101
- };
4102
- }),
4103
- };
4104
- });
4105
- if (diagnostics.some((issue) => issue.severity === "error"))
4106
- return { diagnostics };
4107
- const prepared = { ...program, instruments };
4108
- const lowered = lowerGeneralProgram(prepared);
4109
- if (!lowered.ok) {
4110
- for (const issue of lowered.issues)
4111
- diagnostics.push({
4112
- code: issue.code,
4113
- fix: issue.fix,
4114
- message: issue.message,
4115
- udlCode: issue.udlCode,
4116
- path: issue.path,
4117
- severity: "error",
4118
- span: issue.span,
4119
- });
4120
- return { diagnostics };
4121
- }
4122
- return { diagnostics, program: prepared };
4123
- }
4124
- function derivePieceInputs(instrument, diagnostics) {
4125
- return {
4126
- ...instrument,
4127
- actions: instrument.actions.map((action) => {
4128
- const stage = jsonObject(action.slots.pieceStage);
4129
- if (!stage)
4130
- return action;
4131
- if (action.slots.input !== undefined) {
4132
- diagnostics.push({
4133
- code: "HSX1611",
4134
- severity: "error",
4135
- span: action.origin,
4136
- message: `action ${action.name} overrides its compiler-derived pieceId input`,
4137
- fix: "remove input; piece_stage derives the strict pieceId enum",
4138
- });
4139
- return action;
4140
- }
4141
- const plan = jsonObject(instrument.slots.piecePlan);
4142
- const order = plan?.[`${stage.stage}_order`];
4143
- if (!Array.isArray(order) || order.length === 0)
4144
- return action;
4145
- return {
4146
- ...action,
4147
- slots: {
4148
- ...action.slots,
4149
- input: {
4150
- type: "object",
4151
- additionalProperties: false,
4152
- required: ["pieceId"],
4153
- properties: { pieceId: { type: "string", enum: order } },
4154
- },
4155
- },
4156
- };
4157
- }),
4158
- };
4159
- }
4160
- //# sourceMappingURL=typecheck.js.map