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