@hyperscale0/hsx 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +2 -400
  2. package/README.md +3 -92
  3. package/dist/src/ast.d.ts +99 -310
  4. package/dist/src/ast.d.ts.map +1 -1
  5. package/dist/src/ast.js +16 -69
  6. package/dist/src/ast.js.map +1 -1
  7. package/dist/src/cli.d.ts +6 -19
  8. package/dist/src/cli.d.ts.map +1 -1
  9. package/dist/src/cli.js +76 -315
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/compile.d.ts +23 -73
  12. package/dist/src/compile.d.ts.map +1 -1
  13. package/dist/src/compile.js +1113 -148
  14. package/dist/src/compile.js.map +1 -1
  15. package/dist/src/cost.d.ts +8 -77
  16. package/dist/src/cost.d.ts.map +1 -1
  17. package/dist/src/cost.js +13 -422
  18. package/dist/src/cost.js.map +1 -1
  19. package/dist/src/format.d.ts +8 -7
  20. package/dist/src/format.d.ts.map +1 -1
  21. package/dist/src/format.js +9 -175
  22. package/dist/src/format.js.map +1 -1
  23. package/dist/src/headers.d.ts +35 -0
  24. package/dist/src/headers.d.ts.map +1 -0
  25. package/dist/src/headers.js +108 -0
  26. package/dist/src/headers.js.map +1 -0
  27. package/dist/src/index.d.ts +6 -12
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +4 -9
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/lex.d.ts +8 -28
  32. package/dist/src/lex.d.ts.map +1 -1
  33. package/dist/src/lex.js +88 -146
  34. package/dist/src/lex.js.map +1 -1
  35. package/dist/src/parse.d.ts +4 -17
  36. package/dist/src/parse.d.ts.map +1 -1
  37. package/dist/src/parse.js +402 -1040
  38. package/dist/src/parse.js.map +1 -1
  39. package/dist/src/std-bundle.d.ts.map +1 -1
  40. package/dist/src/std-bundle.js +12 -20
  41. package/dist/src/std-bundle.js.map +1 -1
  42. package/dist/src/std-library.d.ts +1 -1
  43. package/dist/src/std-library.d.ts.map +1 -1
  44. package/dist/src/std-library.js +1 -5
  45. package/dist/src/std-library.js.map +1 -1
  46. package/dist/src/tunables.d.ts +7 -0
  47. package/dist/src/tunables.d.ts.map +1 -0
  48. package/dist/src/tunables.js +26 -0
  49. package/dist/src/tunables.js.map +1 -0
  50. package/dist/src/version.d.ts +2 -4
  51. package/dist/src/version.d.ts.map +1 -1
  52. package/dist/src/version.js +2 -4
  53. package/dist/src/version.js.map +1 -1
  54. package/docs/README.md +179 -41
  55. package/docs/headers.md +44 -0
  56. package/examples/cost-table.json +99 -751
  57. package/examples/library.hsx +59 -0
  58. package/package.json +9 -17
  59. package/src/ast.ts +82 -441
  60. package/src/cli.ts +77 -362
  61. package/src/compile.ts +1535 -247
  62. package/src/cost.ts +28 -671
  63. package/src/format.ts +12 -256
  64. package/src/headers.ts +126 -0
  65. package/src/index.ts +7 -39
  66. package/src/lex.ts +99 -195
  67. package/src/parse.ts +403 -1209
  68. package/src/std-bundle.ts +12 -20
  69. package/src/std-library.ts +2 -7
  70. package/src/tunables.ts +31 -0
  71. package/src/version.ts +2 -5
  72. package/std/approvals.hsx +17 -0
  73. package/std/cards.hsx +63 -0
  74. package/std/collections.hsx +31 -0
  75. package/std/escrow.hsx +43 -0
  76. package/std/financing.hsx +372 -0
  77. package/std/insurance.hsx +59 -0
  78. package/std/lending.hsx +115 -0
  79. package/std/marketplace.hsx +24 -0
  80. package/std/money.hsx +195 -0
  81. package/std/savings.hsx +44 -0
  82. package/std/travel.hsx +98 -0
  83. package/std/wallet.hsx +41 -0
  84. package/dist/src/diagnostics.d.ts +0 -13
  85. package/dist/src/diagnostics.d.ts.map +0 -1
  86. package/dist/src/diagnostics.js +0 -587
  87. package/dist/src/diagnostics.js.map +0 -1
  88. package/dist/src/emit.d.ts +0 -51
  89. package/dist/src/emit.d.ts.map +0 -1
  90. package/dist/src/emit.js +0 -192
  91. package/dist/src/emit.js.map +0 -1
  92. package/dist/src/entry-overrides.d.ts +0 -58
  93. package/dist/src/entry-overrides.d.ts.map +0 -1
  94. package/dist/src/entry-overrides.js +0 -284
  95. package/dist/src/entry-overrides.js.map +0 -1
  96. package/dist/src/ir.d.ts +0 -73
  97. package/dist/src/ir.d.ts.map +0 -1
  98. package/dist/src/ir.js +0 -15
  99. package/dist/src/ir.js.map +0 -1
  100. package/dist/src/limits.d.ts +0 -23
  101. package/dist/src/limits.d.ts.map +0 -1
  102. package/dist/src/limits.js +0 -23
  103. package/dist/src/limits.js.map +0 -1
  104. package/dist/src/lsp/server.d.ts +0 -32
  105. package/dist/src/lsp/server.d.ts.map +0 -1
  106. package/dist/src/lsp/server.js +0 -391
  107. package/dist/src/lsp/server.js.map +0 -1
  108. package/dist/src/modules.d.ts +0 -38
  109. package/dist/src/modules.d.ts.map +0 -1
  110. package/dist/src/modules.js +0 -372
  111. package/dist/src/modules.js.map +0 -1
  112. package/dist/src/typecheck.d.ts +0 -8
  113. package/dist/src/typecheck.d.ts.map +0 -1
  114. package/dist/src/typecheck.js +0 -4131
  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 -5381
  230. package/std/SEMANTICS.md +0 -44
  231. package/std/money_flows/advance.hsx +0 -235
  232. package/std/money_flows/cancellable_booking.hsx +0 -402
  233. package/std/money_flows/captured_payment.hsx +0 -220
  234. package/std/money_flows/conditional_disbursement.hsx +0 -226
  235. package/std/money_flows/credit_facility.hsx +0 -214
  236. package/std/money_flows/held_payment.hsx +0 -812
  237. package/std/money_flows/index.hsx +0 -4
  238. package/std/money_flows/instant_transfer.hsx +0 -231
  239. package/std/money_flows/metered.hsx +0 -104
  240. package/std/money_flows/pooled_split.hsx +0 -109
  241. package/std/money_flows/premium_forward.hsx +0 -307
  242. package/std/money_flows/reconciled_payout.hsx +0 -119
  243. package/std/money_flows/rotating_pool.hsx +0 -305
  244. package/std/money_flows/scheduled.hsx +0 -599
  245. package/std/money_flows/security_deposit.hsx +0 -248
  246. package/std/money_flows/settlement_batch.hsx +0 -173
  247. package/std/money_flows/swap.hsx +0 -1192
  248. package/std/money_flows/threshold_pool.hsx +0 -307
  249. package/std/money_flows/weighted_distribution.hsx +0 -237
