@hyperscale0/hsx 1.0.0 → 2.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.
- package/CHANGELOG.md +28 -4
- package/README.md +58 -38
- package/dist/src/ast.d.ts +4 -2
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts +8 -2
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +8 -1
- package/dist/src/compile.js.map +1 -1
- package/dist/src/cost.d.ts +3 -1
- package/dist/src/cost.d.ts.map +1 -1
- package/dist/src/cost.js +124 -94
- package/dist/src/cost.js.map +1 -1
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +96 -5
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/emit.d.ts +3 -1
- package/dist/src/emit.d.ts.map +1 -1
- package/dist/src/emit.js +27 -10
- package/dist/src/emit.js.map +1 -1
- package/dist/src/format.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/ir.d.ts.map +1 -1
- package/dist/src/lex.d.ts.map +1 -1
- package/dist/src/lsp/server.js.map +1 -1
- package/dist/src/modules.js +7 -3
- package/dist/src/modules.js.map +1 -1
- package/dist/src/parse.js +3 -3
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.js +41 -41
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/std-library.js +2 -2
- package/dist/src/typecheck.d.ts.map +1 -1
- package/dist/src/typecheck.js +948 -157
- package/dist/src/typecheck.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/README.md +10 -1
- package/docs/assets/hsx.svg +18 -0
- package/docs/guide/01-first-program.md +1 -1
- package/docs/guide/02-money.md +1 -1
- package/docs/guide/03-instruments.md +44 -3
- package/docs/guide/04-lifecycles.md +18 -3
- package/docs/guide/05-fees-and-splits.md +2 -2
- package/docs/guide/06-schedules.md +1 -1
- package/docs/guide/07-composition.md +8 -2
- package/docs/guide/08-writing-a-module.md +13 -2
- package/docs/guide/09-cost.md +3 -3
- package/docs/guide/10-diagnostics.md +1 -1
- package/docs/llms-full.txt +473 -190
- package/docs/llms.txt +2 -2
- package/docs/playground.md +4 -3
- package/docs/reference/cli.md +2 -2
- package/docs/reference/diagnostics.md +243 -37
- package/docs/reference/grammar.md +2 -2
- package/docs/reference/std/advance.md +2 -2
- package/docs/reference/std/cancellable_booking.md +10 -10
- package/docs/reference/std/captured_payment.md +14 -14
- package/docs/reference/std/conditional_disbursement.md +3 -3
- package/docs/reference/std/credit_facility.md +2 -2
- package/docs/reference/std/held_payment.md +37 -33
- package/docs/reference/std/instant_transfer.md +8 -8
- package/docs/reference/std/metered.md +2 -2
- package/docs/reference/std/pooled_split.md +2 -2
- package/docs/reference/std/premium_forward.md +13 -13
- package/docs/reference/std/reconciled_payout.md +2 -2
- package/docs/reference/std/recurring_collection.md +2 -2
- package/docs/reference/std/rotating_pool.md +2 -2
- package/docs/reference/std/scheduled.md +18 -18
- package/docs/reference/std/security_deposit.md +14 -14
- package/docs/reference/std/settlement_batch.md +2 -2
- package/docs/reference/std/swap.md +18 -18
- package/docs/reference/std/threshold_pool.md +2 -2
- package/docs/reference/std/weighted_distribution.md +2 -2
- package/docs/reference/types.md +1 -1
- package/docs/reference/udl-output.md +1 -1
- package/examples/01-first-program/README.md +1 -1
- package/examples/01-first-program/tip-jar.hsx +1 -1
- package/examples/02-imports-and-archetypes/photo-booth.hsx +1 -1
- package/examples/03-diagnostics/corner-shop-fixed.hsx +1 -1
- package/examples/03-diagnostics/corner-shop.hsx +1 -1
- package/examples/04-complete-product/study-hall.hsx +1 -1
- package/examples/05-watch-club/README.md +0 -3
- package/examples/05-watch-club/watch-club.hsx +1 -1
- package/examples/README.md +35 -4
- package/examples/advance/advance.hsx +1 -1
- package/examples/advance/advance.udl +16 -11
- package/examples/cancellable_booking/cancellable_booking.hsx +1 -1
- package/examples/cancellable_booking/cancellable_booking.udl +13 -11
- package/examples/captured_payment/captured_payment.hsx +1 -1
- package/examples/captured_payment/captured_payment.udl +10 -7
- package/examples/conditional_disbursement/conditional_disbursement.hsx +1 -1
- package/examples/conditional_disbursement/conditional_disbursement.udl +7 -5
- package/examples/cost-table.json +1542 -382
- package/examples/credit_facility/credit_facility.hsx +1 -1
- package/examples/credit_facility/credit_facility.udl +26 -19
- package/examples/held_payment/held_payment.hsx +1 -1
- package/examples/held_payment/held_payment.udl +61 -15
- package/examples/instant_transfer/instant_transfer.hsx +1 -1
- package/examples/instant_transfer/instant_transfer.udl +17 -11
- package/examples/metered/metered.hsx +1 -1
- package/examples/metered/metered.udl +8 -6
- package/examples/pooled_split/pooled_split.hsx +1 -1
- package/examples/pooled_split/pooled_split.udl +16 -13
- package/examples/premium_forward/premium_forward.hsx +1 -1
- package/examples/premium_forward/premium_forward.udl +22 -16
- package/examples/reconciled_payout/reconciled_payout.hsx +1 -1
- package/examples/reconciled_payout/reconciled_payout.udl +4 -2
- package/examples/recurring_collection/recurring_collection.hsx +1 -1
- package/examples/recurring_collection/recurring_collection.udl +20 -15
- package/examples/rotating_pool/rotating_pool.hsx +1 -1
- package/examples/rotating_pool/rotating_pool.udl +92 -82
- package/examples/scheduled/scheduled.hsx +1 -1
- package/examples/scheduled/scheduled.udl +15 -10
- package/examples/security_deposit/security_deposit.hsx +1 -1
- package/examples/security_deposit/security_deposit.udl +19 -5
- package/examples/settlement_batch/settlement_batch.hsx +1 -1
- package/examples/swap/swap.hsx +1 -1
- package/examples/swap/swap.udl +34 -28
- package/examples/threshold_pool/threshold_pool.hsx +1 -1
- package/examples/threshold_pool/threshold_pool.udl +14 -12
- package/examples/weighted_distribution/weighted_distribution.hsx +1 -1
- package/examples/weighted_distribution/weighted_distribution.udl +6 -4
- package/package.json +6 -5
- package/skills/hsx/SKILL.md +26 -26
- package/src/ast.ts +4 -2
- package/src/cli.ts +4 -4
- package/src/compile.ts +14 -3
- package/src/cost.ts +176 -104
- package/src/diagnostics.ts +99 -4
- package/src/emit.ts +32 -18
- package/src/index.ts +2 -1
- package/src/modules.ts +9 -3
- package/src/parse.ts +3 -3
- package/src/std-bundle.ts +41 -41
- package/src/std-library.ts +2 -2
- package/src/typecheck.ts +1251 -235
- package/src/version.ts +1 -1
- package/std/SEMANTICS.md +12 -3
- package/std/{settlements → money_flows}/advance.hsx +1 -1
- package/std/{settlements → money_flows}/cancellable_booking.hsx +4 -3
- package/std/{settlements → money_flows}/captured_payment.hsx +35 -6
- package/std/{settlements → money_flows}/conditional_disbursement.hsx +17 -16
- package/std/{settlements → money_flows}/credit_facility.hsx +1 -2
- package/std/{settlements → money_flows}/held_payment.hsx +61 -25
- package/std/money_flows/index.hsx +3 -0
- package/std/{settlements → money_flows}/instant_transfer.hsx +2 -3
- package/std/{settlements → money_flows}/metered.hsx +1 -1
- package/std/{settlements → money_flows}/pooled_split.hsx +3 -3
- package/std/{settlements → money_flows}/premium_forward.hsx +22 -23
- package/std/{settlements → money_flows}/reconciled_payout.hsx +3 -3
- package/std/{settlements → money_flows}/recurring_collection.hsx +1 -1
- package/std/{settlements → money_flows}/rotating_pool.hsx +8 -6
- package/std/{settlements → money_flows}/scheduled.hsx +22 -27
- package/std/{settlements → money_flows}/security_deposit.hsx +38 -14
- package/std/{settlements → money_flows}/settlement_batch.hsx +3 -3
- package/std/{settlements → money_flows}/swap.hsx +41 -16
- package/std/{settlements → money_flows}/threshold_pool.hsx +5 -5
- package/std/{settlements → money_flows}/weighted_distribution.hsx +6 -6
- package/docs/sessions/2026-09-02-two-instruments.hsx +0 -21
- package/docs/sessions/2026-09-02-two-instruments.md +0 -41
- package/std/settlements/index.hsx +0 -3
package/src/typecheck.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
ExposeDecl,
|
|
16
16
|
IdentExpr,
|
|
17
17
|
InstrumentDecl,
|
|
18
|
+
ModuleDecl,
|
|
18
19
|
PortDecl,
|
|
19
20
|
Program,
|
|
20
21
|
ProgramDecl,
|
|
@@ -40,6 +41,7 @@ const CAMEL_CASE = /^[a-z][A-Za-z0-9]*$/;
|
|
|
40
41
|
const CURRENCY = /^[A-Z]{3}$/;
|
|
41
42
|
const MONEY_PATTERN = "^[1-9][0-9]{0,17}$";
|
|
42
43
|
const OPTIONAL_MONEY_PATTERN = "^(0|[1-9][0-9]{0,17})$";
|
|
44
|
+
const ACCOUNT_PATTERN = "^acct_(sandbox|live)_[a-z0-9]{8,64}$";
|
|
43
45
|
const MAX_COMPREHENSION_EXPANSIONS = 256;
|
|
44
46
|
const NONE_SENTINEL = "__hsx_none__";
|
|
45
47
|
|
|
@@ -55,6 +57,7 @@ interface ConcreteInstrument {
|
|
|
55
57
|
readonly generatedPrefix: boolean;
|
|
56
58
|
readonly name: IdentExpr;
|
|
57
59
|
readonly parties: ReadonlySet<string>;
|
|
60
|
+
readonly ports: ReadonlyMap<string, readonly PortDecl[]>;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
const actionClauseBySpelling: ReadonlyMap<string, ClauseDefinition> = new Map(
|
|
@@ -130,6 +133,32 @@ export function checkGeneralProgram(
|
|
|
130
133
|
.filter((decl): decl is PortDecl => decl.kind === "port")
|
|
131
134
|
.map((decl) => [decl.name.name, decl]),
|
|
132
135
|
);
|
|
136
|
+
const checkedPorts = new Set<PortDecl>();
|
|
137
|
+
const checkPortDeclarations = (
|
|
138
|
+
declared: ReadonlyMap<string, PortDecl>,
|
|
139
|
+
knownParties: ReadonlySet<string>,
|
|
140
|
+
) => {
|
|
141
|
+
for (const port of declared.values()) {
|
|
142
|
+
if (checkedPorts.has(port)) continue;
|
|
143
|
+
checkedPorts.add(port);
|
|
144
|
+
const allowed = entry(port.body, "allowed")?.value;
|
|
145
|
+
for (const item of allowed?.kind === "list"
|
|
146
|
+
? allowed.items
|
|
147
|
+
: allowed
|
|
148
|
+
? [allowed]
|
|
149
|
+
: []) {
|
|
150
|
+
const party = nameText(item);
|
|
151
|
+
if (knownParties.has(party)) continue;
|
|
152
|
+
report(
|
|
153
|
+
"HSX1024",
|
|
154
|
+
item.span,
|
|
155
|
+
`decision port ${port.name.name} allows undeclared party ${party}; declare the party before using it`,
|
|
156
|
+
`declare party ${party} with its party kind before using it in this port`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
checkPortDeclarations(ports, parties);
|
|
133
162
|
const assetSubjects: TypedSubject[] = program.decls
|
|
134
163
|
.filter((decl) => decl.kind === "asset")
|
|
135
164
|
.map((decl) => ({
|
|
@@ -200,33 +229,24 @@ export function checkGeneralProgram(
|
|
|
200
229
|
(declaration) =>
|
|
201
230
|
declaration.kind === "use" || declaration.kind === "expose",
|
|
202
231
|
);
|
|
203
|
-
|
|
204
|
-
(declaration)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
) {
|
|
213
|
-
return checkPublishedProgram(
|
|
214
|
-
program,
|
|
215
|
-
options.publishedCatalog,
|
|
216
|
-
header,
|
|
217
|
-
subjects,
|
|
218
|
-
diagnostics,
|
|
219
|
-
report,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
for (const declaration of compositionDeclarations) {
|
|
223
|
-
report(
|
|
224
|
-
"HSX1018",
|
|
225
|
-
declaration.span,
|
|
226
|
-
"this compiler host did not supply a published instrument catalog",
|
|
227
|
-
"pass the canonical UDL catalog as the publishedCatalog compile option",
|
|
228
|
-
);
|
|
232
|
+
if (!options.publishedCatalog) {
|
|
233
|
+
for (const declaration of compositionDeclarations) {
|
|
234
|
+
report(
|
|
235
|
+
"HSX1018",
|
|
236
|
+
declaration.span,
|
|
237
|
+
"this compiler host did not supply a published instrument catalog",
|
|
238
|
+
"pass the canonical UDL catalog as the publishedCatalog compile option",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
229
241
|
}
|
|
242
|
+
const catalogById = options.publishedCatalog
|
|
243
|
+
? new Map(
|
|
244
|
+
options.publishedCatalog.instruments.map((instrument) => [
|
|
245
|
+
instrument.id,
|
|
246
|
+
instrument,
|
|
247
|
+
]),
|
|
248
|
+
)
|
|
249
|
+
: undefined;
|
|
230
250
|
|
|
231
251
|
const aliases = new Map(
|
|
232
252
|
program.decls
|
|
@@ -250,6 +270,8 @@ export function checkGeneralProgram(
|
|
|
250
270
|
}
|
|
251
271
|
|
|
252
272
|
const concrete: ConcreteInstrument[] = [];
|
|
273
|
+
const reachedPorts = new Set<PortDecl>();
|
|
274
|
+
const droppedPorts = new Set<PortDecl>();
|
|
253
275
|
const scopedDeclarations = (
|
|
254
276
|
declarationScope: readonly ApplicationScopeDecl[],
|
|
255
277
|
) => {
|
|
@@ -270,6 +292,7 @@ export function checkGeneralProgram(
|
|
|
270
292
|
scopedTemplates.set(local.name.name, local);
|
|
271
293
|
}
|
|
272
294
|
}
|
|
295
|
+
checkPortDeclarations(scopedPorts, scopedParties);
|
|
273
296
|
checkConstants(declarationScope, scopedAliases, diagnostics);
|
|
274
297
|
return {
|
|
275
298
|
aliases: scopedAliases,
|
|
@@ -286,13 +309,50 @@ export function checkGeneralProgram(
|
|
|
286
309
|
decl.typeParameters.length === 0
|
|
287
310
|
) {
|
|
288
311
|
const scoped = scopedDeclarations(decl.declarationScope ?? []);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
312
|
+
const values = new Map(scoped.constants);
|
|
313
|
+
const dependencies = new Map<string, readonly string[]>();
|
|
314
|
+
const shapes = new Map<string, BlockExpr>();
|
|
315
|
+
for (const [name, port] of scoped.ports) {
|
|
316
|
+
const reference: Extract<Expr, { kind: "port_ref" }> = {
|
|
317
|
+
kind: "port_ref",
|
|
318
|
+
name: port.name,
|
|
319
|
+
span: port.span,
|
|
320
|
+
};
|
|
321
|
+
values.set(name, reference);
|
|
322
|
+
bindPortCompileValues(
|
|
323
|
+
values,
|
|
324
|
+
name,
|
|
325
|
+
reference,
|
|
326
|
+
scoped.ports,
|
|
327
|
+
diagnostics,
|
|
328
|
+
scoped.aliases,
|
|
329
|
+
shapes,
|
|
330
|
+
dependencies,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const selected = selectCompileTimeRows(
|
|
334
|
+
decl.body,
|
|
335
|
+
values,
|
|
336
|
+
diagnostics,
|
|
337
|
+
dependencies,
|
|
338
|
+
);
|
|
339
|
+
const body = expandComprehensions(
|
|
340
|
+
selected,
|
|
341
|
+
diagnostics,
|
|
342
|
+
values,
|
|
343
|
+
dependencies,
|
|
344
|
+
);
|
|
345
|
+
concrete.push(
|
|
346
|
+
...bindActionPorts(
|
|
347
|
+
constructedInstruments(decl.name, body, diagnostics),
|
|
348
|
+
scoped.ports,
|
|
349
|
+
reachedPorts,
|
|
350
|
+
).map((candidate) => ({
|
|
351
|
+
...candidate,
|
|
352
|
+
aliases: scoped.aliases,
|
|
353
|
+
parties: scoped.parties,
|
|
354
|
+
})),
|
|
355
|
+
);
|
|
296
356
|
}
|
|
297
357
|
if (decl.kind !== "instrument_apply") continue;
|
|
298
358
|
const application = decl.application;
|
|
@@ -315,6 +375,7 @@ export function checkGeneralProgram(
|
|
|
315
375
|
);
|
|
316
376
|
continue;
|
|
317
377
|
}
|
|
378
|
+
const boundPorts = new Map<string, PortDecl>();
|
|
318
379
|
const body = instantiate(
|
|
319
380
|
template,
|
|
320
381
|
application,
|
|
@@ -324,23 +385,41 @@ export function checkGeneralProgram(
|
|
|
324
385
|
scoped.parties,
|
|
325
386
|
scoped.aliases,
|
|
326
387
|
scoped.constants,
|
|
388
|
+
boundPorts,
|
|
327
389
|
);
|
|
328
390
|
if (body) {
|
|
391
|
+
const applicationPorts = new Set<PortDecl>();
|
|
329
392
|
const merged = decl.metadata
|
|
330
393
|
? mergeApplicationMetadata(body, decl.metadata, diagnostics)
|
|
331
394
|
: body;
|
|
332
395
|
concrete.push(
|
|
333
|
-
...
|
|
334
|
-
(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
396
|
+
...bindActionPorts(
|
|
397
|
+
constructedInstruments(decl.name, merged, diagnostics),
|
|
398
|
+
boundPorts,
|
|
399
|
+
applicationPorts,
|
|
400
|
+
).map((candidate) => ({
|
|
401
|
+
...candidate,
|
|
402
|
+
aliases: scoped.aliases,
|
|
403
|
+
parties: scoped.parties,
|
|
404
|
+
})),
|
|
340
405
|
);
|
|
406
|
+
for (const port of boundPorts.values()) {
|
|
407
|
+
if (applicationPorts.has(port)) reachedPorts.add(port);
|
|
408
|
+
else droppedPorts.add(port);
|
|
409
|
+
}
|
|
341
410
|
}
|
|
342
411
|
}
|
|
343
412
|
|
|
413
|
+
for (const port of checkedPorts) {
|
|
414
|
+
if (reachedPorts.has(port) && !droppedPorts.has(port)) continue;
|
|
415
|
+
report(
|
|
416
|
+
"HSX1024",
|
|
417
|
+
port.name.span,
|
|
418
|
+
`wired decision port ${port.name.name} reaches no action`,
|
|
419
|
+
`carry ${port.name.name}'s condition into the action it authorizes`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
344
423
|
const allocated = allocateGeneratedPrefixes(concrete);
|
|
345
424
|
|
|
346
425
|
const instruments: TypedInstrument[] = [];
|
|
@@ -356,17 +435,39 @@ export function checkGeneralProgram(
|
|
|
356
435
|
continue;
|
|
357
436
|
}
|
|
358
437
|
ids.add(candidate.name.name);
|
|
438
|
+
if (catalogById?.has(candidate.name.name)) {
|
|
439
|
+
report(
|
|
440
|
+
"HSX1022",
|
|
441
|
+
candidate.name.span,
|
|
442
|
+
`authored instrument ${candidate.name.name} collides with published catalog instrument`,
|
|
443
|
+
"rename the authored instrument to avoid colliding with the published catalog",
|
|
444
|
+
);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
359
447
|
const expandedBody = expandComprehensions(candidate.body, diagnostics);
|
|
360
448
|
const checked = checkInstrument(
|
|
361
449
|
candidate.name,
|
|
362
450
|
expandedBody,
|
|
363
451
|
candidate.parties,
|
|
452
|
+
candidate.ports,
|
|
364
453
|
candidate.aliases,
|
|
365
454
|
diagnostics,
|
|
366
455
|
);
|
|
367
456
|
if (checked) instruments.push(checked);
|
|
368
457
|
}
|
|
369
458
|
|
|
459
|
+
if (options.publishedCatalog) {
|
|
460
|
+
return checkPublishedProgram(
|
|
461
|
+
program,
|
|
462
|
+
options.publishedCatalog,
|
|
463
|
+
header,
|
|
464
|
+
subjects,
|
|
465
|
+
instruments,
|
|
466
|
+
diagnostics,
|
|
467
|
+
report,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
370
471
|
crossInstrumentReferenceDiagnostics(instruments, diagnostics);
|
|
371
472
|
|
|
372
473
|
if (
|
|
@@ -402,6 +503,7 @@ function checkPublishedProgram(
|
|
|
402
503
|
catalog: UdlDocument,
|
|
403
504
|
header: ProgramDecl | undefined,
|
|
404
505
|
authoredSubjects: readonly TypedSubject[],
|
|
506
|
+
authoredInstruments: readonly TypedInstrument[],
|
|
405
507
|
diagnostics: GeneralDiagnostic[],
|
|
406
508
|
report: (
|
|
407
509
|
code: string,
|
|
@@ -411,50 +513,43 @@ function checkPublishedProgram(
|
|
|
411
513
|
severity?: "error" | "warning",
|
|
412
514
|
) => void,
|
|
413
515
|
): GeneralCheckResult {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
if (!header.title || header.title.value.length === 0) {
|
|
418
|
-
report(
|
|
419
|
-
"HSX1015",
|
|
420
|
-
header.span,
|
|
421
|
-
`composed program ${header.name.name} needs a product title`,
|
|
422
|
-
'write the title after the product id, like `program product_name "Product title"`',
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
if (header.name.name.length > 60) {
|
|
426
|
-
report(
|
|
427
|
-
"HSX1015",
|
|
428
|
-
header.name.span,
|
|
429
|
-
"a composed program product id is at most 60 characters",
|
|
430
|
-
"shorten the product id without changing its snake_case spelling",
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
if ((header.title?.value.length ?? 0) > 80) {
|
|
434
|
-
report(
|
|
435
|
-
"HSX1015",
|
|
436
|
-
header.title?.span ?? header.span,
|
|
437
|
-
"a composed program title is at most 80 characters",
|
|
438
|
-
"shorten the product title to 80 characters or fewer",
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const authoredInstruments = program.decls.filter(
|
|
443
|
-
(decl) => decl.kind === "instrument" || decl.kind === "instrument_apply",
|
|
516
|
+
const compositionDeclarations = program.decls.filter(
|
|
517
|
+
(decl) => decl.kind === "use" || decl.kind === "expose",
|
|
444
518
|
);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
519
|
+
if (
|
|
520
|
+
header &&
|
|
521
|
+
(compositionDeclarations.length > 0 || authoredInstruments.length === 0)
|
|
522
|
+
) {
|
|
523
|
+
if (!header.title || header.title.value.length === 0) {
|
|
524
|
+
report(
|
|
525
|
+
"HSX1015",
|
|
526
|
+
header.span,
|
|
527
|
+
`composed program ${header.name.name} needs a product title`,
|
|
528
|
+
'write the title after the product id, like `program product_name "Product title"`',
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (header.name.name.length > 60) {
|
|
532
|
+
report(
|
|
533
|
+
"HSX1015",
|
|
534
|
+
header.name.span,
|
|
535
|
+
"a composed program product id is at most 60 characters",
|
|
536
|
+
"shorten the product id without changing its snake_case spelling",
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
if ((header.title?.value.length ?? 0) > 80) {
|
|
540
|
+
report(
|
|
541
|
+
"HSX1015",
|
|
542
|
+
header.title?.span ?? header.span,
|
|
543
|
+
"a composed program title is at most 80 characters",
|
|
544
|
+
"shorten the product title to 80 characters or fewer",
|
|
545
|
+
);
|
|
546
|
+
}
|
|
452
547
|
}
|
|
453
548
|
|
|
454
549
|
const uses = program.decls.filter(
|
|
455
550
|
(decl): decl is UseDecl => decl.kind === "use",
|
|
456
551
|
);
|
|
457
|
-
if (uses.length === 0) {
|
|
552
|
+
if (header && uses.length === 0 && authoredInstruments.length === 0) {
|
|
458
553
|
report(
|
|
459
554
|
"HSX1016",
|
|
460
555
|
header.span,
|
|
@@ -504,6 +599,9 @@ function checkPublishedProgram(
|
|
|
504
599
|
const exposures = program.decls.filter(
|
|
505
600
|
(decl): decl is ExposeDecl => decl.kind === "expose",
|
|
506
601
|
);
|
|
602
|
+
const authoredById = new Map(
|
|
603
|
+
authoredInstruments.map((instrument) => [instrument.id, instrument]),
|
|
604
|
+
);
|
|
507
605
|
const publicNames = new Map<string, ExposeDecl>();
|
|
508
606
|
const exposedTargets = new Map<string, ExposeDecl>();
|
|
509
607
|
for (const exposure of exposures) {
|
|
@@ -511,11 +609,18 @@ function checkPublishedProgram(
|
|
|
511
609
|
const explicitlyUsed = useById.has(exposure.instrument.name);
|
|
512
610
|
const instrument = selectedById.get(exposure.instrument.name)?.instrument;
|
|
513
611
|
const action = instrument?.actions[exposure.action.name];
|
|
514
|
-
|
|
612
|
+
const authoredInstrument = authoredById.get(exposure.instrument.name);
|
|
613
|
+
const authoredAction = authoredInstrument?.actions.find(
|
|
614
|
+
(candidate) => candidate.name === exposure.action.name,
|
|
615
|
+
);
|
|
616
|
+
if (
|
|
617
|
+
(!explicitlyUsed || !instrument || !action) &&
|
|
618
|
+
(!authoredInstrument || !authoredAction)
|
|
619
|
+
) {
|
|
515
620
|
report(
|
|
516
621
|
"HSX1021",
|
|
517
622
|
exposure.span,
|
|
518
|
-
`${target} is not an action on an explicitly used published instrument`,
|
|
623
|
+
`${target} is not an action on an explicitly used published instrument or authored instrument`,
|
|
519
624
|
"add the matching use declaration and choose one of that instrument's actions",
|
|
520
625
|
);
|
|
521
626
|
continue;
|
|
@@ -546,14 +651,66 @@ function checkPublishedProgram(
|
|
|
546
651
|
exposedTargets.set(target, exposure);
|
|
547
652
|
}
|
|
548
653
|
|
|
654
|
+
const updatedAuthoredInstruments = authoredInstruments.map(
|
|
655
|
+
(authoredInstrument) => {
|
|
656
|
+
const instrumentExposures = exposures.filter(
|
|
657
|
+
(exposure) => exposure.instrument.name === authoredInstrument.id,
|
|
658
|
+
);
|
|
659
|
+
if (instrumentExposures.length === 0) return authoredInstrument;
|
|
660
|
+
const exposureByAction = new Map(
|
|
661
|
+
instrumentExposures.map((exposure) => [exposure.action.name, exposure]),
|
|
662
|
+
);
|
|
663
|
+
return {
|
|
664
|
+
...authoredInstrument,
|
|
665
|
+
actions: authoredInstrument.actions.map((action) => {
|
|
666
|
+
const exposure = exposureByAction.get(action.name);
|
|
667
|
+
if (!exposure) return action;
|
|
668
|
+
return {
|
|
669
|
+
...action,
|
|
670
|
+
origin: exposure.span,
|
|
671
|
+
slots: {
|
|
672
|
+
...action.slots,
|
|
673
|
+
publicAction: exposure.publicName.name,
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}),
|
|
677
|
+
};
|
|
678
|
+
},
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const publishedInstruments = selected.map(({ instrument, origin }) => {
|
|
682
|
+
const projected = projectedPublishedInstrument(instrument, exposures);
|
|
683
|
+
return typedPublishedInstrument(
|
|
684
|
+
projected,
|
|
685
|
+
origin,
|
|
686
|
+
exposures.filter(
|
|
687
|
+
(exposure) => exposure.instrument.name === instrument.id,
|
|
688
|
+
),
|
|
689
|
+
catalog.instruments,
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
const instruments = [...publishedInstruments, ...updatedAuthoredInstruments];
|
|
693
|
+
crossInstrumentReferenceDiagnostics(instruments, diagnostics);
|
|
694
|
+
if (
|
|
695
|
+
header &&
|
|
696
|
+
instruments.length === 0 &&
|
|
697
|
+
!diagnostics.some((diagnostic) => diagnostic.severity === "error")
|
|
698
|
+
) {
|
|
699
|
+
report(
|
|
700
|
+
"HSX1502",
|
|
701
|
+
header.span,
|
|
702
|
+
`program ${header.name.name} emits no instruments`,
|
|
703
|
+
"declare or instantiate at least one instrument",
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
549
707
|
const referencedSubjectKinds = new Set(
|
|
550
708
|
selected.flatMap(({ instrument }) => instrument.subject?.kinds ?? []),
|
|
551
709
|
);
|
|
710
|
+
const defaultSpan = uses[0]?.span ?? header?.span ?? program.span;
|
|
552
711
|
const catalogSubjects = catalog.subjects
|
|
553
712
|
.filter((subject) => referencedSubjectKinds.has(subject.kind))
|
|
554
|
-
.map((subject) =>
|
|
555
|
-
typedPublishedSubject(subject, uses[0]?.span ?? header.span),
|
|
556
|
-
);
|
|
713
|
+
.map((subject) => typedPublishedSubject(subject, defaultSpan));
|
|
557
714
|
const catalogSubjectKinds = new Set(
|
|
558
715
|
catalogSubjects.map((subject) => subject.kind),
|
|
559
716
|
);
|
|
@@ -570,19 +727,14 @@ function checkPublishedProgram(
|
|
|
570
727
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
571
728
|
return { diagnostics };
|
|
572
729
|
}
|
|
730
|
+
const module = program.decls.find(
|
|
731
|
+
(decl): decl is ModuleDecl => decl.kind === "module",
|
|
732
|
+
);
|
|
733
|
+
const name = header?.name.name ?? module?.name.parts.at(-1)?.name ?? "module";
|
|
573
734
|
const typed: TypedProgram = {
|
|
574
|
-
instruments
|
|
575
|
-
const projected = projectedPublishedInstrument(instrument, exposures);
|
|
576
|
-
return typedPublishedInstrument(
|
|
577
|
-
projected,
|
|
578
|
-
origin,
|
|
579
|
-
exposures.filter(
|
|
580
|
-
(exposure) => exposure.instrument.name === instrument.id,
|
|
581
|
-
),
|
|
582
|
-
);
|
|
583
|
-
}),
|
|
735
|
+
instruments,
|
|
584
736
|
kind: "typed_program",
|
|
585
|
-
name
|
|
737
|
+
name,
|
|
586
738
|
origin: program.span,
|
|
587
739
|
subjects: [
|
|
588
740
|
...catalogSubjects,
|
|
@@ -590,7 +742,7 @@ function checkPublishedProgram(
|
|
|
590
742
|
(subject) => !catalogSubjectKinds.has(subject.kind),
|
|
591
743
|
),
|
|
592
744
|
],
|
|
593
|
-
title: header
|
|
745
|
+
title: header?.title?.value ?? sentenceCase(name),
|
|
594
746
|
};
|
|
595
747
|
return { diagnostics, program: typed };
|
|
596
748
|
}
|
|
@@ -694,6 +846,7 @@ function typedPublishedInstrument(
|
|
|
694
846
|
source: UdlInstrument,
|
|
695
847
|
origin: Span,
|
|
696
848
|
exposures: readonly ExposeDecl[],
|
|
849
|
+
catalog: readonly UdlInstrument[],
|
|
697
850
|
): TypedInstrument {
|
|
698
851
|
const exposureByAction = new Map(
|
|
699
852
|
exposures.map((exposure) => [exposure.action.name, exposure]),
|
|
@@ -724,7 +877,7 @@ function typedPublishedInstrument(
|
|
|
724
877
|
origin,
|
|
725
878
|
required: requiredFields.has(name),
|
|
726
879
|
schema,
|
|
727
|
-
type:
|
|
880
|
+
type: publishedFieldType(schema, catalog),
|
|
728
881
|
})),
|
|
729
882
|
id: source.id,
|
|
730
883
|
origin,
|
|
@@ -735,6 +888,31 @@ function typedPublishedInstrument(
|
|
|
735
888
|
};
|
|
736
889
|
}
|
|
737
890
|
|
|
891
|
+
/** Recover the semantic types retained by UDL before checking mixed relations. */
|
|
892
|
+
function publishedFieldType(
|
|
893
|
+
schema: Readonly<Record<string, JsonValue>>,
|
|
894
|
+
catalog: readonly UdlInstrument[],
|
|
895
|
+
): HsxType {
|
|
896
|
+
if (schema.type === "integer") return { kind: "integer" };
|
|
897
|
+
if (schema.type === "boolean") return { kind: "boolean" };
|
|
898
|
+
if (schema.type !== "string") return { kind: "unknown" };
|
|
899
|
+
if (
|
|
900
|
+
schema.pattern === MONEY_PATTERN ||
|
|
901
|
+
schema.pattern === OPTIONAL_MONEY_PATTERN
|
|
902
|
+
) {
|
|
903
|
+
const currency = schema["x-hyperscale-currency"];
|
|
904
|
+
return typeof currency === "string"
|
|
905
|
+
? { currency, kind: "money" }
|
|
906
|
+
: { kind: "money" };
|
|
907
|
+
}
|
|
908
|
+
const target = catalog.find(
|
|
909
|
+
(instrument) =>
|
|
910
|
+
schema.pattern ===
|
|
911
|
+
`^${instrument.idPrefix}_(sandbox|live)_[a-z0-9]{8,64}$`,
|
|
912
|
+
);
|
|
913
|
+
return target ? { kind: "ref", target: target.id } : { kind: "text" };
|
|
914
|
+
}
|
|
915
|
+
|
|
738
916
|
function typedPublishedSubject(
|
|
739
917
|
subject: UdlDocument["subjects"][number],
|
|
740
918
|
origin: Span,
|
|
@@ -758,6 +936,7 @@ const applicationMetadataKeys = new Set([
|
|
|
758
936
|
"action",
|
|
759
937
|
"agent_description",
|
|
760
938
|
"description",
|
|
939
|
+
"journeys",
|
|
761
940
|
"nav",
|
|
762
941
|
"navigation",
|
|
763
942
|
"summary",
|
|
@@ -769,6 +948,7 @@ const applicationMetadataKeys = new Set([
|
|
|
769
948
|
const applicationActionMetadataKeys = new Set([
|
|
770
949
|
"agent_description",
|
|
771
950
|
"description",
|
|
951
|
+
"examples",
|
|
772
952
|
"public",
|
|
773
953
|
"public_action",
|
|
774
954
|
"summary",
|
|
@@ -875,6 +1055,74 @@ function mergeApplicationMetadata(
|
|
|
875
1055
|
return { ...body, entries };
|
|
876
1056
|
}
|
|
877
1057
|
|
|
1058
|
+
function bindActionPorts(
|
|
1059
|
+
candidates: ReturnType<typeof constructedInstruments>,
|
|
1060
|
+
bindings: ReadonlyMap<string, PortDecl>,
|
|
1061
|
+
reached: Set<PortDecl>,
|
|
1062
|
+
) {
|
|
1063
|
+
const declarations = new Map(
|
|
1064
|
+
[...bindings.values()].map((port) => [port.name.name, port]),
|
|
1065
|
+
);
|
|
1066
|
+
return candidates.map((candidate) => {
|
|
1067
|
+
const ports = new Map<string, readonly PortDecl[]>();
|
|
1068
|
+
const entries = candidate.body.entries.map((row) => {
|
|
1069
|
+
if (row.key.name !== "action" || row.value.kind !== "block") return row;
|
|
1070
|
+
const action = row.qualifiers[0]?.name;
|
|
1071
|
+
if (!action) return row;
|
|
1072
|
+
const declared = (row.conditionPorts ?? []).flatMap((name) => {
|
|
1073
|
+
const port = declarations.get(name);
|
|
1074
|
+
if (!port) return [];
|
|
1075
|
+
reached.add(port);
|
|
1076
|
+
return [port];
|
|
1077
|
+
});
|
|
1078
|
+
if (!declared.length) return row;
|
|
1079
|
+
ports.set(action, declared);
|
|
1080
|
+
if (entry(row.value, "port")) return row;
|
|
1081
|
+
const source = declared[0]!;
|
|
1082
|
+
const allowed = entry(source.body, "allowed")?.value;
|
|
1083
|
+
const span = source.span;
|
|
1084
|
+
return {
|
|
1085
|
+
...row,
|
|
1086
|
+
value: {
|
|
1087
|
+
...row.value,
|
|
1088
|
+
entries: [
|
|
1089
|
+
...row.value.entries,
|
|
1090
|
+
{
|
|
1091
|
+
key: { kind: "ident" as const, name: "port", span },
|
|
1092
|
+
qualifiers: [],
|
|
1093
|
+
span,
|
|
1094
|
+
value: {
|
|
1095
|
+
kind: "block" as const,
|
|
1096
|
+
span,
|
|
1097
|
+
entries: [
|
|
1098
|
+
{
|
|
1099
|
+
key: {
|
|
1100
|
+
kind: "ident" as const,
|
|
1101
|
+
name: "allowed_parties",
|
|
1102
|
+
span,
|
|
1103
|
+
},
|
|
1104
|
+
qualifiers: [],
|
|
1105
|
+
span,
|
|
1106
|
+
value:
|
|
1107
|
+
allowed?.kind === "list"
|
|
1108
|
+
? allowed
|
|
1109
|
+
: {
|
|
1110
|
+
kind: "list" as const,
|
|
1111
|
+
span,
|
|
1112
|
+
items: allowed ? [allowed] : [],
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
],
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
],
|
|
1119
|
+
},
|
|
1120
|
+
};
|
|
1121
|
+
});
|
|
1122
|
+
return { ...candidate, body: { ...candidate.body, entries }, ports };
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
878
1126
|
function constructedInstruments(
|
|
879
1127
|
rootName: IdentExpr,
|
|
880
1128
|
body: BlockExpr,
|
|
@@ -1089,7 +1337,10 @@ function instantiate(
|
|
|
1089
1337
|
parties: ReadonlySet<string>,
|
|
1090
1338
|
aliases: ReadonlyMap<string, Expr>,
|
|
1091
1339
|
constants: ReadonlyMap<string, Expr>,
|
|
1340
|
+
boundPorts: Map<string, PortDecl>,
|
|
1092
1341
|
): BlockExpr | undefined {
|
|
1342
|
+
const loweredPortShapes = new Map<string, BlockExpr>();
|
|
1343
|
+
const portDependencies = new Map<string, readonly string[]>();
|
|
1093
1344
|
const partyKeyedParameters = partyKeyedBlockParameters(template, aliases);
|
|
1094
1345
|
const named = new Map<string, Expr>();
|
|
1095
1346
|
const positional: Expr[] = [];
|
|
@@ -1169,9 +1420,14 @@ function instantiate(
|
|
|
1169
1420
|
if (value) values.set(parameter.name.name, value);
|
|
1170
1421
|
});
|
|
1171
1422
|
for (const [name, value] of named) values.set(name, value);
|
|
1423
|
+
const resolvedArgs: Array<{
|
|
1424
|
+
readonly parameter: InstrumentDecl["parameters"][number];
|
|
1425
|
+
readonly value: Expr | undefined;
|
|
1426
|
+
}> = [];
|
|
1172
1427
|
let position = 0;
|
|
1173
1428
|
for (const parameter of template.parameters) {
|
|
1174
1429
|
const value = named.get(parameter.name.name) ?? positional[position++];
|
|
1430
|
+
resolvedArgs.push({ parameter, value });
|
|
1175
1431
|
if (!value) {
|
|
1176
1432
|
if (optionalInner(parameter.type)) {
|
|
1177
1433
|
values.set(parameter.name.name, booleanExpr(false, parameter.span));
|
|
@@ -1253,17 +1509,30 @@ function instantiate(
|
|
|
1253
1509
|
} else {
|
|
1254
1510
|
values.set(parameter.name.name, value);
|
|
1255
1511
|
}
|
|
1256
|
-
|
|
1512
|
+
}
|
|
1513
|
+
for (const { parameter, value } of resolvedArgs) {
|
|
1514
|
+
if (value?.kind === "port_ref") {
|
|
1515
|
+
const port = ports.get(value.name.name);
|
|
1516
|
+
if (port) boundPorts.set(parameter.name.name, port);
|
|
1257
1517
|
bindPortCompileValues(
|
|
1258
1518
|
values,
|
|
1259
1519
|
parameter.name.name,
|
|
1260
1520
|
value,
|
|
1261
1521
|
ports,
|
|
1262
1522
|
diagnostics,
|
|
1523
|
+
aliases,
|
|
1524
|
+
loweredPortShapes,
|
|
1525
|
+
portDependencies,
|
|
1263
1526
|
);
|
|
1264
1527
|
}
|
|
1265
1528
|
}
|
|
1266
|
-
|
|
1529
|
+
checkClaimAmountPortField(resolvedArgs, ports, aliases, diagnostics);
|
|
1530
|
+
const selected = selectCompileTimeRows(
|
|
1531
|
+
template.body,
|
|
1532
|
+
values,
|
|
1533
|
+
diagnostics,
|
|
1534
|
+
portDependencies,
|
|
1535
|
+
);
|
|
1267
1536
|
const dependencyDiagnosticStart = diagnostics.length;
|
|
1268
1537
|
for (const [name, parameter] of missingOptional) {
|
|
1269
1538
|
if (!referencesIdentifier(selected, name, values)) continue;
|
|
@@ -1278,7 +1547,12 @@ function instantiate(
|
|
|
1278
1547
|
});
|
|
1279
1548
|
}
|
|
1280
1549
|
if (diagnostics.length > dependencyDiagnosticStart) return undefined;
|
|
1281
|
-
const body =
|
|
1550
|
+
const body = expandComprehensions(
|
|
1551
|
+
selected,
|
|
1552
|
+
diagnostics,
|
|
1553
|
+
values,
|
|
1554
|
+
portDependencies,
|
|
1555
|
+
);
|
|
1282
1556
|
return {
|
|
1283
1557
|
...body,
|
|
1284
1558
|
entries: body.entries.map((row) => {
|
|
@@ -1499,12 +1773,268 @@ function partyKeyedBlockParameters(
|
|
|
1499
1773
|
);
|
|
1500
1774
|
}
|
|
1501
1775
|
|
|
1776
|
+
function shapeTypeText(expr: Expr): string {
|
|
1777
|
+
if (expr.kind === "ident") return expr.name;
|
|
1778
|
+
if (expr.kind === "block") return "block";
|
|
1779
|
+
if (expr.kind === "list") return "list";
|
|
1780
|
+
if (expr.kind === "number") return expr.raw;
|
|
1781
|
+
if (expr.kind === "string") return `"${expr.value}"`;
|
|
1782
|
+
if (expr.kind === "boolean") return String(expr.value);
|
|
1783
|
+
if (expr.kind === "call") {
|
|
1784
|
+
const args = expr.args.map(shapeTypeText).join(", ");
|
|
1785
|
+
return `${expr.callee.name}(${args})`;
|
|
1786
|
+
}
|
|
1787
|
+
if (expr.kind === "type_apply") {
|
|
1788
|
+
const args = expr.args.map(shapeTypeText).join(", ");
|
|
1789
|
+
return `${expr.callee.name}<${args}>`;
|
|
1790
|
+
}
|
|
1791
|
+
return nameText(expr);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function lowerPortShapeField(
|
|
1795
|
+
port: PortDecl,
|
|
1796
|
+
row: Entry,
|
|
1797
|
+
aliases: ReadonlyMap<string, Expr>,
|
|
1798
|
+
diagnostics: GeneralDiagnostic[],
|
|
1799
|
+
): Entry | undefined {
|
|
1800
|
+
const type = typeOf(row.value, aliases);
|
|
1801
|
+
if (
|
|
1802
|
+
![
|
|
1803
|
+
"text",
|
|
1804
|
+
"money",
|
|
1805
|
+
"date",
|
|
1806
|
+
"integer",
|
|
1807
|
+
"boolean",
|
|
1808
|
+
"account",
|
|
1809
|
+
"ref",
|
|
1810
|
+
"bps",
|
|
1811
|
+
"percent",
|
|
1812
|
+
].includes(type.kind) ||
|
|
1813
|
+
((type.kind === "money" || type.kind === "account") &&
|
|
1814
|
+
(!type.currency || !CURRENCY.test(type.currency)))
|
|
1815
|
+
) {
|
|
1816
|
+
diagnostics.push({
|
|
1817
|
+
code: "HSX1025",
|
|
1818
|
+
fix: `declare ${row.key.name} as text, money(CUR), date, integer, boolean, account(CUR), ref<instrument_id>, id(instrument_id), bps, or percent`,
|
|
1819
|
+
message: `decision port ${port.name.name} shape field ${row.key.name} has unknown type ${shapeTypeText(row.value)}`,
|
|
1820
|
+
severity: "error",
|
|
1821
|
+
span: row.value.span,
|
|
1822
|
+
});
|
|
1823
|
+
return undefined;
|
|
1824
|
+
}
|
|
1825
|
+
const extras: Record<string, string | number> = {};
|
|
1826
|
+
if (type.kind === "text") {
|
|
1827
|
+
extras.description =
|
|
1828
|
+
"Required decision reference retained in the operation receipt";
|
|
1829
|
+
extras.max_length = 180;
|
|
1830
|
+
extras.min_length = 1;
|
|
1831
|
+
} else if (type.kind === "money") {
|
|
1832
|
+
extras.description = `Decided amount in ${type.currency} minor units`;
|
|
1833
|
+
} else if (type.kind === "ref") {
|
|
1834
|
+
extras.max_length = 180;
|
|
1835
|
+
extras.min_length = 1;
|
|
1836
|
+
if (type.target)
|
|
1837
|
+
extras.pattern = `^${prefixFor(type.target)}_(sandbox|live)_[a-z0-9]{8,64}$`;
|
|
1838
|
+
} else if (type.kind === "account") {
|
|
1839
|
+
extras.pattern = ACCOUNT_PATTERN;
|
|
1840
|
+
}
|
|
1841
|
+
const scalarEntries = (
|
|
1842
|
+
values: Readonly<Record<string, JsonValue>>,
|
|
1843
|
+
): Entry[] =>
|
|
1844
|
+
Object.entries(values).map(([name, value]) => ({
|
|
1845
|
+
key: { kind: "ident", name, span: row.span },
|
|
1846
|
+
qualifiers: [],
|
|
1847
|
+
span: row.span,
|
|
1848
|
+
value:
|
|
1849
|
+
typeof value === "number"
|
|
1850
|
+
? { kind: "number", raw: String(value), span: row.span }
|
|
1851
|
+
: { kind: "string", value: String(value), span: row.span },
|
|
1852
|
+
}));
|
|
1853
|
+
// Port inputs require every key, but a caller-decided amount may be zero.
|
|
1854
|
+
const field = lowerField(
|
|
1855
|
+
{
|
|
1856
|
+
...row,
|
|
1857
|
+
value: {
|
|
1858
|
+
kind: "block",
|
|
1859
|
+
span: row.span,
|
|
1860
|
+
entries: [
|
|
1861
|
+
{
|
|
1862
|
+
key: { kind: "ident", name: "type", span: row.span },
|
|
1863
|
+
qualifiers: [],
|
|
1864
|
+
span: row.span,
|
|
1865
|
+
value: row.value,
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
key: { kind: "ident", name: "optional", span: row.span },
|
|
1869
|
+
qualifiers: [],
|
|
1870
|
+
span: row.span,
|
|
1871
|
+
value: { kind: "boolean", value: true, span: row.span },
|
|
1872
|
+
},
|
|
1873
|
+
...scalarEntries(extras),
|
|
1874
|
+
],
|
|
1875
|
+
},
|
|
1876
|
+
},
|
|
1877
|
+
aliases,
|
|
1878
|
+
diagnostics,
|
|
1879
|
+
);
|
|
1880
|
+
const schema =
|
|
1881
|
+
type.kind === "text"
|
|
1882
|
+
? {
|
|
1883
|
+
type: field.schema.type!,
|
|
1884
|
+
description: field.schema.description!,
|
|
1885
|
+
...field.schema,
|
|
1886
|
+
}
|
|
1887
|
+
: field.schema;
|
|
1888
|
+
|
|
1889
|
+
return {
|
|
1890
|
+
key: row.key,
|
|
1891
|
+
qualifiers: [],
|
|
1892
|
+
span: row.span,
|
|
1893
|
+
value: {
|
|
1894
|
+
kind: "block",
|
|
1895
|
+
span: row.span,
|
|
1896
|
+
entries: scalarEntries(schema),
|
|
1897
|
+
},
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function lowerPortShape(
|
|
1902
|
+
port: PortDecl,
|
|
1903
|
+
rawShape: Expr,
|
|
1904
|
+
aliases: ReadonlyMap<string, Expr>,
|
|
1905
|
+
diagnostics: GeneralDiagnostic[],
|
|
1906
|
+
): BlockExpr {
|
|
1907
|
+
if (rawShape.kind !== "block") {
|
|
1908
|
+
diagnostics.push({
|
|
1909
|
+
code: "HSX1025",
|
|
1910
|
+
fix: "declare shape as a block of fields, like shape { note: text; }",
|
|
1911
|
+
message: `decision port ${port.name.name} shape must be a block`,
|
|
1912
|
+
severity: "error",
|
|
1913
|
+
span: rawShape.span,
|
|
1914
|
+
});
|
|
1915
|
+
return { entries: [], kind: "block", span: rawShape.span };
|
|
1916
|
+
}
|
|
1917
|
+
const entries: Entry[] = [];
|
|
1918
|
+
const names = new Set<string>();
|
|
1919
|
+
for (const row of rawShape.entries) {
|
|
1920
|
+
if (names.has(row.key.name)) {
|
|
1921
|
+
diagnostics.push({
|
|
1922
|
+
code: "HSX1004",
|
|
1923
|
+
fix: `keep one ${row.key.name} field in decision port ${port.name.name}`,
|
|
1924
|
+
message: `decision port ${port.name.name} shape field ${row.key.name} is declared twice`,
|
|
1925
|
+
severity: "error",
|
|
1926
|
+
span: row.key.span,
|
|
1927
|
+
});
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
names.add(row.key.name);
|
|
1931
|
+
const lowered = lowerPortShapeField(port, row, aliases, diagnostics);
|
|
1932
|
+
if (lowered) entries.push(lowered);
|
|
1933
|
+
}
|
|
1934
|
+
return {
|
|
1935
|
+
entries,
|
|
1936
|
+
kind: "block",
|
|
1937
|
+
span: rawShape.span,
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
function checkClaimAmountPortField(
|
|
1942
|
+
resolvedArgs: ReadonlyArray<{
|
|
1943
|
+
readonly parameter: InstrumentDecl["parameters"][number];
|
|
1944
|
+
readonly value: Expr | undefined;
|
|
1945
|
+
}>,
|
|
1946
|
+
ports: ReadonlyMap<string, PortDecl>,
|
|
1947
|
+
aliases: ReadonlyMap<string, Expr>,
|
|
1948
|
+
diagnostics: GeneralDiagnostic[],
|
|
1949
|
+
): void {
|
|
1950
|
+
for (const { value } of resolvedArgs) {
|
|
1951
|
+
if (value?.kind !== "block") continue;
|
|
1952
|
+
const fieldEntry = entry(value, "field");
|
|
1953
|
+
const boundEntry = entry(value, "bound");
|
|
1954
|
+
if (!fieldEntry || !boundEntry) continue;
|
|
1955
|
+
|
|
1956
|
+
const claimFieldName =
|
|
1957
|
+
fieldEntry.value.kind === "ident"
|
|
1958
|
+
? fieldEntry.value.name
|
|
1959
|
+
: fieldEntry.value.kind === "string"
|
|
1960
|
+
? fieldEntry.value.value
|
|
1961
|
+
: undefined;
|
|
1962
|
+
const boundFieldName =
|
|
1963
|
+
boundEntry.value.kind === "ident"
|
|
1964
|
+
? boundEntry.value.name
|
|
1965
|
+
: boundEntry.value.kind === "string"
|
|
1966
|
+
? boundEntry.value.value
|
|
1967
|
+
: undefined;
|
|
1968
|
+
if (!claimFieldName || !boundFieldName) continue;
|
|
1969
|
+
|
|
1970
|
+
const boundArg = resolvedArgs.find(
|
|
1971
|
+
(arg) =>
|
|
1972
|
+
(arg.value?.kind === "binding" &&
|
|
1973
|
+
arg.value.name.name === boundFieldName) ||
|
|
1974
|
+
arg.parameter.name.name === boundFieldName,
|
|
1975
|
+
);
|
|
1976
|
+
const boundTypeExpr =
|
|
1977
|
+
boundArg?.value?.kind === "binding"
|
|
1978
|
+
? boundArg.value.type
|
|
1979
|
+
: boundArg?.value;
|
|
1980
|
+
if (!boundTypeExpr) continue;
|
|
1981
|
+
const boundType = typeOf(boundTypeExpr, aliases);
|
|
1982
|
+
if (boundType.kind !== "money") continue;
|
|
1983
|
+
|
|
1984
|
+
const portArg = resolvedArgs.find(
|
|
1985
|
+
(arg) => arg.parameter.name.name === "claim",
|
|
1986
|
+
);
|
|
1987
|
+
if (portArg?.value?.kind !== "port_ref") continue;
|
|
1988
|
+
const portDecl = ports.get(portArg.value.name.name);
|
|
1989
|
+
if (!portDecl) continue;
|
|
1990
|
+
const shapeExpr = entry(portDecl.body, "shape")?.value;
|
|
1991
|
+
if (shapeExpr?.kind !== "block") continue;
|
|
1992
|
+
const shapeField = entry(shapeExpr, claimFieldName);
|
|
1993
|
+
if (!shapeField) {
|
|
1994
|
+
diagnostics.push({
|
|
1995
|
+
code: "HSX1011",
|
|
1996
|
+
fix: `add ${claimFieldName}: money(${boundType.currency ?? "CUR"}) to the shape of decision port ${portDecl.name.name}`,
|
|
1997
|
+
message: `decision port ${portDecl.name.name} shape lacks claim field ${claimFieldName}`,
|
|
1998
|
+
severity: "error",
|
|
1999
|
+
span: portArg.value.span,
|
|
2000
|
+
});
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
const actualType = typeOf(shapeField.value, aliases);
|
|
2005
|
+
if (actualType.kind !== "money") {
|
|
2006
|
+
diagnostics.push({
|
|
2007
|
+
code: "HSX1104",
|
|
2008
|
+
fix: `pass money<${boundType.currency ?? "CUR"}> to ${shapeField.key.name}`,
|
|
2009
|
+
message: `${shapeField.key.name} needs money<${boundType.currency ?? "CUR"}> but receives ${argumentWords(shapeField.value, aliases)}`,
|
|
2010
|
+
severity: "error",
|
|
2011
|
+
span: shapeField.value.span,
|
|
2012
|
+
});
|
|
2013
|
+
} else if (
|
|
2014
|
+
actualType.currency &&
|
|
2015
|
+
boundType.currency &&
|
|
2016
|
+
actualType.currency !== boundType.currency
|
|
2017
|
+
) {
|
|
2018
|
+
diagnostics.push({
|
|
2019
|
+
code: "HSX1101",
|
|
2020
|
+
fix: `pass money<${boundType.currency}> to ${shapeField.key.name}`,
|
|
2021
|
+
message: `${shapeField.key.name} needs money<${boundType.currency}> but receives money<${actualType.currency}>`,
|
|
2022
|
+
severity: "error",
|
|
2023
|
+
span: shapeField.value.span,
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1502
2029
|
function bindPortCompileValues(
|
|
1503
2030
|
values: Map<string, Expr>,
|
|
1504
2031
|
binding: string,
|
|
1505
2032
|
value: Extract<Expr, { readonly kind: "port_ref" }>,
|
|
1506
2033
|
ports: ReadonlyMap<string, PortDecl>,
|
|
1507
2034
|
diagnostics: GeneralDiagnostic[],
|
|
2035
|
+
aliases: ReadonlyMap<string, Expr> = new Map(),
|
|
2036
|
+
loweredPortShapes: Map<string, BlockExpr> = new Map(),
|
|
2037
|
+
dependencies: Map<string, readonly string[]> = new Map(),
|
|
1508
2038
|
): void {
|
|
1509
2039
|
const port = ports.get(value.name.name);
|
|
1510
2040
|
if (!port) {
|
|
@@ -1517,13 +2047,41 @@ function bindPortCompileValues(
|
|
|
1517
2047
|
});
|
|
1518
2048
|
return;
|
|
1519
2049
|
}
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2050
|
+
for (const key of [binding, `${binding}_allowed`, `${binding}_fields`]) {
|
|
2051
|
+
dependencies.set(key, [port.name.name]);
|
|
2052
|
+
}
|
|
2053
|
+
const allowed = entry(port.body, "allowed")?.value;
|
|
2054
|
+
let shape = loweredPortShapes.get(value.name.name);
|
|
2055
|
+
if (!shape) {
|
|
2056
|
+
const rawShape = entry(port.body, "shape")?.value ?? {
|
|
2057
|
+
entries: [],
|
|
2058
|
+
kind: "block",
|
|
2059
|
+
span: port.body.span,
|
|
2060
|
+
};
|
|
2061
|
+
const shapeDiagnostics: GeneralDiagnostic[] = [];
|
|
2062
|
+
shape = lowerPortShape(port, rawShape, aliases, shapeDiagnostics);
|
|
2063
|
+
for (const diagnostic of shapeDiagnostics) {
|
|
2064
|
+
if (
|
|
2065
|
+
!diagnostics.some(
|
|
2066
|
+
(prior) =>
|
|
2067
|
+
prior.code === diagnostic.code &&
|
|
2068
|
+
prior.span === diagnostic.span &&
|
|
2069
|
+
prior.message === diagnostic.message,
|
|
2070
|
+
)
|
|
2071
|
+
)
|
|
2072
|
+
diagnostics.push(diagnostic);
|
|
2073
|
+
}
|
|
2074
|
+
loweredPortShapes.set(value.name.name, shape);
|
|
2075
|
+
}
|
|
2076
|
+
const normalizedAllowed: Expr | undefined =
|
|
2077
|
+
allowed?.kind === "ident"
|
|
2078
|
+
? {
|
|
2079
|
+
items: [allowed],
|
|
2080
|
+
kind: "list",
|
|
2081
|
+
span: allowed.span,
|
|
2082
|
+
}
|
|
2083
|
+
: allowed;
|
|
2084
|
+
if (normalizedAllowed) values.set(`${binding}_allowed`, normalizedAllowed);
|
|
1527
2085
|
values.set(`${binding}_fields`, shape);
|
|
1528
2086
|
if (value.within) values.set(`${binding}_within`, value.within);
|
|
1529
2087
|
if (value.deadline) values.set(`${binding}_deadline`, value.deadline);
|
|
@@ -1759,17 +2317,65 @@ function booleanExpr(value: boolean, span: Span): Expr {
|
|
|
1759
2317
|
return { kind: "boolean", span, value };
|
|
1760
2318
|
}
|
|
1761
2319
|
|
|
2320
|
+
function selectedControls(value: Expr): readonly string[] {
|
|
2321
|
+
if (value.kind !== "block") return [];
|
|
2322
|
+
return value.entries.flatMap((row) => [
|
|
2323
|
+
...(row.conditionPorts ?? []),
|
|
2324
|
+
...selectedControls(row.value),
|
|
2325
|
+
]);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// Retain dependencies before compile-time evaluation erases identifiers.
|
|
2329
|
+
function expressionPorts(
|
|
2330
|
+
value: unknown,
|
|
2331
|
+
dependencies: ReadonlyMap<string, readonly string[]>,
|
|
2332
|
+
): string[] {
|
|
2333
|
+
if (!value || typeof value !== "object") return [];
|
|
2334
|
+
if (Array.isArray(value))
|
|
2335
|
+
return [
|
|
2336
|
+
...new Set(value.flatMap((item) => expressionPorts(item, dependencies))),
|
|
2337
|
+
];
|
|
2338
|
+
const node = value as Record<string, unknown>;
|
|
2339
|
+
const found = new Set<string>(
|
|
2340
|
+
(node.conditionPorts as readonly string[] | undefined) ?? [],
|
|
2341
|
+
);
|
|
2342
|
+
if (node.kind === "ident" && typeof node.name === "string") {
|
|
2343
|
+
for (const port of dependencies.get(node.name.replace(/^\[|\]$/g, "")) ??
|
|
2344
|
+
[])
|
|
2345
|
+
found.add(port);
|
|
2346
|
+
}
|
|
2347
|
+
if (node.kind === "string" && typeof node.value === "string") {
|
|
2348
|
+
for (const match of node.value.matchAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g)) {
|
|
2349
|
+
for (const port of dependencies.get(match[1]!) ?? []) found.add(port);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
for (const [key, child] of Object.entries(node)) {
|
|
2353
|
+
if (key === "span" || key === "conditionPorts") continue;
|
|
2354
|
+
for (const port of expressionPorts(child, dependencies)) found.add(port);
|
|
2355
|
+
}
|
|
2356
|
+
return [...found];
|
|
2357
|
+
}
|
|
2358
|
+
|
|
1762
2359
|
function selectCompileTimeRows(
|
|
1763
2360
|
block: BlockExpr,
|
|
1764
2361
|
values: ReadonlyMap<string, Expr>,
|
|
1765
2362
|
diagnostics: GeneralDiagnostic[],
|
|
2363
|
+
dependencies: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
2364
|
+
controls: readonly string[] = [],
|
|
1766
2365
|
): BlockExpr {
|
|
1767
2366
|
const entries: Entry[] = [];
|
|
1768
2367
|
const locals = new Map(values);
|
|
2368
|
+
const localDependencies = new Map(dependencies);
|
|
1769
2369
|
for (const row of block.entries) {
|
|
1770
2370
|
if (row.key.name === "let") {
|
|
1771
2371
|
const binding = row.qualifiers[0];
|
|
1772
|
-
if (binding)
|
|
2372
|
+
if (binding) {
|
|
2373
|
+
localDependencies.set(
|
|
2374
|
+
binding.name,
|
|
2375
|
+
expressionPorts(row.value, localDependencies),
|
|
2376
|
+
);
|
|
2377
|
+
locals.set(binding.name, substituteExpr(row.value, locals));
|
|
2378
|
+
}
|
|
1773
2379
|
entries.push(row);
|
|
1774
2380
|
continue;
|
|
1775
2381
|
}
|
|
@@ -1809,13 +2415,53 @@ function selectCompileTimeRows(
|
|
|
1809
2415
|
row.key.name !== "when_not" &&
|
|
1810
2416
|
row.key.name !== "when_eq"
|
|
1811
2417
|
) {
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2418
|
+
const value =
|
|
2419
|
+
row.value.kind === "block" && !row.iteration
|
|
2420
|
+
? selectCompileTimeRows(
|
|
2421
|
+
row.value,
|
|
2422
|
+
locals,
|
|
2423
|
+
diagnostics,
|
|
2424
|
+
localDependencies,
|
|
2425
|
+
controls,
|
|
2426
|
+
)
|
|
2427
|
+
: row.value;
|
|
2428
|
+
const conditionPorts =
|
|
2429
|
+
row.key.name === "action"
|
|
2430
|
+
? [
|
|
2431
|
+
...new Set([
|
|
2432
|
+
...(value.kind === "block" && entry(value, "due")
|
|
2433
|
+
? []
|
|
2434
|
+
: controls),
|
|
2435
|
+
...row.qualifiers
|
|
2436
|
+
.filter((qualifier) => qualifier.name.startsWith("["))
|
|
2437
|
+
.flatMap((qualifier) =>
|
|
2438
|
+
expressionPorts(qualifier, localDependencies),
|
|
2439
|
+
),
|
|
2440
|
+
...expressionPorts(
|
|
2441
|
+
value.kind === "block"
|
|
2442
|
+
? entry(value, "port")?.value
|
|
2443
|
+
: undefined,
|
|
2444
|
+
localDependencies,
|
|
2445
|
+
),
|
|
2446
|
+
...expressionPorts(
|
|
2447
|
+
value.kind === "block"
|
|
2448
|
+
? entry(value, "input")?.value
|
|
2449
|
+
: undefined,
|
|
2450
|
+
localDependencies,
|
|
2451
|
+
),
|
|
2452
|
+
...expressionPorts(
|
|
2453
|
+
value.kind === "block"
|
|
2454
|
+
? entry(value, "capture_input", "captureInput")?.value
|
|
2455
|
+
: undefined,
|
|
2456
|
+
localDependencies,
|
|
2457
|
+
),
|
|
2458
|
+
...(value.kind === "block" && entry(value, "due")
|
|
2459
|
+
? []
|
|
2460
|
+
: selectedControls(value)),
|
|
2461
|
+
]),
|
|
2462
|
+
]
|
|
2463
|
+
: controls;
|
|
2464
|
+
entries.push({ ...row, conditionPorts, value });
|
|
1819
2465
|
continue;
|
|
1820
2466
|
}
|
|
1821
2467
|
const subject = row.qualifiers[0];
|
|
@@ -1842,7 +2488,18 @@ function selectCompileTimeRows(
|
|
|
1842
2488
|
}
|
|
1843
2489
|
if (include) {
|
|
1844
2490
|
entries.push(
|
|
1845
|
-
...selectCompileTimeRows(
|
|
2491
|
+
...selectCompileTimeRows(
|
|
2492
|
+
row.value,
|
|
2493
|
+
locals,
|
|
2494
|
+
diagnostics,
|
|
2495
|
+
localDependencies,
|
|
2496
|
+
[
|
|
2497
|
+
...new Set([
|
|
2498
|
+
...controls,
|
|
2499
|
+
...(subject ? expressionPorts(subject, localDependencies) : []),
|
|
2500
|
+
]),
|
|
2501
|
+
],
|
|
2502
|
+
).entries,
|
|
1846
2503
|
);
|
|
1847
2504
|
}
|
|
1848
2505
|
}
|
|
@@ -2104,14 +2761,21 @@ function nameText(value: Expr): string {
|
|
|
2104
2761
|
function expandComprehensions(
|
|
2105
2762
|
block: BlockExpr,
|
|
2106
2763
|
diagnostics: GeneralDiagnostic[],
|
|
2764
|
+
values: ReadonlyMap<string, Expr> = new Map(),
|
|
2765
|
+
dependencies: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
2107
2766
|
): BlockExpr {
|
|
2108
2767
|
const budget = { used: 0 };
|
|
2109
2768
|
const expand = (
|
|
2110
2769
|
current: BlockExpr,
|
|
2111
2770
|
inherited: ReadonlyMap<string, Expr> = new Map(),
|
|
2771
|
+
inheritedDependencies: ReadonlyMap<
|
|
2772
|
+
string,
|
|
2773
|
+
readonly string[]
|
|
2774
|
+
> = dependencies,
|
|
2112
2775
|
): BlockExpr => {
|
|
2113
2776
|
const entries: Entry[] = [];
|
|
2114
2777
|
const locals = new Map(inherited);
|
|
2778
|
+
const localDependencies = new Map(inheritedDependencies);
|
|
2115
2779
|
for (const row of current.entries) {
|
|
2116
2780
|
if (row.key.name === "let") {
|
|
2117
2781
|
const binding = row.qualifiers[0];
|
|
@@ -2125,6 +2789,10 @@ function expandComprehensions(
|
|
|
2125
2789
|
});
|
|
2126
2790
|
continue;
|
|
2127
2791
|
}
|
|
2792
|
+
localDependencies.set(
|
|
2793
|
+
binding.name,
|
|
2794
|
+
expressionPorts(row.value, localDependencies),
|
|
2795
|
+
);
|
|
2128
2796
|
locals.set(binding.name, substituteExpr(row.value, locals));
|
|
2129
2797
|
continue;
|
|
2130
2798
|
}
|
|
@@ -2142,7 +2810,7 @@ function expandComprehensions(
|
|
|
2142
2810
|
: compileTimeTruthy(actual);
|
|
2143
2811
|
if (row.key.name === "when_not") include = !include;
|
|
2144
2812
|
if (include && row.value.kind === "block") {
|
|
2145
|
-
entries.push(...expand(row.value, locals).entries);
|
|
2813
|
+
entries.push(...expand(row.value, locals, localDependencies).entries);
|
|
2146
2814
|
}
|
|
2147
2815
|
continue;
|
|
2148
2816
|
}
|
|
@@ -2162,8 +2830,8 @@ function expandComprehensions(
|
|
|
2162
2830
|
entries.push({
|
|
2163
2831
|
...substituted,
|
|
2164
2832
|
value:
|
|
2165
|
-
|
|
2166
|
-
? expand(
|
|
2833
|
+
row.value.kind === "block"
|
|
2834
|
+
? expand(row.value, locals, localDependencies)
|
|
2167
2835
|
: substituted.value,
|
|
2168
2836
|
});
|
|
2169
2837
|
continue;
|
|
@@ -2197,14 +2865,19 @@ function expandComprehensions(
|
|
|
2197
2865
|
...locals,
|
|
2198
2866
|
[row.iteration.binding.name, value],
|
|
2199
2867
|
]);
|
|
2200
|
-
const body =
|
|
2201
|
-
|
|
2202
|
-
|
|
2868
|
+
const body = selectCompileTimeRows(
|
|
2869
|
+
row.value,
|
|
2870
|
+
substitutions,
|
|
2871
|
+
diagnostics,
|
|
2872
|
+
localDependencies,
|
|
2873
|
+
row.conditionPorts,
|
|
2874
|
+
);
|
|
2875
|
+
entries.push(...expand(body, substitutions, localDependencies).entries);
|
|
2203
2876
|
}
|
|
2204
2877
|
}
|
|
2205
2878
|
return { ...current, entries };
|
|
2206
2879
|
};
|
|
2207
|
-
return expand(block);
|
|
2880
|
+
return expand(block, values);
|
|
2208
2881
|
}
|
|
2209
2882
|
|
|
2210
2883
|
function finiteIterationValues(bound: Expr): readonly Expr[] | undefined {
|
|
@@ -2216,20 +2889,18 @@ function finiteIterationValues(bound: Expr): readonly Expr[] | undefined {
|
|
|
2216
2889
|
if (bound.kind !== "number" || bound.raw.includes(".")) return undefined;
|
|
2217
2890
|
const count = Number(bound.raw);
|
|
2218
2891
|
if (!Number.isSafeInteger(count) || count < 0) return undefined;
|
|
2219
|
-
return Array.from(
|
|
2220
|
-
|
|
2221
|
-
(
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
span: bound.span,
|
|
2225
|
-
}),
|
|
2226
|
-
);
|
|
2892
|
+
return Array.from({ length: count }, (_, index): Expr => ({
|
|
2893
|
+
kind: "number",
|
|
2894
|
+
raw: String(index + 1),
|
|
2895
|
+
span: bound.span,
|
|
2896
|
+
}));
|
|
2227
2897
|
}
|
|
2228
2898
|
|
|
2229
2899
|
function checkInstrument(
|
|
2230
2900
|
name: IdentExpr,
|
|
2231
2901
|
body: BlockExpr,
|
|
2232
2902
|
parties: ReadonlySet<string>,
|
|
2903
|
+
ports: ReadonlyMap<string, readonly PortDecl[]>,
|
|
2233
2904
|
aliases: ReadonlyMap<string, Expr>,
|
|
2234
2905
|
diagnostics: GeneralDiagnostic[],
|
|
2235
2906
|
): TypedInstrument | undefined {
|
|
@@ -2283,12 +2954,66 @@ function checkInstrument(
|
|
|
2283
2954
|
}
|
|
2284
2955
|
const slots: Record<string, JsonValue> = {};
|
|
2285
2956
|
const partiesEntry = entry(body, "parties");
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2957
|
+
const roleObject = blockToObject(
|
|
2958
|
+
partiesEntry?.value.kind === "block" ? partiesEntry.value : undefined,
|
|
2959
|
+
new Map(),
|
|
2960
|
+
diagnostics,
|
|
2961
|
+
);
|
|
2962
|
+
const partyAccounts = new Map(
|
|
2963
|
+
[...parties].map((party) => [party, `${camel(party)}AccountId`]),
|
|
2964
|
+
);
|
|
2965
|
+
for (const [role, binding] of Object.entries(roleObject)) {
|
|
2966
|
+
if (!isJsonObject(binding)) continue;
|
|
2967
|
+
if (
|
|
2968
|
+
typeof binding.party !== "string" ||
|
|
2969
|
+
!parties.has(binding.party) ||
|
|
2970
|
+
typeof binding.account !== "string" ||
|
|
2971
|
+
!endpointFields.has(binding.account)
|
|
2972
|
+
) {
|
|
2973
|
+
diagnostics.push({
|
|
2974
|
+
code: "HSX1024",
|
|
2975
|
+
fix: "bind a declared party to a declared account field",
|
|
2976
|
+
message: `party role ${role} has an invalid party/account binding`,
|
|
2977
|
+
severity: "error",
|
|
2978
|
+
span: partiesEntry?.span ?? name.span,
|
|
2979
|
+
});
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
partyAccounts.set(binding.party, binding.account);
|
|
2983
|
+
roleObject[role] = binding.account;
|
|
2984
|
+
}
|
|
2985
|
+
const decisionRoles = new Map<string, string>();
|
|
2986
|
+
const inferredDecisionFields = new Set<string>();
|
|
2987
|
+
// Decision authority needs an identity binding, even when no money moves
|
|
2988
|
+
// through that party. Reuse the same account binding as economic roles.
|
|
2989
|
+
for (const action of body.entries) {
|
|
2990
|
+
if (action.key.name !== "action" || action.value.kind !== "block") continue;
|
|
2991
|
+
const port = entry(action.value, "port")?.value;
|
|
2992
|
+
if (port?.kind !== "block") continue;
|
|
2993
|
+
const allowed = entry(port, "allowed_parties", "allowedParties")?.value;
|
|
2994
|
+
if (allowed?.kind !== "list") continue;
|
|
2995
|
+
for (const item of allowed.items) {
|
|
2996
|
+
const party = nameText(item);
|
|
2997
|
+
if (!parties.has(party)) continue;
|
|
2998
|
+
const accountField = partyAccounts.get(party)!;
|
|
2999
|
+
const existing = Object.entries(roleObject).find(
|
|
3000
|
+
([, value]) => value === party || value === accountField,
|
|
3001
|
+
);
|
|
3002
|
+
let role = existing?.[0] ?? party;
|
|
3003
|
+
for (
|
|
3004
|
+
let suffix = 2;
|
|
3005
|
+
!existing && Object.hasOwn(roleObject, role);
|
|
3006
|
+
suffix++
|
|
3007
|
+
)
|
|
3008
|
+
role = `${party}_${suffix}`;
|
|
3009
|
+
if (!existing) {
|
|
3010
|
+
roleObject[role] = party;
|
|
3011
|
+
inferredDecisionFields.add(accountField);
|
|
3012
|
+
}
|
|
3013
|
+
decisionRoles.set(party, role);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
if (Object.keys(roleObject).length > 0) {
|
|
2292
3017
|
for (const [role, value] of Object.entries(roleObject)) {
|
|
2293
3018
|
if (typeof value !== "string") continue;
|
|
2294
3019
|
if (fieldNames.has(value)) {
|
|
@@ -2304,11 +3029,11 @@ function checkInstrument(
|
|
|
2304
3029
|
if (!fieldNames.has(field)) {
|
|
2305
3030
|
fields.push({
|
|
2306
3031
|
name: field,
|
|
2307
|
-
origin: partiesEntry.span,
|
|
3032
|
+
origin: partiesEntry?.span ?? name.span,
|
|
2308
3033
|
required: true,
|
|
2309
3034
|
schema: {
|
|
2310
3035
|
description: `The ${value.replaceAll("_", " ")} account`,
|
|
2311
|
-
pattern:
|
|
3036
|
+
pattern: ACCOUNT_PATTERN,
|
|
2312
3037
|
type: "string",
|
|
2313
3038
|
"x-hyperscale-reference-filter": {
|
|
2314
3039
|
column: "role",
|
|
@@ -2327,8 +3052,12 @@ function checkInstrument(
|
|
|
2327
3052
|
message: `party role refers to ${String(value)}, which is not declared`,
|
|
2328
3053
|
severity: "error",
|
|
2329
3054
|
span:
|
|
2330
|
-
partiesEntry
|
|
2331
|
-
|
|
3055
|
+
(partiesEntry?.value.kind === "block"
|
|
3056
|
+
? partiesEntry.value.entries
|
|
3057
|
+
: []
|
|
3058
|
+
).find((entry) => entry.key.name === role)?.value.span ??
|
|
3059
|
+
partiesEntry?.span ??
|
|
3060
|
+
name.span,
|
|
2332
3061
|
});
|
|
2333
3062
|
}
|
|
2334
3063
|
slots.parties = roleObject;
|
|
@@ -2370,7 +3099,7 @@ function checkInstrument(
|
|
|
2370
3099
|
origin: actionEntry.span,
|
|
2371
3100
|
required: true,
|
|
2372
3101
|
schema: {
|
|
2373
|
-
pattern:
|
|
3102
|
+
pattern: ACCOUNT_PATTERN,
|
|
2374
3103
|
type: "string",
|
|
2375
3104
|
},
|
|
2376
3105
|
type: { kind: "account" },
|
|
@@ -2380,21 +3109,122 @@ function checkInstrument(
|
|
|
2380
3109
|
}
|
|
2381
3110
|
}
|
|
2382
3111
|
}
|
|
2383
|
-
const actions = actionEntries
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
3112
|
+
const actions = actionEntries
|
|
3113
|
+
.flatMap((candidate) => {
|
|
3114
|
+
const actionName = candidate.qualifiers[0];
|
|
3115
|
+
if (!actionName || candidate.value.kind !== "block") return [];
|
|
3116
|
+
return [
|
|
3117
|
+
checkAction(
|
|
3118
|
+
actionName,
|
|
3119
|
+
candidate.value,
|
|
3120
|
+
fields,
|
|
3121
|
+
name.name,
|
|
3122
|
+
endpointFields,
|
|
3123
|
+
holdDestinations,
|
|
3124
|
+
diagnostics,
|
|
3125
|
+
),
|
|
3126
|
+
];
|
|
3127
|
+
})
|
|
3128
|
+
.map((action) => {
|
|
3129
|
+
const port = action.slots.port;
|
|
3130
|
+
if (
|
|
3131
|
+
port === undefined ||
|
|
3132
|
+
!isJsonObject(port) ||
|
|
3133
|
+
!Array.isArray(port.allowedParties)
|
|
3134
|
+
)
|
|
3135
|
+
return action;
|
|
3136
|
+
return {
|
|
3137
|
+
...action,
|
|
3138
|
+
slots: {
|
|
3139
|
+
...action.slots,
|
|
3140
|
+
port: {
|
|
3141
|
+
...port,
|
|
3142
|
+
allowedParties: port.allowedParties.map((party) =>
|
|
3143
|
+
typeof party === "string"
|
|
3144
|
+
? (decisionRoles.get(party) ?? party)
|
|
3145
|
+
: party,
|
|
3146
|
+
),
|
|
3147
|
+
},
|
|
3148
|
+
},
|
|
3149
|
+
};
|
|
3150
|
+
})
|
|
3151
|
+
.map((action) => pinFixedCurrencyBindings(action, fields, diagnostics));
|
|
3152
|
+
for (const action of actions) {
|
|
3153
|
+
for (const declared of ports.get(action.name) ?? []) {
|
|
3154
|
+
const allowed = entry(declared.body, "allowed")?.value;
|
|
3155
|
+
const allowedItems =
|
|
3156
|
+
allowed?.kind === "list" ? allowed.items : allowed ? [allowed] : [];
|
|
3157
|
+
if (allowedItems.length === 0) {
|
|
3158
|
+
diagnostics.push({
|
|
3159
|
+
code: "HSX1024",
|
|
3160
|
+
fix: `declare a nonempty allowed list of declared parties on port ${declared.name.name}`,
|
|
3161
|
+
message: `wired decision port ${declared.name.name} reaches action ${action.name} without declared allowed parties`,
|
|
3162
|
+
severity: "error",
|
|
3163
|
+
span: declared.name.span,
|
|
3164
|
+
});
|
|
3165
|
+
continue;
|
|
3166
|
+
}
|
|
3167
|
+
const port = action.slots.port;
|
|
3168
|
+
if (
|
|
3169
|
+
port === undefined ||
|
|
3170
|
+
!isJsonObject(port) ||
|
|
3171
|
+
!Array.isArray(port.allowedParties)
|
|
3172
|
+
) {
|
|
3173
|
+
diagnostics.push({
|
|
3174
|
+
code: "HSX1024",
|
|
3175
|
+
fix: `emit the allowed parties of ${declared.name.name} in action ${action.name}'s port`,
|
|
3176
|
+
message: `wired decision port ${declared.name.name} reaches action ${action.name} without a decision port`,
|
|
3177
|
+
severity: "error",
|
|
3178
|
+
span: action.origin,
|
|
3179
|
+
});
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
const allowedFields = new Set<string>();
|
|
3183
|
+
for (const item of allowedItems) {
|
|
3184
|
+
const party = nameText(item);
|
|
3185
|
+
const role =
|
|
3186
|
+
decisionRoles.get(party) ??
|
|
3187
|
+
Object.keys(roleObject).find(
|
|
3188
|
+
(role) =>
|
|
3189
|
+
roleObject[role] ===
|
|
3190
|
+
(partyAccounts.get(party) ?? `${camel(party)}AccountId`),
|
|
3191
|
+
);
|
|
3192
|
+
const field = role === undefined ? undefined : roleObject[role];
|
|
3193
|
+
if (typeof field === "string") {
|
|
3194
|
+
allowedFields.add(field);
|
|
3195
|
+
continue;
|
|
3196
|
+
}
|
|
3197
|
+
if (!parties.has(party)) continue;
|
|
3198
|
+
diagnostics.push({
|
|
3199
|
+
code: "HSX1024",
|
|
3200
|
+
fix: `bind ${party} to an account field and use that binding in action ${action.name}'s port`,
|
|
3201
|
+
message: `decision port ${action.name} allows party ${party}, but settlement ${name.name} does not bind that party to the decision action`,
|
|
3202
|
+
severity: "error",
|
|
3203
|
+
span: item.span,
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
if (
|
|
3207
|
+
port.allowedParties.length === 0 ||
|
|
3208
|
+
allowedFields.size !==
|
|
3209
|
+
new Set(
|
|
3210
|
+
port.allowedParties.map((role) => String(roleObject[String(role)])),
|
|
3211
|
+
).size ||
|
|
3212
|
+
port.allowedParties.some(
|
|
3213
|
+
(role) =>
|
|
3214
|
+
typeof role !== "string" ||
|
|
3215
|
+
!allowedFields.has(String(roleObject[role])),
|
|
3216
|
+
)
|
|
3217
|
+
) {
|
|
3218
|
+
diagnostics.push({
|
|
3219
|
+
code: "HSX1024",
|
|
3220
|
+
fix: `use only the account bindings allowed by ${declared.name.name}`,
|
|
3221
|
+
message: `action ${action.name} does not preserve the allowed parties of decision port ${declared.name.name}`,
|
|
3222
|
+
severity: "error",
|
|
3223
|
+
span: action.origin,
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
2398
3228
|
if (!actionNames.has("create")) {
|
|
2399
3229
|
diagnostics.push({
|
|
2400
3230
|
code: "HSX1504",
|
|
@@ -2479,10 +3309,49 @@ function checkInstrument(
|
|
|
2479
3309
|
slots.summary ??= `${titleize(name.name)} instrument`;
|
|
2480
3310
|
slots.title ??= titleize(name.name);
|
|
2481
3311
|
linearityDiagnostics(actionEntries, diagnostics);
|
|
3312
|
+
agentDescriptionDiagnostics(name, slots, actions, diagnostics);
|
|
3313
|
+
if (Array.isArray(slots.required)) {
|
|
3314
|
+
slots.required = [
|
|
3315
|
+
...new Set([...slots.required, ...inferredDecisionFields]),
|
|
3316
|
+
];
|
|
3317
|
+
}
|
|
2482
3318
|
|
|
2483
3319
|
return { actions, fields, id: name.name, origin: name.span, slots };
|
|
2484
3320
|
}
|
|
2485
3321
|
|
|
3322
|
+
// Mirrors the catalog law agent_description_required: every action a caller
|
|
3323
|
+
// can reach becomes an agent tool whose description is authored, never
|
|
3324
|
+
// synthesized from the human-facing summary. System actions with a due clause
|
|
3325
|
+
// are exempt because no caller can reach them.
|
|
3326
|
+
function agentDescriptionDiagnostics(
|
|
3327
|
+
name: IdentExpr,
|
|
3328
|
+
slots: Readonly<Record<string, JsonValue>>,
|
|
3329
|
+
actions: readonly TypedAction[],
|
|
3330
|
+
diagnostics: GeneralDiagnostic[],
|
|
3331
|
+
): void {
|
|
3332
|
+
const callable = actions.filter((action) => action.slots.due === undefined);
|
|
3333
|
+
if (callable.length === 0) return;
|
|
3334
|
+
if (slots.agentDescription === undefined) {
|
|
3335
|
+
diagnostics.push({
|
|
3336
|
+
code: "HSX1509",
|
|
3337
|
+
fix: 'add agent_description: "..." telling an agent when to reach for this instrument',
|
|
3338
|
+
message: `instrument ${name.name} exposes callable actions without an agent_description`,
|
|
3339
|
+
severity: "error",
|
|
3340
|
+
span: name.span,
|
|
3341
|
+
});
|
|
3342
|
+
}
|
|
3343
|
+
for (const action of callable) {
|
|
3344
|
+
if (action.slots.agentDescription !== undefined) continue;
|
|
3345
|
+
diagnostics.push({
|
|
3346
|
+
code: "HSX1509",
|
|
3347
|
+
fix: 'add agent_description: "..." telling an agent when to call this action',
|
|
3348
|
+
message: `action ${action.name} of ${name.name} is callable without an agent_description`,
|
|
3349
|
+
severity: "error",
|
|
3350
|
+
span: action.origin,
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
|
|
2486
3355
|
function checkRequiredFields(
|
|
2487
3356
|
value: JsonValue,
|
|
2488
3357
|
fields: readonly TypedField[],
|
|
@@ -2576,71 +3445,93 @@ function checkFields(
|
|
|
2576
3445
|
continue;
|
|
2577
3446
|
}
|
|
2578
3447
|
names.add(name);
|
|
2579
|
-
|
|
2580
|
-
let description: string | undefined;
|
|
2581
|
-
let required = true;
|
|
2582
|
-
let extra: Record<string, JsonValue> = {};
|
|
2583
|
-
if (row.value.kind === "block") {
|
|
2584
|
-
const typeRow = entry(row.value, "type");
|
|
2585
|
-
if (typeRow) typeExpr = typeRow.value;
|
|
2586
|
-
const desc = entry(row.value, "description") ?? entry(row.value, "desc");
|
|
2587
|
-
if (desc?.value.kind === "string") description = desc.value.value;
|
|
2588
|
-
const optional = entry(row.value, "optional");
|
|
2589
|
-
if (optional?.value.kind === "boolean") required = !optional.value.value;
|
|
2590
|
-
extra = blockToObject(
|
|
2591
|
-
{
|
|
2592
|
-
...row.value,
|
|
2593
|
-
entries: row.value.entries.filter(
|
|
2594
|
-
(item) =>
|
|
2595
|
-
!["type", "description", "desc", "optional"].includes(
|
|
2596
|
-
item.key.name,
|
|
2597
|
-
),
|
|
2598
|
-
),
|
|
2599
|
-
},
|
|
2600
|
-
new Map(),
|
|
2601
|
-
diagnostics,
|
|
2602
|
-
);
|
|
2603
|
-
}
|
|
2604
|
-
const type = typeOf(typeExpr, aliases);
|
|
2605
|
-
const fixedMoneyIssue = invalidFixedMoneyBinding(typeExpr);
|
|
2606
|
-
if (fixedMoneyIssue) {
|
|
2607
|
-
diagnostics.push({
|
|
2608
|
-
code: "HSX1103",
|
|
2609
|
-
fix: `write ${name} as money(SAR, 2500) with a positive safe integer in minor units`,
|
|
2610
|
-
message: `field ${name} ${fixedMoneyIssue}`,
|
|
2611
|
-
severity: "error",
|
|
2612
|
-
span: typeExpr.span,
|
|
2613
|
-
});
|
|
2614
|
-
}
|
|
2615
|
-
if (
|
|
2616
|
-
(typeExpr.kind === "ident" && typeExpr.name === "ref") ||
|
|
2617
|
-
(type.kind === "ref" && !type.target)
|
|
2618
|
-
) {
|
|
2619
|
-
diagnostics.push({
|
|
2620
|
-
code: "HSX1007",
|
|
2621
|
-
fix: "declare the target instrument as ref<instrument_id>",
|
|
2622
|
-
message: `field ${name} declares ref without an instrument target`,
|
|
2623
|
-
severity: "error",
|
|
2624
|
-
span: typeExpr.span,
|
|
2625
|
-
});
|
|
2626
|
-
}
|
|
2627
|
-
const structuralType = Object.hasOwn(extra, "items")
|
|
2628
|
-
? "array"
|
|
2629
|
-
: Object.hasOwn(extra, "properties") ||
|
|
2630
|
-
Object.hasOwn(extra, "additionalProperties")
|
|
2631
|
-
? "object"
|
|
2632
|
-
: undefined;
|
|
2633
|
-
const schema = {
|
|
2634
|
-
...schemaFor(type, required),
|
|
2635
|
-
...extra,
|
|
2636
|
-
...(structuralType ? { type: structuralType } : {}),
|
|
2637
|
-
...(description ? { description } : {}),
|
|
2638
|
-
};
|
|
2639
|
-
result.push({ name, origin: row.span, required, schema, type });
|
|
3448
|
+
result.push(lowerField(row, aliases, diagnostics));
|
|
2640
3449
|
}
|
|
2641
3450
|
return result;
|
|
2642
3451
|
}
|
|
2643
3452
|
|
|
3453
|
+
function lowerField(
|
|
3454
|
+
row: Entry,
|
|
3455
|
+
aliases: ReadonlyMap<string, Expr>,
|
|
3456
|
+
diagnostics: GeneralDiagnostic[],
|
|
3457
|
+
): TypedField {
|
|
3458
|
+
const name = camel(row.key.name);
|
|
3459
|
+
let typeExpr = row.value;
|
|
3460
|
+
let description: string | undefined;
|
|
3461
|
+
let required = true;
|
|
3462
|
+
let extra: Record<string, JsonValue> = {};
|
|
3463
|
+
if (row.value.kind === "block") {
|
|
3464
|
+
const typeRow = entry(row.value, "type");
|
|
3465
|
+
if (typeRow) typeExpr = typeRow.value;
|
|
3466
|
+
const desc = entry(row.value, "description") ?? entry(row.value, "desc");
|
|
3467
|
+
if (desc?.value.kind === "string") description = desc.value.value;
|
|
3468
|
+
const optional = entry(row.value, "optional");
|
|
3469
|
+
if (optional?.value.kind === "boolean") required = !optional.value.value;
|
|
3470
|
+
extra = blockToObject(
|
|
3471
|
+
{
|
|
3472
|
+
...row.value,
|
|
3473
|
+
entries: row.value.entries.filter(
|
|
3474
|
+
(item) =>
|
|
3475
|
+
!["type", "description", "desc", "optional"].includes(
|
|
3476
|
+
item.key.name,
|
|
3477
|
+
),
|
|
3478
|
+
),
|
|
3479
|
+
},
|
|
3480
|
+
new Map(),
|
|
3481
|
+
diagnostics,
|
|
3482
|
+
);
|
|
3483
|
+
}
|
|
3484
|
+
const type = typeOf(typeExpr, aliases);
|
|
3485
|
+
// Diagnostics point at the field's own type expression, not at an alias
|
|
3486
|
+
// declaration that may live in an imported module.
|
|
3487
|
+
const declaredSpan = typeExpr.span;
|
|
3488
|
+
const seen = new Set<string>();
|
|
3489
|
+
while (
|
|
3490
|
+
typeExpr.kind === "ident" &&
|
|
3491
|
+
aliases.has(typeExpr.name) &&
|
|
3492
|
+
!seen.has(typeExpr.name)
|
|
3493
|
+
) {
|
|
3494
|
+
seen.add(typeExpr.name);
|
|
3495
|
+
typeExpr = aliases.get(typeExpr.name)!;
|
|
3496
|
+
}
|
|
3497
|
+
const fixedMoneyIssue = invalidFixedMoneyBinding(typeExpr);
|
|
3498
|
+
if (fixedMoneyIssue) {
|
|
3499
|
+
diagnostics.push({
|
|
3500
|
+
code: "HSX1103",
|
|
3501
|
+
fix: `write ${name} as money(SAR, 2500) with a positive safe integer in minor units`,
|
|
3502
|
+
message: `field ${name} ${fixedMoneyIssue}`,
|
|
3503
|
+
severity: "error",
|
|
3504
|
+
span: declaredSpan,
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
if (
|
|
3508
|
+
(typeExpr.kind === "ident" && typeExpr.name === "ref") ||
|
|
3509
|
+
(type.kind === "ref" && !type.target)
|
|
3510
|
+
) {
|
|
3511
|
+
diagnostics.push({
|
|
3512
|
+
code: "HSX1007",
|
|
3513
|
+
fix: "declare the target instrument as ref<instrument_id>",
|
|
3514
|
+
message: `field ${name} declares ref without an instrument target`,
|
|
3515
|
+
severity: "error",
|
|
3516
|
+
span: declaredSpan,
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
const structuralType = Object.hasOwn(extra, "items")
|
|
3521
|
+
? "array"
|
|
3522
|
+
: Object.hasOwn(extra, "properties") ||
|
|
3523
|
+
Object.hasOwn(extra, "additionalProperties")
|
|
3524
|
+
? "object"
|
|
3525
|
+
: undefined;
|
|
3526
|
+
const schema = {
|
|
3527
|
+
...schemaFor(type, required),
|
|
3528
|
+
...extra,
|
|
3529
|
+
...(structuralType ? { type: structuralType } : {}),
|
|
3530
|
+
...(description ? { description } : {}),
|
|
3531
|
+
};
|
|
3532
|
+
return { name, origin: row.span, required, schema, type };
|
|
3533
|
+
}
|
|
3534
|
+
|
|
2644
3535
|
function checkLifecycle(
|
|
2645
3536
|
instrument: string,
|
|
2646
3537
|
block: BlockExpr,
|
|
@@ -2775,6 +3666,19 @@ function checkAction(
|
|
|
2775
3666
|
continue;
|
|
2776
3667
|
}
|
|
2777
3668
|
if (definition?.scope === "action") {
|
|
3669
|
+
if (definition.target === "port" && row.value.kind === "block") {
|
|
3670
|
+
const allowedEntry = entry(row.value, "allowed");
|
|
3671
|
+
if (allowedEntry) {
|
|
3672
|
+
diagnostics.push({
|
|
3673
|
+
code: "HSX1508",
|
|
3674
|
+
fix: "Use allowed_parties: [...] inside an action-level port clause; allowed: is for top-level port declarations.",
|
|
3675
|
+
message:
|
|
3676
|
+
"action-level port clause uses allowed: instead of allowed_parties:",
|
|
3677
|
+
severity: "error",
|
|
3678
|
+
span: allowedEntry.key.span,
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
2778
3682
|
const rawValue = exprToJsonForUdlSlot(
|
|
2779
3683
|
row.value,
|
|
2780
3684
|
definition.target,
|
|
@@ -2982,6 +3886,109 @@ function isJsonObject(value: JsonValue): value is Record<string, JsonValue> {
|
|
|
2982
3886
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2983
3887
|
}
|
|
2984
3888
|
|
|
3889
|
+
/**
|
|
3890
|
+
* A money<SAR> field moves SAR, whatever the caller wrote in the instance
|
|
3891
|
+
* currency field. Every move of a fixed-currency amount, and every account
|
|
3892
|
+
* step of an instrument whose money is fixed, binds that currency as a
|
|
3893
|
+
* constant: a binding that reads one of the instrument's own text fields is
|
|
3894
|
+
* pinned here, and one the compiler cannot pin (an input, a ref, or a
|
|
3895
|
+
* constant naming another currency) is refused as HSX1306.
|
|
3896
|
+
*/
|
|
3897
|
+
function pinFixedCurrencyBindings(
|
|
3898
|
+
action: TypedAction,
|
|
3899
|
+
fields: readonly TypedField[],
|
|
3900
|
+
diagnostics: GeneralDiagnostic[],
|
|
3901
|
+
): TypedAction {
|
|
3902
|
+
const fieldByName = new Map(fields.map((field) => [field.name, field]));
|
|
3903
|
+
const fixedCurrencyOf = (field: TypedField | undefined) =>
|
|
3904
|
+
field?.type.kind === "money" &&
|
|
3905
|
+
field.type.currency &&
|
|
3906
|
+
CURRENCY.test(field.type.currency)
|
|
3907
|
+
? field.type.currency
|
|
3908
|
+
: undefined;
|
|
3909
|
+
const fixed = new Set(
|
|
3910
|
+
fields.flatMap((field) => fixedCurrencyOf(field) ?? []),
|
|
3911
|
+
);
|
|
3912
|
+
// Two fixed currencies is HSX1305's refusal; one decides here, none skips.
|
|
3913
|
+
const ledger = fixed.size === 1 ? [...fixed][0] : undefined;
|
|
3914
|
+
if (!ledger) return action;
|
|
3915
|
+
const instanceField = (
|
|
3916
|
+
binding: JsonValue | undefined,
|
|
3917
|
+
): TypedField | undefined =>
|
|
3918
|
+
binding !== undefined &&
|
|
3919
|
+
isJsonObject(binding) &&
|
|
3920
|
+
binding.from === "instance" &&
|
|
3921
|
+
typeof binding.path === "string" &&
|
|
3922
|
+
binding.path.startsWith("fields.")
|
|
3923
|
+
? fieldByName.get(binding.path.slice("fields.".length))
|
|
3924
|
+
: undefined;
|
|
3925
|
+
const pin = (
|
|
3926
|
+
entry: JsonValue,
|
|
3927
|
+
currency: string,
|
|
3928
|
+
subject: string,
|
|
3929
|
+
): JsonValue => {
|
|
3930
|
+
if (!isJsonObject(entry)) return entry;
|
|
3931
|
+
const bind = entry.bind;
|
|
3932
|
+
if (bind === undefined || !isJsonObject(bind)) return entry;
|
|
3933
|
+
const binding = bind.currency;
|
|
3934
|
+
if (binding === undefined) return entry;
|
|
3935
|
+
const bound =
|
|
3936
|
+
isJsonObject(binding) && binding.from === "const"
|
|
3937
|
+
? String(binding.value)
|
|
3938
|
+
: undefined;
|
|
3939
|
+
if (bound === currency) return entry;
|
|
3940
|
+
if (bound === undefined && instanceField(binding)?.type.kind === "text") {
|
|
3941
|
+
return {
|
|
3942
|
+
...entry,
|
|
3943
|
+
bind: { ...bind, currency: { from: "const", value: currency } },
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
diagnostics.push({
|
|
3947
|
+
code: "HSX1306",
|
|
3948
|
+
fix: `bind currency as { from: const; value: ${currency}; }`,
|
|
3949
|
+
message: `action ${action.name} binds the currency of ${subject} to ${bound ?? "a caller-supplied value"}; a money<${currency}> amount moves in ${currency}`,
|
|
3950
|
+
severity: "error",
|
|
3951
|
+
span: action.origin,
|
|
3952
|
+
});
|
|
3953
|
+
return entry;
|
|
3954
|
+
};
|
|
3955
|
+
const moves = action.slots.moves ?? [];
|
|
3956
|
+
const steps = action.slots.steps ?? [];
|
|
3957
|
+
return {
|
|
3958
|
+
...action,
|
|
3959
|
+
slots: {
|
|
3960
|
+
...action.slots,
|
|
3961
|
+
moves: Array.isArray(moves)
|
|
3962
|
+
? moves.map((move) => {
|
|
3963
|
+
if (!isJsonObject(move)) return move;
|
|
3964
|
+
// A post settles a reservation; the ledger refuses a post whose
|
|
3965
|
+
// currency differs from the reserve, which is pinned here.
|
|
3966
|
+
if (move.operation === "internal_transfer.post") return move;
|
|
3967
|
+
const bind = move.bind;
|
|
3968
|
+
const amount =
|
|
3969
|
+
bind !== undefined && isJsonObject(bind)
|
|
3970
|
+
? instanceField(bind.amount)
|
|
3971
|
+
: undefined;
|
|
3972
|
+
return pin(
|
|
3973
|
+
move,
|
|
3974
|
+
fixedCurrencyOf(amount) ?? ledger,
|
|
3975
|
+
amount ? `fields.${amount.name}` : "the moved amount",
|
|
3976
|
+
);
|
|
3977
|
+
})
|
|
3978
|
+
: moves,
|
|
3979
|
+
steps: Array.isArray(steps)
|
|
3980
|
+
? steps.map((step) =>
|
|
3981
|
+
isJsonObject(step) &&
|
|
3982
|
+
typeof step.operation === "string" &&
|
|
3983
|
+
step.operation.startsWith("account.")
|
|
3984
|
+
? pin(step, ledger, `the ${step.operation} account`)
|
|
3985
|
+
: step,
|
|
3986
|
+
)
|
|
3987
|
+
: steps,
|
|
3988
|
+
},
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
|
|
2985
3992
|
function bindFieldReferences(
|
|
2986
3993
|
slots: Readonly<Record<string, JsonValue>>,
|
|
2987
3994
|
fields: ReadonlySet<string>,
|
|
@@ -3109,10 +4116,22 @@ function crossInstrumentReferenceDiagnostics(
|
|
|
3109
4116
|
);
|
|
3110
4117
|
}
|
|
3111
4118
|
}
|
|
4119
|
+
const parentField = invariant.parentField;
|
|
4120
|
+
const parent =
|
|
4121
|
+
typeof parentField === "string"
|
|
4122
|
+
? fieldIn(owner, parentField)
|
|
4123
|
+
: undefined;
|
|
3112
4124
|
const childField = invariant.childField;
|
|
4125
|
+
const childFieldDef =
|
|
4126
|
+
typeof childField === "string" ? fieldIn(child, childField) : undefined;
|
|
4127
|
+
const isIntegerSum =
|
|
4128
|
+
invariant.count !== true &&
|
|
4129
|
+
parent?.type.kind === "integer" &&
|
|
4130
|
+
childFieldDef?.type.kind === "integer";
|
|
3113
4131
|
if (
|
|
3114
4132
|
typeof childField === "string" &&
|
|
3115
|
-
|
|
4133
|
+
!isIntegerSum &&
|
|
4134
|
+
childFieldDef?.type.kind !== "money"
|
|
3116
4135
|
) {
|
|
3117
4136
|
report(
|
|
3118
4137
|
owner,
|
|
@@ -3134,19 +4153,15 @@ function crossInstrumentReferenceDiagnostics(
|
|
|
3134
4153
|
);
|
|
3135
4154
|
}
|
|
3136
4155
|
}
|
|
3137
|
-
const parentField = invariant.parentField;
|
|
3138
|
-
const parent =
|
|
3139
|
-
typeof parentField === "string"
|
|
3140
|
-
? fieldIn(owner, parentField)
|
|
3141
|
-
: undefined;
|
|
3142
4156
|
if (
|
|
3143
4157
|
typeof parentField === "string" &&
|
|
3144
4158
|
(!parent ||
|
|
3145
|
-
(invariant.count === true
|
|
4159
|
+
(invariant.count === true || isIntegerSum
|
|
3146
4160
|
? parent.type.kind !== "integer"
|
|
3147
4161
|
: parent.type.kind !== "money"))
|
|
3148
4162
|
) {
|
|
3149
|
-
const expected =
|
|
4163
|
+
const expected =
|
|
4164
|
+
invariant.count === true || isIntegerSum ? "integer" : "money<C>";
|
|
3150
4165
|
report(
|
|
3151
4166
|
owner,
|
|
3152
4167
|
owner.origin,
|
|
@@ -3792,16 +4807,15 @@ function schemaFor(
|
|
|
3792
4807
|
): Record<string, JsonValue> {
|
|
3793
4808
|
switch (type.kind) {
|
|
3794
4809
|
case "money":
|
|
3795
|
-
return
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
};
|
|
4810
|
+
return {
|
|
4811
|
+
...(type.fixedAmount
|
|
4812
|
+
? { const: type.fixedAmount, pattern: MONEY_PATTERN }
|
|
4813
|
+
: { pattern: required ? MONEY_PATTERN : OPTIONAL_MONEY_PATTERN }),
|
|
4814
|
+
type: "string",
|
|
4815
|
+
...(type.currency && CURRENCY.test(type.currency)
|
|
4816
|
+
? { "x-hyperscale-currency": type.currency }
|
|
4817
|
+
: {}),
|
|
4818
|
+
};
|
|
3805
4819
|
case "bps":
|
|
3806
4820
|
case "percent":
|
|
3807
4821
|
return { maximum: 10_000, minimum: 0, type: "integer" };
|
|
@@ -3813,6 +4827,8 @@ function schemaFor(
|
|
|
3813
4827
|
return { format: "hyperscale-date-time", type: "string" };
|
|
3814
4828
|
case "ref":
|
|
3815
4829
|
return { type: "string" };
|
|
4830
|
+
case "account":
|
|
4831
|
+
return { pattern: ACCOUNT_PATTERN, type: "string" };
|
|
3816
4832
|
default:
|
|
3817
4833
|
return { type: "string" };
|
|
3818
4834
|
}
|