package/dist/src/parse.js CHANGED
@@ -1,1106 +1,468 @@
1
- import { udlClauseVocabulary } from "@hyperscale0/udl";
2
- const compoundClauseHeads = new Set(udlClauseVocabulary
3
- .filter((clause) => clause.spelling.includes(" "))
4
- .map((clause) => clause.spelling.split(" ")[0]));
5
1
  import { lex } from "./lex.js";
6
- import { HSX_LIMITS } from "./limits.js";
2
+ class ParseFailure extends Error {
3
+ diagnostic;
4
+ constructor(diagnostic) {
5
+ super(diagnostic.message);
6
+ this.diagnostic = diagnostic;
7
+ }
8
+ }
7
9
  export function parseProgram(source) {
8
- // Measured before lexing. The ceiling counts UTF-8 bytes, so the encode is
9
- // the measurement rather than an extra step: `source.length` counts UTF-16
10
- // code units, which is a third of the size for CJK text.
11
- const sourceBytes = new TextEncoder().encode(source).byteLength;
12
- if (sourceBytes > HSX_LIMITS.maxSourceBytes) {
10
+ const empty = {
11
+ name: "",
12
+ title: "",
13
+ currency: "SAR",
14
+ header: false,
15
+ decls: [],
16
+ span: { start: 0, end: source.length },
17
+ };
18
+ if (new TextEncoder().encode(source).length > 262144)
13
19
  return {
20
+ program: empty,
14
21
  diagnostics: [
15
22
  {
16
- message: `this file is ${sourceBytes} bytes; HSX reads at most ${HSX_LIMITS.maxSourceBytes}`,
17
- span: { end: 0, start: 0 },
23
+ code: "HSX1004",
24
+ message: "source exceeds 256 KiB",
25
+ fix: "split the program into headers",
26
+ span: empty.span,
18
27
  },
19
28
  ],
20
- program: { decls: [], span: { end: 0, start: 0 } },
21
29
  };
22
- }
23
- const first = source.search(/\S/);
24
- if (first >= 0 && source[first] === "{") {
30
+ const result = lex(source);
31
+ if (result.diagnostics.length)
32
+ return { program: empty, diagnostics: result.diagnostics };
33
+ try {
25
34
  return {
26
- diagnostics: [
27
- {
28
- code: "HSX1014",
29
- fix: "replace the JSON object with HSX declarations",
30
- message: "JSON is not HSX",
31
- span: { end: first + 1, start: first },
32
- },
33
- ],
34
- program: { decls: [], span: { end: source.length, start: 0 } },
35
+ program: new Parser(result.tokens).program(empty),
36
+ diagnostics: [],
35
37
  };
36
38
  }
37
- const lexed = lex(source);
38
- const parser = new Parser(lexed.tokens, [...lexed.diagnostics]);
39
- const program = parser.parseProgram(source.length);
40
- return { diagnostics: parser.diagnostics, program };
39
+ catch (error) {
40
+ if (error instanceof ParseFailure)
41
+ return { program: empty, diagnostics: [error.diagnostic] };
42
+ throw error;
43
+ }
41
44
  }
42
45
  class Parser {
43
- diagnostics;
44
46
  tokens;
45
47
  index = 0;
46
48
  depth = 0;
47
- constructor(tokens, diagnostics) {
49
+ constructor(tokens) {
48
50
  this.tokens = tokens;
49
- this.diagnostics = diagnostics;
50
51
  }
51
- parseProgram(sourceLength) {
52
- const decls = [];
53
- while (!this.at("eof")) {
54
- if (this.atPunct(";")) {
55
- this.advance();
56
- continue;
57
- }
58
- const decl = this.parseDecl();
59
- if (decl) {
60
- decls.push(decl);
61
- continue;
62
- }
63
- this.recoverToDecl();
64
- }
65
- return { decls, span: { end: sourceLength, start: 0 } };
52
+ peek() {
53
+ return this.tokens[this.index];
66
54
  }
67
- // --- Declarations --------------------------------------------------------
68
- parseDecl(exported = false) {
55
+ take() {
69
56
  const token = this.peek();
70
- if (token.kind !== "keyword" || token.text === "from") {
71
- this.error(token.span, token.kind === "eof"
72
- ? "the program ends unexpectedly"
73
- : `expected a declaration (module, program, use, expose, import, party, asset, subject, instrument, settlement, type, const, or port), found "${token.text}"`);
74
- return undefined;
75
- }
76
- switch (token.text) {
77
- case "export":
78
- this.advance();
79
- return this.parseDecl(true);
80
- case "module":
81
- return this.parseModuleDecl(exported);
82
- case "program":
83
- return this.parseProgramDecl();
84
- case "use":
85
- return this.parseUseDecl();
86
- case "expose":
87
- return this.parseExposeDecl();
88
- case "import":
89
- return this.parseImportDecl();
90
- case "party":
91
- return this.parsePartyDecl();
92
- case "asset":
93
- return this.parseAssetDecl();
94
- case "subject":
95
- return this.parseSubjectDecl(exported);
96
- case "instrument":
97
- return this.parseInstrumentDecl(exported);
98
- case "settlement":
99
- return this.parseSettlementDecl();
100
- case "port":
101
- return this.parsePortDecl();
102
- case "type":
103
- return this.parseTypeDecl(exported);
104
- case "const":
105
- return this.parseConstDecl(exported);
106
- default:
107
- this.error(token.span, `"${token.text}" cannot start a declaration`);
108
- this.advance();
109
- return undefined;
110
- }
111
- }
112
- parseModuleDecl(exported) {
113
- const start = this.advance();
114
- if (exported) {
115
- this.error(start.span, "a module declaration cannot be exported");
116
- }
117
- const name = this.parsePath("the module needs a dotted name");
118
- return name
119
- ? { kind: "module", name, span: this.spanFrom(start.span) }
120
- : undefined;
121
- }
122
- parseInstrumentDecl(exported) {
123
- const start = this.advance();
124
- const name = this.expectIdent("the instrument needs a snake_case name");
125
- if (!name)
126
- return undefined;
127
- const typeParameters = this.parseTypeParameters();
128
- if (!typeParameters)
129
- return undefined;
130
- const hasParameterList = this.atPunct("(");
131
- const parameters = hasParameterList ? this.parseParameters() : [];
132
- if (!parameters)
133
- return undefined;
134
- if (this.atPunct("=")) {
135
- this.advance();
136
- const application = this.parseApplication();
137
- if (!application)
138
- return undefined;
139
- const metadata = this.atPunct("{") ? this.parseBlock() : undefined;
140
- return {
141
- application,
142
- exported,
143
- kind: "instrument_apply",
144
- ...(metadata ? { metadata } : {}),
145
- name,
146
- span: this.spanFrom(start.span),
147
- };
148
- }
149
- if (!this.atPunct("{")) {
150
- this.error(this.peek().span, `instrument ${name.name} needs a body or an exported instrument application`);
151
- return undefined;
152
- }
153
- const body = this.parseBlock();
154
- if (!body)
155
- return undefined;
156
- return {
157
- body,
158
- exported,
159
- hasParameterList,
160
- kind: "instrument",
161
- name,
162
- parameters,
163
- span: this.spanFrom(start.span),
164
- typeParameters,
165
- };
166
- }
167
- parseTypeDecl(exported) {
168
- const start = this.advance();
169
- const name = this.expectIdent("the type needs a name");
170
- if (!name)
171
- return undefined;
172
- if (!this.expectPunct("=", `type ${name.name} needs = before its definition`)) {
173
- return undefined;
174
- }
175
- const value = this.parseExpr();
176
- return value
177
- ? { exported, kind: "type", name, span: this.spanFrom(start.span), value }
178
- : undefined;
179
- }
180
- parseConstDecl(exported) {
181
- const start = this.advance();
182
- const name = this.expectIdent("the constant needs a name");
183
- if (!name)
184
- return undefined;
185
- let type;
186
- if (this.atPunct(":")) {
187
- this.advance();
188
- type = this.parseExpr();
189
- if (!type)
190
- return undefined;
191
- }
192
- if (!this.expectPunct("=", `const ${name.name} needs = before its value`)) {
193
- return undefined;
194
- }
195
- const value = this.parseExpr();
196
- return value
197
- ? {
198
- exported,
199
- kind: "const",
200
- name,
201
- span: this.spanFrom(start.span),
202
- ...(type ? { type } : {}),
203
- value,
204
- }
205
- : undefined;
206
- }
207
- parseTypeParameters() {
208
- if (!this.atPunct("<"))
209
- return [];
210
- this.advance();
211
- const parameters = [];
212
- while (!this.atPunct(">") && !this.at("eof")) {
213
- const name = this.expectIdent("expected a type parameter");
214
- if (!name)
215
- return undefined;
216
- parameters.push({ name, span: name.span });
217
- if (this.atPunct(","))
218
- this.advance();
219
- }
220
- return this.expectPunct(">", "the type parameter list never closes")
221
- ? parameters
222
- : undefined;
223
- }
224
- parseParameters() {
225
- this.advance();
226
- const parameters = [];
227
- while (!this.atPunct(")") && !this.at("eof")) {
228
- const name = this.expectIdent("expected a parameter name");
229
- if (!name)
230
- return undefined;
231
- if (!this.expectPunct(":", `parameter ${name.name} needs a type`)) {
232
- return undefined;
233
- }
234
- const type = this.parseExpr();
235
- if (!type)
236
- return undefined;
237
- parameters.push({
238
- name,
239
- span: { start: name.span.start, end: type.span.end },
240
- type,
241
- });
242
- if (this.atPunct(","))
243
- this.advance();
244
- }
245
- return this.expectPunct(")", "the parameter list never closes")
246
- ? parameters
247
- : undefined;
248
- }
249
- parseProgramDecl() {
250
- const start = this.advance();
251
- const name = this.expectIdent("the program needs a snake_case name");
252
- if (!name)
253
- return undefined;
254
- let title;
255
- if (this.at("string")) {
256
- const token = this.advance();
257
- title = { kind: "string", span: token.span, value: token.value };
258
- }
259
- return {
260
- kind: "program",
261
- name,
262
- span: this.spanFrom(start.span),
263
- ...(title ? { title } : {}),
264
- };
265
- }
266
- parseUseDecl() {
267
- const start = this.advance();
268
- const instrument = this.expectIdent("use needs a published instrument id, like: use held_settlement");
269
- return instrument
270
- ? { instrument, kind: "use", span: this.spanFrom(start.span) }
271
- : undefined;
272
- }
273
- parseExposeDecl() {
274
- const start = this.advance();
275
- const instrument = this.expectIdent("expose needs an instrument action, like: expose held_settlement.release as releaseFunds");
276
- if (!instrument)
277
- return undefined;
278
- if (!this.expectPunct(".", "expose separates the instrument and action with .")) {
279
- return undefined;
280
- }
281
- const action = this.expectName("expose needs an action after the instrument id");
282
- if (!action)
283
- return undefined;
284
- if (!this.at("keyword") || this.peek().text !== "as") {
285
- this.error(this.peek().span, "expose needs as before its public action name");
286
- return undefined;
287
- }
288
- this.advance();
289
- const publicName = this.expectIdent("expose needs a camelCase public action name");
290
- return publicName
291
- ? {
292
- action,
293
- instrument,
294
- kind: "expose",
295
- publicName,
296
- span: this.spanFrom(start.span),
297
- }
298
- : undefined;
299
- }
300
- parseImportDecl() {
301
- const start = this.advance();
302
- if (!this.expectPunct("{", 'import lists its names in braces: import { held_payment } from "std/money_flows"')) {
303
- return undefined;
304
- }
305
- const names = [];
306
- while (!this.atPunct("}") && !this.at("eof")) {
307
- const name = this.expectIdent("expected an imported name");
308
- if (!name)
309
- return undefined;
310
- names.push(name);
311
- if (this.atPunct(","))
312
- this.advance();
313
- }
314
- if (!this.expectPunct("}", "the import name list never closes")) {
315
- return undefined;
316
- }
317
- if (!this.at("keyword") || this.peek().text !== "from") {
318
- this.error(this.peek().span, 'import needs from: import { held_payment } from "std/money_flows"');
319
- return undefined;
320
- }
321
- this.advance();
322
- if (!this.at("string")) {
323
- this.error(this.peek().span, 'the import source must be a quoted module name like "std/money_flows"');
324
- return undefined;
325
- }
326
- const sourceToken = this.advance();
327
- if (names.length === 0) {
328
- this.error(start.span, "import brings in at least one name");
329
- }
330
- return {
331
- from: {
332
- kind: "string",
333
- span: sourceToken.span,
334
- value: sourceToken.value,
335
- },
336
- kind: "import",
337
- names,
338
- span: this.spanFrom(start.span),
339
- };
340
- }
341
- parsePartyDecl() {
342
- const start = this.advance();
343
- const name = this.expectIdent("the party needs a name, like: party buyer: person");
344
- if (!name)
345
- return undefined;
346
- if (!this.expectPunct(":", `party ${name.name} needs a kind, like: party ${name.name}: person`)) {
347
- return undefined;
348
- }
349
- const partyKind = this.expectIdent(`expected a kind for party ${name.name}`);
350
- if (!partyKind)
351
- return undefined;
352
- const attrs = this.atPunct("{") ? this.parseBlock() : undefined;
353
- return {
354
- ...(attrs ? { attrs } : {}),
355
- kind: "party",
356
- name,
357
- partyKind,
358
- span: this.spanFrom(start.span),
359
- };
360
- }
361
- parseAssetDecl() {
362
- const start = this.advance();
363
- const name = this.expectIdent("the asset needs a name, like: asset vehicle: good");
364
- if (!name)
365
- return undefined;
366
- if (!this.expectPunct(":", `asset ${name.name} needs a kind, like: asset ${name.name}: good`)) {
367
- return undefined;
368
- }
369
- const assetKind = this.expectIdent(`expected a kind for asset ${name.name}`);
370
- if (!assetKind)
371
- return undefined;
372
- const attrs = this.atPunct("{") ? this.parseBlock() : undefined;
373
- return {
374
- assetKind,
375
- ...(attrs ? { attrs } : {}),
376
- kind: "asset",
377
- name,
378
- span: this.spanFrom(start.span),
379
- };
380
- }
381
- parseSubjectDecl(exported) {
382
- const start = this.advance();
383
- if (exported) {
384
- this.error(start.span, "a subject declaration cannot be exported");
385
- }
386
- const name = this.expectIdent("the subject needs a snake_case kind name");
387
- if (!name)
388
- return undefined;
389
- if (!this.atPunct("{")) {
390
- this.error(this.peek().span, `subject ${name.name} needs a body`);
391
- return undefined;
392
- }
393
- const body = this.parseBlock();
394
- return body
395
- ? { body, kind: "subject", name, span: this.spanFrom(start.span) }
396
- : undefined;
397
- }
398
- parseSettlementDecl() {
399
- const start = this.advance();
400
- const name = this.expectIdent("the settlement needs a name, like: settlement sale = held_payment { ... }");
401
- if (!name)
402
- return undefined;
403
- if (!this.expectPunct("=", `settlement ${name.name} instantiates a module: settlement ${name.name} = held_payment { ... }`)) {
404
- return undefined;
405
- }
406
- const moduleName = this.expectIdent(`expected a module name for settlement ${name.name}`);
407
- if (!moduleName)
408
- return undefined;
409
- if (!this.atPunct("{")) {
410
- this.error(this.peek().span, `settlement ${name.name} needs a body in braces`);
411
- return undefined;
412
- }
413
- const body = this.parseBlock();
414
- if (!body)
415
- return undefined;
416
- return {
417
- application: {
418
- args: body.entries.map((row) => ({
419
- kind: "binding",
420
- name: row.key,
421
- span: row.value.span,
422
- type: row.value,
423
- })),
424
- callee: { kind: "path", parts: [moduleName], span: moduleName.span },
425
- kind: "apply",
426
- span: { start: moduleName.span.start, end: body.span.end },
427
- typeArgs: [],
428
- },
429
- exported: false,
430
- kind: "instrument_apply",
431
- name,
432
- span: this.spanFrom(start.span),
433
- };
57
+ if (token.kind !== "eof")
58
+ this.index++;
59
+ return token;
434
60
  }
435
- parsePortDecl() {
436
- const start = this.advance();
437
- const name = this.expectIdent("the port needs a name, like: port confirm_handover { ... }");
438
- if (!name)
439
- return undefined;
440
- if (!this.atPunct("{")) {
441
- this.error(this.peek().span, `port ${name.name} needs a body in braces`);
442
- return undefined;
443
- }
444
- const body = this.parseBlock();
445
- if (!body)
446
- return undefined;
447
- return { body, kind: "port", name, span: this.spanFrom(start.span) };
61
+ at(text) {
62
+ return this.peek().text === text;
448
63
  }
449
- // --- Entries and expressions ---------------------------------------------
450
- /**
451
- * Every recursive edge in the grammar passes through `parseExpr` or
452
- * `parseBlock`: the list, block, call-argument, and binding-type branches of
453
- * one, and the block to entry to block cycle of the other. Guarding those
454
- * two entry points is therefore the whole ceiling, and the guard lives in
455
- * one place instead of five.
456
- *
457
- * The budget counts those entries, not source levels, and the two are not
458
- * the same number: `k { ... }` spends one per level, while `k: { ... }`
459
- * spends two because the colon branch enters `parseExpr` and `parseExpr`
460
- * then enters `parseBlock`. So the message names a budget rather than a
461
- * depth. A builder told they exceeded "64 levels" would count 32 braces and
462
- * have nothing to act on.
463
- */
464
- enterNesting(span) {
465
- if (this.depth >= HSX_LIMITS.maxNestingDepth) {
466
- this.error(span, `this nests past the parser's depth budget of ${HSX_LIMITS.maxNestingDepth}; flatten it`);
64
+ eat(text) {
65
+ if (!this.at(text))
467
66
  return false;
468
- }
469
- this.depth += 1;
67
+ this.take();
470
68
  return true;
471
69
  }
472
- parseBlock(context = "general") {
473
- if (!this.enterNesting(this.peek().span))
474
- return undefined;
475
- const block = this.parseBlockUnguarded(context);
476
- this.depth -= 1;
477
- return block;
478
- }
479
- parseBlockUnguarded(context) {
480
- const open = this.advance();
481
- const entries = [];
482
- while (!this.atPunct("}") && !this.at("eof")) {
483
- if (this.atPunct(";") || this.atPunct(",")) {
484
- this.advance();
70
+ fail(message, fix) {
71
+ throw new ParseFailure({
72
+ code: "HSX1000",
73
+ message,
74
+ fix,
75
+ span: this.peek().span,
76
+ });
77
+ }
78
+ expect(text) {
79
+ if (!this.eat(text))
80
+ this.fail(`expected ${text}, found ${this.peek().text || "end of file"}`, `write ${text} here`);
81
+ }
82
+ identifier() {
83
+ if (this.peek().kind !== "name")
84
+ this.fail("expected a name", "write a name starting with a letter");
85
+ return this.take().text;
86
+ }
87
+ path() {
88
+ let name = this.identifier();
89
+ while (this.eat("."))
90
+ name += "." + this.identifier();
91
+ return name;
92
+ }
93
+ separators() {
94
+ while (this.eat(",") || this.eat(";")) { }
95
+ }
96
+ span(start) {
97
+ return { start, end: this.tokens[Math.max(0, this.index - 1)].span.end };
98
+ }
99
+ program(program) {
100
+ if (this.eat("header"))
101
+ program.header = true;
102
+ else
103
+ this.expect("program");
104
+ program.name = this.identifier();
105
+ if (!program.header) {
106
+ if (this.peek().kind !== "string")
107
+ this.fail("program needs a title", "write a quoted title after its name");
108
+ program.title = JSON.parse(this.take().text);
109
+ }
110
+ while (this.peek().kind !== "eof") {
111
+ this.separators();
112
+ if (this.peek().kind === "eof")
113
+ break;
114
+ const start = this.peek().span.start;
115
+ if (this.eat("currency")) {
116
+ program.currency = this.identifier();
485
117
  continue;
486
118
  }
487
- const before = this.index;
488
- const entry = this.parseEntry(context);
489
- if (!entry) {
490
- this.recoverInBlock();
491
- // recoverInBlock bails without consuming when it lands on a depth-0
492
- // keyword (e.g. `from` used as an entry key), and a failed
493
- // parseEntry may not have consumed either. Without forced progress
494
- // this loop never terminates.
495
- if (this.index === before)
496
- this.advance();
119
+ if (this.eat("use")) {
120
+ program.decls.push({
121
+ kind: "use",
122
+ name: this.identifier(),
123
+ span: this.span(start),
124
+ });
497
125
  continue;
498
126
  }
499
- entries.push(entry);
500
- if (this.atPunct(",") || this.atPunct(";"))
501
- this.advance();
502
- }
503
- if (!this.expectPunct("}", "this block never closes; add the matching }")) {
504
- return { entries, kind: "block", span: this.spanFrom(open.span) };
505
- }
506
- return { entries, kind: "block", span: this.spanFrom(open.span) };
507
- }
508
- parseEntry(context) {
509
- const token = this.peek();
510
- const key = token.kind === "string"
511
- ? (() => {
512
- this.advance();
513
- return {
514
- kind: "ident",
515
- name: token.value,
516
- quoted: !token.value.includes("["),
517
- span: token.span,
518
- };
519
- })()
520
- : this.isName(token) && (token.text === "action" || token.text === "on")
521
- ? this.parseIdent()
522
- : this.parseTemplatedName("expected an entry name here");
523
- if (!key)
524
- return undefined;
525
- const qualifiers = [];
526
- if (key.name === "for") {
527
- const binding = this.expectIdent("for needs a compile-time binding name");
528
- const inWord = binding
529
- ? this.expectName("for needs in before its finite bound")
530
- : undefined;
531
- if (!binding || inWord?.name !== "in") {
532
- this.error(this.peek().span, "write `for item in <finite bound> { ... }`");
533
- return undefined;
534
- }
535
- const bound = this.parseExpr();
536
- if (!bound || !this.atPunct("{")) {
537
- this.error(this.peek().span, "a bounded comprehension needs a body in braces");
538
- return undefined;
127
+ if (this.eat("party")) {
128
+ const name = this.identifier();
129
+ this.expect(":");
130
+ const partyKind = this.identifier();
131
+ const role = this.eat("role") ? this.identifier() : undefined;
132
+ program.decls.push({
133
+ kind: "party",
134
+ name,
135
+ partyKind,
136
+ ...(role ? { role } : {}),
137
+ span: this.span(start),
138
+ });
139
+ continue;
539
140
  }
540
- const value = this.parseBlock(context);
541
- return value
542
- ? {
543
- iteration: { binding, bound },
544
- key,
545
- qualifiers,
546
- span: { start: key.span.start, end: value.span.end },
547
- value,
141
+ if (this.at("expose") || this.at("hide")) {
142
+ const kind = this.take().text;
143
+ const target = this.path();
144
+ let name;
145
+ if (kind === "expose") {
146
+ this.expect("as");
147
+ name = this.identifier();
548
148
  }
549
- : undefined;
550
- }
551
- if (key.name === "action" &&
552
- (this.isName(this.peek()) || this.atPunct("["))) {
553
- const action = this.parseTemplatedName("action needs a name");
554
- if (!action)
555
- return undefined;
556
- qualifiers.push(action);
557
- if (!this.atPunct("{")) {
558
- this.error(this.peek().span, `action ${qualifiers[0]?.name ?? ""} needs a body`);
559
- return undefined;
149
+ program.decls.push(kind === "expose"
150
+ ? { kind, target, name: name, span: this.span(start) }
151
+ : { kind, target, span: this.span(start) });
152
+ continue;
560
153
  }
561
- const value = this.parseBlock();
562
- return value
563
- ? {
564
- key,
565
- qualifiers,
566
- span: { start: key.span.start, end: value.span.end },
567
- value,
568
- }
569
- : undefined;
570
- }
571
- if (context === "lifecycle" && key.name === "states") {
572
- const items = [];
573
- while (!this.atPunct(";") && !this.atPunct("}") && !this.at("eof")) {
574
- const state = this.parseTemplatedName("expected a lifecycle state");
575
- if (!state)
576
- return undefined;
577
- items.push(state);
154
+ if (this.eat("instrument")) {
155
+ const name = this.identifier();
156
+ const parameters = this.eat("(") ? this.entries(")") : [];
157
+ const body = this.block();
158
+ program.decls.push({
159
+ kind: "instrument",
160
+ name,
161
+ parameters,
162
+ body,
163
+ span: this.span(start),
164
+ });
165
+ continue;
578
166
  }
579
- const end = items.at(-1)?.span.end ?? key.span.end;
580
- return {
581
- key,
582
- qualifiers,
583
- span: { start: key.span.start, end },
584
- value: { items, kind: "list", span: { start: key.span.end, end } },
585
- };
167
+ const name = this.identifier();
168
+ this.expect("=");
169
+ const object = this.path();
170
+ const body = this.block();
171
+ program.decls.push({
172
+ kind: "object",
173
+ name,
174
+ object,
175
+ body,
176
+ span: this.span(start),
177
+ });
586
178
  }
587
- if (context === "lifecycle" && key.name === "initial") {
588
- const value = this.parseTemplatedName("initial needs one declared lifecycle state");
589
- return value
179
+ return program;
180
+ }
181
+ entries(end) {
182
+ const entries = [];
183
+ while (!this.eat(end)) {
184
+ this.separators();
185
+ if (this.eat(end))
186
+ break;
187
+ if (this.peek().kind === "eof")
188
+ this.fail(`unclosed block, expected ${end}`, `add ${end}`);
189
+ const start = this.peek().span.start;
190
+ let key = this.path();
191
+ if (key === "action" && !this.at(":"))
192
+ key += " " + this.identifier();
193
+ if (key === "when") {
194
+ const tunable = this.identifier();
195
+ this.expect("is");
196
+ const choice = this.identifier();
197
+ entries.push({
198
+ key: `when ${tunable} is ${choice}`,
199
+ value: this.block(),
200
+ span: this.span(start),
201
+ });
202
+ continue;
203
+ }
204
+ const clause = ["requires", "invariants", "moves"].includes(key);
205
+ const value = clause && !this.at(":") && !this.at("{")
590
206
  ? {
591
- key,
592
- qualifiers,
593
- span: { start: key.span.start, end: value.span.end },
594
- value,
207
+ kind: "list",
208
+ items: [key === "moves" ? this.move() : this.requirement()],
209
+ span: this.span(start),
595
210
  }
596
- : undefined;
211
+ : this.at("{")
212
+ ? this.block()
213
+ : (this.expect(":"), this.expression());
214
+ entries.push({ key, value, span: this.span(start) });
215
+ this.separators();
597
216
  }
598
- if (context === "lifecycle" && key.name === "parked") {
599
- const state = this.parseTemplatedName("parked needs a lifecycle state");
600
- if (!state)
601
- return undefined;
602
- qualifiers.push(state);
603
- const reasonWord = this.expectName('parked needs reason "..."');
604
- if (!reasonWord || reasonWord.name !== "reason" || !this.at("string")) {
605
- this.error(this.peek().span, 'parked needs reason "..."');
606
- return undefined;
607
- }
608
- const token = this.advance();
609
- const value = {
610
- kind: "string",
611
- span: token.span,
612
- value: token.value,
613
- };
614
- return {
217
+ return entries;
218
+ }
219
+ node(value) {
220
+ return { kind: "text", value, span: this.peek().span };
221
+ }
222
+ record(values) {
223
+ const span = this.peek().span;
224
+ return {
225
+ kind: "block",
226
+ entries: Object.entries(values).map(([key, value]) => ({
615
227
  key,
616
- qualifiers,
617
- span: { start: key.span.start, end: value.span.end },
618
228
  value,
229
+ span: value.span,
230
+ })),
231
+ span,
232
+ };
233
+ }
234
+ operand() {
235
+ const value = this.atom();
236
+ return this.record({
237
+ [value.kind === "name" && /^(self|input|party)\./.test(value.value)
238
+ ? "field"
239
+ : "literal"]: value,
240
+ });
241
+ }
242
+ operator() {
243
+ const operator = this.take();
244
+ if (!["==", "!=", "<", "<=", ">", ">="].includes(operator.text))
245
+ this.fail("expected a comparison", "write ==, !=, <, <=, >, or >=");
246
+ return this.node(operator.text);
247
+ }
248
+ requirement() {
249
+ if (this.eat("approval")) {
250
+ this.expect("by");
251
+ const party = this.atom();
252
+ const values = {
253
+ kind: this.node("approval"),
254
+ party,
255
+ decision: this.node("approved"),
619
256
  };
257
+ if (this.eat("for"))
258
+ values.action = this.atom();
259
+ if (this.eat("is"))
260
+ values.decision = this.atom();
261
+ return this.record(values);
262
+ }
263
+ if (this.eat("unique")) {
264
+ const namespace = this.atom();
265
+ this.expect("on");
266
+ return this.record({
267
+ kind: this.node("unique"),
268
+ namespace,
269
+ fields: this.atom(),
270
+ });
620
271
  }
621
- if (context === "lifecycle" && key.name === "on") {
622
- const action = this.parseTemplatedName("on needs an action name");
623
- if (!action)
624
- return undefined;
625
- qualifiers.push(action);
626
- if (!this.expectPunct(":", `on ${action.name} needs : before its source states`)) {
627
- return undefined;
628
- }
629
- while (!this.atPunct("->") && !this.at("eof")) {
630
- const state = this.parseTemplatedName("expected a source lifecycle state");
631
- if (!state)
632
- return undefined;
633
- qualifiers.push(state);
634
- if (this.atPunct("|"))
635
- this.advance();
636
- }
637
- if (!this.expectPunct("->", `on ${action.name} needs -> before its target state`)) {
638
- return undefined;
639
- }
640
- const value = this.parseTemplatedName("expected a target lifecycle state");
641
- return value
642
- ? {
643
- key,
644
- qualifiers,
645
- span: { start: key.span.start, end: value.span.end },
646
- value,
647
- }
648
- : undefined;
272
+ if (this.at("count") || this.at("sum")) {
273
+ const measure = this.eat("count")
274
+ ? this.node("count")
275
+ : (this.expect("sum"), this.record({ sum: this.atom() }));
276
+ this.expect("of");
277
+ const selection = this.block();
278
+ const operator = this.operator();
279
+ const value = this.operand();
280
+ return this.record({
281
+ kind: this.node("aggregate"),
282
+ selection,
283
+ measure,
284
+ operator,
285
+ value,
286
+ });
649
287
  }
650
- if (key.name === "notify") {
651
- const role = this.expectName("notify needs a declared role");
652
- const via = role
653
- ? this.expectName("notify needs via before its channel")
654
- : undefined;
655
- const channel = via
656
- ? this.expectName("notify needs a channel")
657
- : undefined;
658
- if (!role || via?.name !== "via" || !channel)
659
- return undefined;
660
- qualifiers.push(role, channel);
661
- return {
662
- key,
663
- qualifiers,
664
- span: { start: key.span.start, end: channel.span.end },
665
- value: { kind: "boolean", span: channel.span, value: true },
666
- };
288
+ if (this.eat("hours")) {
289
+ const at = this.atom();
290
+ this.expect("between");
291
+ const start = this.atom();
292
+ this.expect("and");
293
+ const end = this.atom();
294
+ this.expect("timezone");
295
+ return this.record({
296
+ kind: this.node("hours"),
297
+ at,
298
+ start,
299
+ end,
300
+ timezone: this.atom(),
301
+ });
667
302
  }
668
- if (this.atPunct("(")) {
669
- this.advance();
670
- while (!this.atPunct(")") && !this.at("eof")) {
671
- const qualifier = this.at("number")
672
- ? this.parseIdent()
673
- : this.expectIdent(`expected a qualifier inside ${key.name}( ... )`);
674
- if (!qualifier)
675
- return undefined;
676
- qualifiers.push(qualifier);
677
- if (this.atPunct(","))
678
- this.advance();
679
- }
680
- if (!this.expectPunct(")", `${key.name}( ... ) never closes`)) {
681
- return undefined;
303
+ if (this.eat("evidence")) {
304
+ const subject = this.atom();
305
+ const values = {
306
+ kind: this.node("evidence"),
307
+ subject,
308
+ };
309
+ for (const key of ["family", "check", "result", "maxAge"]) {
310
+ this.expect(key);
311
+ values[key] = this.atom();
682
312
  }
313
+ return this.record(values);
314
+ }
315
+ const left = this.operand();
316
+ if (this.eat("in")) {
317
+ const reference = left.entries.find((entry) => entry.key === "field")?.value;
318
+ if (!reference)
319
+ this.fail("state requirement needs a reference", "write requires self.record in [state]");
320
+ return this.record({
321
+ kind: this.node("state"),
322
+ reference,
323
+ states: this.atom(),
324
+ });
683
325
  }
684
- if (compoundClauseHeads.has(key.name) && this.isName(this.peek())) {
685
- while (this.isName(this.peek()))
686
- qualifiers.push(this.parseIdent());
687
- }
688
- let value;
689
- if (this.atPunct("{")) {
690
- value = this.parseBlock(key.name === "lifecycle" || context === "lifecycle"
691
- ? "lifecycle"
692
- : "general");
693
- }
694
- else if (this.atPunct(":")) {
695
- this.advance();
696
- value = this.atPunct("{")
697
- ? this.parseBlock(key.name === "lifecycle" || context === "lifecycle"
698
- ? "lifecycle"
699
- : "general")
700
- : this.parseExpr();
701
- }
702
- else if (this.canStartExpr(this.peek())) {
703
- value = this.parseExpr();
326
+ const operator = this.operator();
327
+ return this.record({
328
+ kind: this.node("compare"),
329
+ left,
330
+ operator,
331
+ right: this.operand(),
332
+ });
333
+ }
334
+ move() {
335
+ const values = {};
336
+ if (this.at("post") || this.at("void")) {
337
+ values.operation = this.node("internal_transfer." + this.take().text);
338
+ values.transfer = this.atom();
704
339
  }
705
340
  else {
706
- this.error(this.peek().span, `entry ${key.name} needs a value: either "${key.name}: <value>" or "${key.name} { ... }"`);
707
- return undefined;
341
+ if (this.eat("reserve"))
342
+ values.operation = this.node("internal_transfer.reserve");
343
+ values.amount = this.atom();
344
+ this.expect("from");
345
+ values.from = this.atom();
346
+ if (this.eat("to"))
347
+ values.to = this.atom();
348
+ else {
349
+ this.expect("shares");
350
+ values.shares = this.atom();
351
+ }
708
352
  }
709
- if (!value)
710
- return undefined;
711
- return {
712
- key,
713
- qualifiers,
714
- span: { end: value.span.end, start: key.span.start },
715
- value,
716
- };
717
- }
718
- parseExpr() {
719
- if (!this.enterNesting(this.peek().span))
720
- return undefined;
721
- const expr = this.parseExprUnguarded();
722
- this.depth -= 1;
723
- return expr;
353
+ for (const key of ["fee", "capture", "key"])
354
+ if (this.eat(key))
355
+ values[key] = this.atom();
356
+ return this.record(values);
357
+ }
358
+ block() {
359
+ const start = this.peek().span.start;
360
+ this.expect("{");
361
+ if (++this.depth > 32)
362
+ this.fail("block nesting exceeds 32", "reduce nested blocks");
363
+ const entries = this.entries("}");
364
+ this.depth--;
365
+ return { kind: "block", entries, span: this.span(start) };
366
+ }
367
+ expression() {
368
+ if (++this.depth > 32)
369
+ this.fail("expression nesting exceeds 32", "reduce nested values");
370
+ const start = this.peek().span.start;
371
+ let value = this.atom();
372
+ if (this.eat("cap"))
373
+ value = {
374
+ kind: "capped",
375
+ rate: value,
376
+ cap: this.atom(),
377
+ span: this.span(start),
378
+ };
379
+ if (this.eat("="))
380
+ value = {
381
+ kind: "default",
382
+ type: value,
383
+ value: this.expression(),
384
+ span: this.span(start),
385
+ };
386
+ this.depth--;
387
+ return value;
724
388
  }
725
- parseExprUnguarded() {
389
+ atom() {
726
390
  const token = this.peek();
727
- if (token.kind === "string") {
728
- this.advance();
729
- return { kind: "string", span: token.span, value: token.value };
730
- }
731
- if (token.kind === "number") {
732
- this.advance();
733
- return { kind: "number", raw: token.value, span: token.span };
734
- }
735
- if (token.kind === "percent") {
736
- this.advance();
737
- const bps = percentToBps(token.value);
738
- if (bps === undefined) {
739
- this.error(token.span, `${token.text} is finer than the money system can settle; percents go down to a hundredth (basis point), like 99.75%`);
740
- return { bps: 0, kind: "percent", raw: token.value, span: token.span };
741
- }
742
- return { bps, kind: "percent", raw: token.value, span: token.span };
743
- }
744
- if (this.atPunct("[")) {
745
- const open = this.advance();
391
+ const start = token.span.start;
392
+ if (this.at("{"))
393
+ return this.block();
394
+ if (this.eat("[")) {
746
395
  const items = [];
747
- while (!this.atPunct("]") && !this.at("eof")) {
748
- const item = this.parseExpr();
749
- if (!item)
750
- return undefined;
751
- items.push(item);
752
- if (this.atPunct(","))
753
- this.advance();
754
- }
755
- if (!this.expectPunct("]", "this list never closes; add the matching ]")) {
756
- return undefined;
757
- }
758
- return { items, kind: "list", span: this.spanFrom(open.span) };
759
- }
760
- if (this.atPunct("{")) {
761
- return this.parseBlock();
762
- }
763
- if (token.kind === "keyword" && token.text === "port") {
764
- const start = this.advance();
765
- const name = this.expectIdent("expected a port name after the port keyword");
766
- if (!name)
767
- return undefined;
768
- let within;
769
- if (this.at("ident") && this.peek().text === "within") {
770
- this.advance();
771
- within = this.expectIdent("expected a fixed duration after within, like P14D");
772
- if (!within)
773
- return undefined;
396
+ while (!this.eat("]")) {
397
+ if (this.peek().kind === "eof")
398
+ this.fail("unclosed list", "add ]");
399
+ items.push(this.expression());
400
+ this.separators();
774
401
  }
775
- let deadline;
776
- if (this.atPunct("|")) {
777
- this.advance();
778
- if (!this.at("ident") || this.peek().text !== "at") {
779
- this.error(this.peek().span, "after | comes the date the platform decides on, like: | at(releaseDueAt)");
780
- return undefined;
781
- }
782
- this.advance();
783
- if (!this.expectPunct("(", "at names its date field in parentheses: at(releaseDueAt)")) {
784
- return undefined;
785
- }
786
- deadline = this.expectIdent("expected a stored date field inside at( ... ), like at(releaseDueAt)");
787
- if (!deadline)
788
- return undefined;
789
- if (!this.expectPunct(")", "at( ... ) never closes"))
790
- return undefined;
791
- }
792
- return {
793
- ...(deadline ? { deadline } : {}),
794
- kind: "port_ref",
795
- name,
796
- span: this.spanFrom(start.span),
797
- ...(within ? { within } : {}),
798
- };
799
- }
800
- if (token.kind === "keyword") {
801
- return this.parseIdent();
402
+ return { kind: "list", items, span: this.span(start) };
802
403
  }
803
- if (token.kind === "ident" &&
804
- /^[A-Z]{3}$/.test(token.text) &&
805
- this.peekAt(1).kind === "number") {
806
- const currency = this.parseIdent();
807
- const amount = this.advance();
808
- return {
809
- currency,
810
- kind: "money",
811
- raw: amount.value,
812
- span: { start: currency.span.start, end: amount.span.end },
813
- };
814
- }
815
- if (token.kind === "ident") {
816
- const ident = this.parseIdent();
817
- if (ident.name === "true" || ident.name === "false") {
818
- return {
819
- kind: "boolean",
820
- span: ident.span,
821
- value: ident.name === "true",
822
- };
823
- }
824
- if (ident.name === "decided" && this.atPunct("{")) {
825
- const body = this.parseBlock();
826
- if (!body)
827
- return undefined;
828
- return {
829
- body,
830
- kind: "decided_amount",
831
- span: { end: body.span.end, start: ident.span.start },
832
- };
833
- }
834
- if (this.atPunct(".")) {
835
- this.advance();
836
- const member = this.expectIdent(`expected the exit after ${ident.name}., like ${ident.name}.release`);
837
- if (!member)
838
- return undefined;
839
- return {
840
- kind: "settlement_ref",
841
- member,
842
- owner: ident,
843
- span: { end: member.span.end, start: ident.span.start },
844
- };
845
- }
846
- if (this.atPunct("<")) {
847
- const args = this.parseAngleArguments();
848
- if (!args)
849
- return undefined;
850
- return {
851
- args,
852
- callee: ident,
853
- kind: "type_apply",
854
- span: this.spanFrom(ident.span),
855
- };
856
- }
857
- if (this.atPunct("(")) {
858
- this.advance();
859
- const args = [];
860
- while (!this.atPunct(")") && !this.at("eof")) {
861
- const arg = this.parseExpr();
862
- if (!arg)
863
- return undefined;
864
- args.push(arg);
865
- if (this.atPunct(","))
866
- this.advance();
867
- }
868
- if (!this.expectPunct(")", `${ident.name}( ... ) never closes`)) {
869
- return undefined;
870
- }
404
+ if (token.kind === "number") {
405
+ this.take();
406
+ if (token.text.endsWith("%"))
871
407
  return {
872
- args,
873
- callee: ident,
874
- kind: "call",
875
- span: this.spanFrom(ident.span),
408
+ kind: "percent",
409
+ value: token.text.slice(0, -1),
410
+ span: token.span,
876
411
  };
877
- }
878
- if (this.atPunct(":")) {
879
- this.advance();
880
- const type = this.parseExpr();
881
- if (!type)
882
- return undefined;
412
+ if (/[a-z]$/.test(token.text))
413
+ return { kind: "duration", value: token.text, span: token.span };
414
+ if (/^[A-Z]{3}$/.test(this.peek().text)) {
415
+ const currency = this.take().text;
883
416
  return {
884
- kind: "binding",
885
- name: ident,
886
- span: { end: type.span.end, start: ident.span.start },
887
- type,
417
+ kind: "money",
418
+ value: token.text + " " + currency,
419
+ span: this.span(start),
888
420
  };
889
421
  }
890
- return ident;
422
+ return { kind: "number", value: token.text, span: token.span };
891
423
  }
892
- this.error(token.span, token.kind === "eof"
893
- ? "the program ends where a value was expected"
894
- : `"${token.text}" is not a value HSX understands here`);
895
- return undefined;
896
- }
897
- // --- Recovery ------------------------------------------------------------
898
- recoverToDecl() {
899
- while (!this.at("eof")) {
900
- const token = this.peek();
901
- if (token.kind === "keyword" && token.text !== "from")
902
- return;
903
- this.advance();
424
+ if (token.kind === "string" || token.kind === "date") {
425
+ this.take();
426
+ return {
427
+ kind: token.kind === "string" ? "text" : "date",
428
+ value: token.kind === "string"
429
+ ? JSON.parse(token.text)
430
+ : token.text,
431
+ span: token.span,
432
+ };
904
433
  }
905
- }
906
- /** Skip to the next plausible entry start or the end of the block. */
907
- recoverInBlock() {
908
- let depth = 0;
909
- while (!this.at("eof")) {
910
- const token = this.peek();
911
- if (token.kind === "punct") {
912
- if (token.text === "{" || token.text === "[" || token.text === "(")
913
- depth += 1;
914
- if (token.text === "}" || token.text === "]" || token.text === ")") {
915
- if (depth === 0)
916
- return;
917
- depth -= 1;
918
- }
919
- if (token.text === "," && depth === 0) {
920
- this.advance();
921
- return;
922
- }
434
+ const name = this.path();
435
+ if (this.eat("(")) {
436
+ const args = [];
437
+ while (!this.eat(")")) {
438
+ args.push(this.expression());
439
+ this.separators();
923
440
  }
924
- if (token.kind === "keyword" && depth === 0)
925
- return;
926
- this.advance();
927
- }
928
- }
929
- // --- Token helpers -------------------------------------------------------
930
- peek() {
931
- return this.tokens[this.index];
932
- }
933
- peekAt(offset) {
934
- return this.tokens[Math.min(this.index + offset, this.tokens.length - 1)];
935
- }
936
- previous() {
937
- return this.tokens[Math.max(0, this.index - 1)];
938
- }
939
- advance() {
940
- const token = this.peek();
941
- if (token.kind !== "eof")
942
- this.index += 1;
943
- return token;
944
- }
945
- at(kind) {
946
- return this.peek().kind === kind;
947
- }
948
- atPunct(glyph) {
949
- const token = this.peek();
950
- return token.kind === "punct" && token.text === glyph;
951
- }
952
- parseIdent() {
953
- const token = this.advance();
954
- return { kind: "ident", name: token.text, span: token.span };
955
- }
956
- isName(token) {
957
- return token.kind === "ident" || token.kind === "keyword";
958
- }
959
- expectName(message) {
960
- if (this.isName(this.peek()))
961
- return this.parseIdent();
962
- this.error(this.peek().span, message);
963
- return undefined;
964
- }
965
- parseTemplatedName(message) {
966
- if (this.atPunct("[")) {
967
- const open = this.advance();
968
- const binding = this.expectIdent("an indexed name needs its loop binding");
969
- if (!binding || !this.expectPunct("]", "an indexed name needs ]")) {
970
- return undefined;
971
- }
972
- const suffix = this.isName(this.peek()) ? this.parseIdent() : undefined;
441
+ return { kind: "call", name, args, span: this.span(start) };
442
+ }
443
+ let target;
444
+ let owner;
445
+ if (["ref", "list"].includes(name) && this.eat("<")) {
446
+ target = this.path();
447
+ this.expect(">");
448
+ }
449
+ const many = name === "ref" && this.eat("[");
450
+ if (many)
451
+ this.expect("]");
452
+ if (this.eat("of"))
453
+ owner = this.path();
454
+ const optional = this.eat("?");
455
+ if (target || owner || optional || many)
973
456
  return {
974
- kind: "ident",
975
- name: `[${binding.name}]${suffix?.name ?? ""}`,
976
- span: {
977
- start: open.span.start,
978
- end: suffix?.span.end ?? this.previous().span.end,
979
- },
457
+ kind: "type",
458
+ name,
459
+ ...(target ? { target } : {}),
460
+ ...(owner ? { owner } : {}),
461
+ optional,
462
+ ...(many ? { many: true } : {}),
463
+ span: this.span(start),
980
464
  };
981
- }
982
- const base = this.expectName(message);
983
- if (!base || !this.atPunct("["))
984
- return base;
985
- this.advance();
986
- const binding = this.expectIdent("an indexed name needs its loop binding");
987
- if (!binding || !this.expectPunct("]", "an indexed name needs ]")) {
988
- return undefined;
989
- }
990
- const suffix = this.isName(this.peek()) ? this.parseIdent() : undefined;
991
- return {
992
- kind: "ident",
993
- name: `${base.name}[${binding.name}]${suffix?.name ?? ""}`,
994
- span: {
995
- start: base.span.start,
996
- end: suffix?.span.end ?? this.previous().span.end,
997
- },
998
- };
999
- }
1000
- canStartExpr(token) {
1001
- return (token.kind === "ident" ||
1002
- token.kind === "keyword" ||
1003
- token.kind === "number" ||
1004
- token.kind === "percent" ||
1005
- token.kind === "string" ||
1006
- (token.kind === "punct" && (token.text === "{" || token.text === "[")));
1007
- }
1008
- parsePath(message) {
1009
- const first = this.expectName(message);
1010
- if (!first)
1011
- return undefined;
1012
- const parts = [first];
1013
- while (this.atPunct(".")) {
1014
- this.advance();
1015
- const part = this.expectName("expected a name after .");
1016
- if (!part)
1017
- return undefined;
1018
- parts.push(part);
1019
- }
1020
- return {
1021
- kind: "path",
1022
- parts,
1023
- span: {
1024
- start: first.span.start,
1025
- end: parts.at(-1)?.span.end ?? first.span.end,
1026
- },
1027
- };
465
+ return { kind: "name", value: name, span: this.span(start) };
1028
466
  }
1029
- parseAngleArguments() {
1030
- this.advance();
1031
- const args = [];
1032
- while (!this.atPunct(">") && !this.at("eof")) {
1033
- const arg = this.parseExpr();
1034
- if (!arg)
1035
- return undefined;
1036
- args.push(arg);
1037
- if (this.atPunct(","))
1038
- this.advance();
1039
- }
1040
- return this.expectPunct(">", "the type argument list never closes")
1041
- ? args
1042
- : undefined;
1043
- }
1044
- parseApplication() {
1045
- const callee = this.parsePath("expected an instrument function name");
1046
- if (!callee)
1047
- return undefined;
1048
- const typeArgs = this.atPunct("<") ? this.parseAngleArguments() : [];
1049
- if (!typeArgs)
1050
- return undefined;
1051
- if (!this.expectPunct("(", "an instrument application needs arguments in parentheses")) {
1052
- return undefined;
1053
- }
1054
- const args = [];
1055
- while (!this.atPunct(")") && !this.at("eof")) {
1056
- const arg = this.parseExpr();
1057
- if (!arg)
1058
- return undefined;
1059
- args.push(arg);
1060
- if (this.atPunct(","))
1061
- this.advance();
1062
- }
1063
- if (!this.expectPunct(")", "the instrument argument list never closes")) {
1064
- return undefined;
1065
- }
1066
- return {
1067
- args,
1068
- callee,
1069
- kind: "apply",
1070
- span: { start: callee.span.start, end: this.previous().span.end },
1071
- typeArgs,
1072
- };
1073
- }
1074
- expectIdent(message) {
1075
- if (this.at("ident"))
1076
- return this.parseIdent();
1077
- this.error(this.peek().span, message);
1078
- return undefined;
1079
- }
1080
- expectPunct(glyph, message) {
1081
- if (this.atPunct(glyph)) {
1082
- this.advance();
1083
- return true;
1084
- }
1085
- this.error(this.peek().span, message);
1086
- return false;
1087
- }
1088
- spanFrom(start) {
1089
- return { end: this.previous().span.end, start: start.start };
1090
- }
1091
- error(span, message) {
1092
- this.diagnostics.push({ message, span });
1093
- }
1094
- }
1095
- /**
1096
- * Convert a percent literal's raw digits to exact basis points, or refuse
1097
- * when the literal is finer than a basis point. "99.5" -> 9950; "0.05" -> 5.
1098
- */
1099
- export function percentToBps(raw) {
1100
- const [whole = "0", fraction = ""] = raw.split(".");
1101
- if (fraction.length > 2)
1102
- return undefined;
1103
- const scaled = fraction.padEnd(2, "0");
1104
- return Number(whole) * 100 + Number(scaled === "" ? "0" : scaled);
1105
467
  }
1106
468
  //# sourceMappingURL=parse.js.map