@grimoirelabs/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builders/expressions.d.ts +33 -0
- package/dist/builders/expressions.d.ts.map +1 -0
- package/dist/builders/expressions.js +57 -0
- package/dist/builders/expressions.js.map +1 -0
- package/dist/builders/index.d.ts +44 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +32 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/builders/spell-builder.d.ts +124 -0
- package/dist/builders/spell-builder.d.ts.map +1 -0
- package/dist/builders/spell-builder.js +299 -0
- package/dist/builders/spell-builder.js.map +1 -0
- package/dist/builders/step-builder.d.ts +212 -0
- package/dist/builders/step-builder.d.ts.map +1 -0
- package/dist/builders/step-builder.js +499 -0
- package/dist/builders/step-builder.js.map +1 -0
- package/dist/compiler/expression-parser.d.ts +14 -0
- package/dist/compiler/expression-parser.d.ts.map +1 -0
- package/dist/compiler/expression-parser.js +460 -0
- package/dist/compiler/expression-parser.js.map +1 -0
- package/dist/compiler/grimoire/ast.d.ts +450 -0
- package/dist/compiler/grimoire/ast.d.ts.map +1 -0
- package/dist/compiler/grimoire/ast.js +19 -0
- package/dist/compiler/grimoire/ast.js.map +1 -0
- package/dist/compiler/grimoire/errors.d.ts +65 -0
- package/dist/compiler/grimoire/errors.d.ts.map +1 -0
- package/dist/compiler/grimoire/errors.js +86 -0
- package/dist/compiler/grimoire/errors.js.map +1 -0
- package/dist/compiler/grimoire/index.d.ts +24 -0
- package/dist/compiler/grimoire/index.d.ts.map +1 -0
- package/dist/compiler/grimoire/index.js +63 -0
- package/dist/compiler/grimoire/index.js.map +1 -0
- package/dist/compiler/grimoire/parser.d.ts +135 -0
- package/dist/compiler/grimoire/parser.d.ts.map +1 -0
- package/dist/compiler/grimoire/parser.js +2148 -0
- package/dist/compiler/grimoire/parser.js.map +1 -0
- package/dist/compiler/grimoire/tokenizer.d.ts +59 -0
- package/dist/compiler/grimoire/tokenizer.d.ts.map +1 -0
- package/dist/compiler/grimoire/tokenizer.js +509 -0
- package/dist/compiler/grimoire/tokenizer.js.map +1 -0
- package/dist/compiler/grimoire/transformer.d.ts +71 -0
- package/dist/compiler/grimoire/transformer.d.ts.map +1 -0
- package/dist/compiler/grimoire/transformer.js +1011 -0
- package/dist/compiler/grimoire/transformer.js.map +1 -0
- package/dist/compiler/index.d.ts +45 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +97 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/ir-generator.d.ts +16 -0
- package/dist/compiler/ir-generator.d.ts.map +1 -0
- package/dist/compiler/ir-generator.js +997 -0
- package/dist/compiler/ir-generator.js.map +1 -0
- package/dist/compiler/validator.d.ts +15 -0
- package/dist/compiler/validator.d.ts.map +1 -0
- package/dist/compiler/validator.js +401 -0
- package/dist/compiler/validator.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/circuit-breaker.d.ts +59 -0
- package/dist/runtime/circuit-breaker.d.ts.map +1 -0
- package/dist/runtime/circuit-breaker.js +155 -0
- package/dist/runtime/circuit-breaker.js.map +1 -0
- package/dist/runtime/context.d.ts +92 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +219 -0
- package/dist/runtime/context.js.map +1 -0
- package/dist/runtime/error-classifier.d.ts +16 -0
- package/dist/runtime/error-classifier.d.ts.map +1 -0
- package/dist/runtime/error-classifier.js +38 -0
- package/dist/runtime/error-classifier.js.map +1 -0
- package/dist/runtime/expression-evaluator.d.ts +36 -0
- package/dist/runtime/expression-evaluator.d.ts.map +1 -0
- package/dist/runtime/expression-evaluator.js +391 -0
- package/dist/runtime/expression-evaluator.js.map +1 -0
- package/dist/runtime/index.d.ts +12 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +11 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/interpreter.d.ts +59 -0
- package/dist/runtime/interpreter.d.ts.map +1 -0
- package/dist/runtime/interpreter.js +414 -0
- package/dist/runtime/interpreter.js.map +1 -0
- package/dist/runtime/skills/registry.d.ts +11 -0
- package/dist/runtime/skills/registry.d.ts.map +1 -0
- package/dist/runtime/skills/registry.js +73 -0
- package/dist/runtime/skills/registry.js.map +1 -0
- package/dist/runtime/sqlite-state-store.d.ts +28 -0
- package/dist/runtime/sqlite-state-store.d.ts.map +1 -0
- package/dist/runtime/sqlite-state-store.js +180 -0
- package/dist/runtime/sqlite-state-store.js.map +1 -0
- package/dist/runtime/state-store.d.ts +52 -0
- package/dist/runtime/state-store.d.ts.map +1 -0
- package/dist/runtime/state-store.js +32 -0
- package/dist/runtime/state-store.js.map +1 -0
- package/dist/runtime/steps/action.d.ts +17 -0
- package/dist/runtime/steps/action.d.ts.map +1 -0
- package/dist/runtime/steps/action.js +430 -0
- package/dist/runtime/steps/action.js.map +1 -0
- package/dist/runtime/steps/advisory.d.ts +28 -0
- package/dist/runtime/steps/advisory.d.ts.map +1 -0
- package/dist/runtime/steps/advisory.js +209 -0
- package/dist/runtime/steps/advisory.js.map +1 -0
- package/dist/runtime/steps/compute.d.ts +9 -0
- package/dist/runtime/steps/compute.d.ts.map +1 -0
- package/dist/runtime/steps/compute.js +74 -0
- package/dist/runtime/steps/compute.js.map +1 -0
- package/dist/runtime/steps/conditional.d.ts +14 -0
- package/dist/runtime/steps/conditional.d.ts.map +1 -0
- package/dist/runtime/steps/conditional.js +58 -0
- package/dist/runtime/steps/conditional.js.map +1 -0
- package/dist/runtime/steps/emit.d.ts +9 -0
- package/dist/runtime/steps/emit.d.ts.map +1 -0
- package/dist/runtime/steps/emit.js +70 -0
- package/dist/runtime/steps/emit.js.map +1 -0
- package/dist/runtime/steps/halt.d.ts +9 -0
- package/dist/runtime/steps/halt.d.ts.map +1 -0
- package/dist/runtime/steps/halt.js +19 -0
- package/dist/runtime/steps/halt.js.map +1 -0
- package/dist/runtime/steps/loop.d.ts +14 -0
- package/dist/runtime/steps/loop.d.ts.map +1 -0
- package/dist/runtime/steps/loop.js +109 -0
- package/dist/runtime/steps/loop.js.map +1 -0
- package/dist/runtime/steps/parallel.d.ts +9 -0
- package/dist/runtime/steps/parallel.d.ts.map +1 -0
- package/dist/runtime/steps/parallel.js +87 -0
- package/dist/runtime/steps/parallel.js.map +1 -0
- package/dist/runtime/steps/pipeline.d.ts +9 -0
- package/dist/runtime/steps/pipeline.d.ts.map +1 -0
- package/dist/runtime/steps/pipeline.js +125 -0
- package/dist/runtime/steps/pipeline.js.map +1 -0
- package/dist/runtime/steps/try.d.ts +13 -0
- package/dist/runtime/steps/try.d.ts.map +1 -0
- package/dist/runtime/steps/try.js +222 -0
- package/dist/runtime/steps/try.js.map +1 -0
- package/dist/runtime/steps/wait.d.ts +9 -0
- package/dist/runtime/steps/wait.d.ts.map +1 -0
- package/dist/runtime/steps/wait.js +38 -0
- package/dist/runtime/steps/wait.js.map +1 -0
- package/dist/types/actions.d.ts +162 -0
- package/dist/types/actions.d.ts.map +1 -0
- package/dist/types/actions.js +5 -0
- package/dist/types/actions.js.map +1 -0
- package/dist/types/execution.d.ts +276 -0
- package/dist/types/execution.d.ts.map +1 -0
- package/dist/types/execution.js +5 -0
- package/dist/types/execution.js.map +1 -0
- package/dist/types/expressions.d.ts +100 -0
- package/dist/types/expressions.d.ts.map +1 -0
- package/dist/types/expressions.js +48 -0
- package/dist/types/expressions.js.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/ir.d.ts +187 -0
- package/dist/types/ir.d.ts.map +1 -0
- package/dist/types/ir.js +5 -0
- package/dist/types/ir.js.map +1 -0
- package/dist/types/policy.d.ts +123 -0
- package/dist/types/policy.d.ts.map +1 -0
- package/dist/types/policy.js +5 -0
- package/dist/types/policy.js.map +1 -0
- package/dist/types/primitives.d.ts +76 -0
- package/dist/types/primitives.d.ts.map +1 -0
- package/dist/types/primitives.js +10 -0
- package/dist/types/primitives.js.map +1 -0
- package/dist/types/steps.d.ts +226 -0
- package/dist/types/steps.d.ts.map +1 -0
- package/dist/types/steps.js +5 -0
- package/dist/types/steps.js.map +1 -0
- package/dist/venues/index.d.ts +6 -0
- package/dist/venues/index.d.ts.map +1 -0
- package/dist/venues/index.js +26 -0
- package/dist/venues/index.js.map +1 -0
- package/dist/venues/types.d.ts +40 -0
- package/dist/venues/types.d.ts.map +1 -0
- package/dist/venues/types.js +5 -0
- package/dist/venues/types.js.map +1 -0
- package/dist/wallet/executor.d.ts +109 -0
- package/dist/wallet/executor.d.ts.map +1 -0
- package/dist/wallet/executor.js +354 -0
- package/dist/wallet/executor.js.map +1 -0
- package/dist/wallet/index.d.ts +14 -0
- package/dist/wallet/index.d.ts.map +1 -0
- package/dist/wallet/index.js +13 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/keystore.d.ts +44 -0
- package/dist/wallet/keystore.d.ts.map +1 -0
- package/dist/wallet/keystore.js +296 -0
- package/dist/wallet/keystore.js.map +1 -0
- package/dist/wallet/provider.d.ts +111 -0
- package/dist/wallet/provider.d.ts.map +1 -0
- package/dist/wallet/provider.js +309 -0
- package/dist/wallet/provider.js.map +1 -0
- package/dist/wallet/tx-builder.d.ts +85 -0
- package/dist/wallet/tx-builder.d.ts.map +1 -0
- package/dist/wallet/tx-builder.js +290 -0
- package/dist/wallet/tx-builder.js.map +1 -0
- package/dist/wallet/types.d.ts +116 -0
- package/dist/wallet/types.d.ts.map +1 -0
- package/dist/wallet/types.js +86 -0
- package/dist/wallet/types.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,2148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive descent parser for Grimoire syntax
|
|
3
|
+
*/
|
|
4
|
+
import { ParseError } from "./errors.js";
|
|
5
|
+
import { tokenize } from "./tokenizer.js";
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// PARSER CLASS
|
|
8
|
+
// =============================================================================
|
|
9
|
+
export class Parser {
|
|
10
|
+
tokens;
|
|
11
|
+
source;
|
|
12
|
+
pos = 0;
|
|
13
|
+
constructor(tokens, source) {
|
|
14
|
+
this.tokens = tokens;
|
|
15
|
+
this.source = source;
|
|
16
|
+
}
|
|
17
|
+
// ===========================================================================
|
|
18
|
+
// UTILITIES
|
|
19
|
+
// ===========================================================================
|
|
20
|
+
/** Get current token */
|
|
21
|
+
current() {
|
|
22
|
+
return (this.tokens[this.pos] ?? {
|
|
23
|
+
type: "EOF",
|
|
24
|
+
value: "",
|
|
25
|
+
location: { line: 0, column: 0, offset: 0 },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Peek at token at offset */
|
|
29
|
+
peek(offset = 1) {
|
|
30
|
+
return (this.tokens[this.pos + offset] ?? {
|
|
31
|
+
type: "EOF",
|
|
32
|
+
value: "",
|
|
33
|
+
location: { line: 0, column: 0, offset: 0 },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Check if current token matches */
|
|
37
|
+
check(type, value) {
|
|
38
|
+
const token = this.current();
|
|
39
|
+
if (token.type !== type)
|
|
40
|
+
return false;
|
|
41
|
+
if (value !== undefined && token.value !== value)
|
|
42
|
+
return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/** Advance and return previous token */
|
|
46
|
+
advance() {
|
|
47
|
+
const token = this.current();
|
|
48
|
+
if (token.type !== "EOF") {
|
|
49
|
+
this.pos++;
|
|
50
|
+
}
|
|
51
|
+
return token;
|
|
52
|
+
}
|
|
53
|
+
/** Expect and consume a specific token */
|
|
54
|
+
expect(type, value) {
|
|
55
|
+
const token = this.current();
|
|
56
|
+
if (token.type !== type) {
|
|
57
|
+
throw new ParseError(`Expected ${type}${value ? ` '${value}'` : ""} but got ${token.type} '${token.value}'`, { location: token.location, source: this.source });
|
|
58
|
+
}
|
|
59
|
+
if (value !== undefined && token.value !== value) {
|
|
60
|
+
throw new ParseError(`Expected '${value}' but got '${token.value}'`, {
|
|
61
|
+
location: token.location,
|
|
62
|
+
source: this.source,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return this.advance();
|
|
66
|
+
}
|
|
67
|
+
/** Skip newlines */
|
|
68
|
+
skipNewlines() {
|
|
69
|
+
while (this.check("NEWLINE")) {
|
|
70
|
+
this.advance();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Consume a newline (required) */
|
|
74
|
+
expectNewline() {
|
|
75
|
+
if (!this.check("NEWLINE") && !this.check("EOF")) {
|
|
76
|
+
throw new ParseError(`Expected newline but got ${this.current().type} '${this.current().value}'`, { location: this.current().location, source: this.source });
|
|
77
|
+
}
|
|
78
|
+
if (this.check("NEWLINE")) {
|
|
79
|
+
this.advance();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Create a source span from a start location to the current token's location */
|
|
83
|
+
makeSpan(startToken) {
|
|
84
|
+
const end = this.current().location;
|
|
85
|
+
return { start: startToken.location, end };
|
|
86
|
+
}
|
|
87
|
+
// ===========================================================================
|
|
88
|
+
// TOP-LEVEL PARSING
|
|
89
|
+
// ===========================================================================
|
|
90
|
+
/** Parse a complete spell file */
|
|
91
|
+
parseSpellFile() {
|
|
92
|
+
this.skipNewlines();
|
|
93
|
+
// Expect: spell Name
|
|
94
|
+
this.expect("KEYWORD", "spell");
|
|
95
|
+
const nameToken = this.expect("IDENTIFIER");
|
|
96
|
+
const name = nameToken.value;
|
|
97
|
+
this.expectNewline();
|
|
98
|
+
this.skipNewlines();
|
|
99
|
+
// Parse sections and triggers
|
|
100
|
+
const sections = [];
|
|
101
|
+
const triggers = [];
|
|
102
|
+
const imports = [];
|
|
103
|
+
const blocks = [];
|
|
104
|
+
// Expect INDENT for spell body
|
|
105
|
+
if (!this.check("INDENT")) {
|
|
106
|
+
throw new ParseError("Expected indented spell body", {
|
|
107
|
+
location: this.current().location,
|
|
108
|
+
source: this.source,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
this.advance(); // INDENT
|
|
112
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
113
|
+
this.skipNewlines();
|
|
114
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
115
|
+
break;
|
|
116
|
+
// Check for import or block or trigger
|
|
117
|
+
if (this.check("KEYWORD", "import")) {
|
|
118
|
+
imports.push(this.parseImport());
|
|
119
|
+
}
|
|
120
|
+
else if (this.check("KEYWORD", "block")) {
|
|
121
|
+
blocks.push(this.parseBlock());
|
|
122
|
+
}
|
|
123
|
+
else if (this.check("KEYWORD", "on")) {
|
|
124
|
+
triggers.push(this.parseTriggerHandler());
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
sections.push(this.parseSection());
|
|
128
|
+
}
|
|
129
|
+
this.skipNewlines();
|
|
130
|
+
}
|
|
131
|
+
// Consume DEDENT
|
|
132
|
+
if (this.check("DEDENT")) {
|
|
133
|
+
this.advance();
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
kind: "spell",
|
|
137
|
+
name,
|
|
138
|
+
sections,
|
|
139
|
+
triggers,
|
|
140
|
+
imports,
|
|
141
|
+
blocks,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ===========================================================================
|
|
145
|
+
// SECTION PARSING
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
/** Parse import declaration: import "path" */
|
|
148
|
+
parseImport() {
|
|
149
|
+
const startToken = this.current();
|
|
150
|
+
this.expect("KEYWORD", "import");
|
|
151
|
+
const pathToken = this.expect("STRING");
|
|
152
|
+
let alias;
|
|
153
|
+
if (this.check("KEYWORD", "as") ||
|
|
154
|
+
(this.check("IDENTIFIER") && this.current().value === "as")) {
|
|
155
|
+
this.advance();
|
|
156
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
157
|
+
alias = this.advance().value;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
throw new ParseError("Expected alias name after 'as'", {
|
|
161
|
+
location: this.current().location,
|
|
162
|
+
source: this.source,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.expectNewline();
|
|
167
|
+
const node = { kind: "import", path: pathToken.value, alias };
|
|
168
|
+
node.span = this.makeSpan(startToken);
|
|
169
|
+
return node;
|
|
170
|
+
}
|
|
171
|
+
/** Parse block definition: block name(arg, ...): */
|
|
172
|
+
parseBlock() {
|
|
173
|
+
const startToken = this.current();
|
|
174
|
+
this.expect("KEYWORD", "block");
|
|
175
|
+
const name = this.expect("IDENTIFIER").value;
|
|
176
|
+
const params = [];
|
|
177
|
+
if (this.check("LPAREN")) {
|
|
178
|
+
this.advance();
|
|
179
|
+
while (!this.check("RPAREN")) {
|
|
180
|
+
const param = this.expect("IDENTIFIER").value;
|
|
181
|
+
params.push(param);
|
|
182
|
+
if (this.check("COMMA")) {
|
|
183
|
+
this.advance();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
this.expect("RPAREN");
|
|
187
|
+
}
|
|
188
|
+
this.expect("COLON");
|
|
189
|
+
this.expectNewline();
|
|
190
|
+
const body = this.parseStatementBlock();
|
|
191
|
+
const node = { kind: "block", name, params, body };
|
|
192
|
+
node.span = this.makeSpan(startToken);
|
|
193
|
+
return node;
|
|
194
|
+
}
|
|
195
|
+
/** Parse a section (version, assets, params, etc.) */
|
|
196
|
+
parseSection() {
|
|
197
|
+
const token = this.current();
|
|
198
|
+
if (token.type === "KEYWORD") {
|
|
199
|
+
switch (token.value) {
|
|
200
|
+
case "version":
|
|
201
|
+
return this.parseVersionSection();
|
|
202
|
+
case "description":
|
|
203
|
+
return this.parseDescriptionSection();
|
|
204
|
+
case "assets":
|
|
205
|
+
return this.parseAssetsSection();
|
|
206
|
+
case "params":
|
|
207
|
+
return this.parseParamsSection();
|
|
208
|
+
case "limits":
|
|
209
|
+
return this.parseLimitsSection();
|
|
210
|
+
case "venues":
|
|
211
|
+
return this.parseVenuesSection();
|
|
212
|
+
case "state":
|
|
213
|
+
return this.parseStateSection();
|
|
214
|
+
case "skills":
|
|
215
|
+
return this.parseSkillsSection();
|
|
216
|
+
case "advisors":
|
|
217
|
+
return this.parseAdvisorsSection();
|
|
218
|
+
case "guards":
|
|
219
|
+
return this.parseGuardsSection();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Could be a simple key: value
|
|
223
|
+
if (token.type === "IDENTIFIER") {
|
|
224
|
+
const key = token.value;
|
|
225
|
+
this.advance();
|
|
226
|
+
this.expect("COLON");
|
|
227
|
+
// Handle known identifiers as sections
|
|
228
|
+
if (key === "version") {
|
|
229
|
+
const value = this.parseSimpleValue();
|
|
230
|
+
this.expectNewline();
|
|
231
|
+
return { kind: "version", value: String(value) };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw new ParseError(`Unexpected token in section: ${token.type} '${token.value}'`, {
|
|
235
|
+
location: token.location,
|
|
236
|
+
source: this.source,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** Parse version: "1.0.0" */
|
|
240
|
+
parseVersionSection() {
|
|
241
|
+
this.expect("KEYWORD", "version");
|
|
242
|
+
this.expect("COLON");
|
|
243
|
+
const value = this.parseSimpleValue();
|
|
244
|
+
this.expectNewline();
|
|
245
|
+
return { kind: "version", value: String(value) };
|
|
246
|
+
}
|
|
247
|
+
/** Parse description: "..." */
|
|
248
|
+
parseDescriptionSection() {
|
|
249
|
+
this.expect("KEYWORD", "description");
|
|
250
|
+
this.expect("COLON");
|
|
251
|
+
const value = this.parseSimpleValue();
|
|
252
|
+
this.expectNewline();
|
|
253
|
+
return { kind: "description", value: String(value) };
|
|
254
|
+
}
|
|
255
|
+
/** Parse simple value (string, number, boolean) */
|
|
256
|
+
parseSimpleValue() {
|
|
257
|
+
const token = this.current();
|
|
258
|
+
if (token.type === "STRING") {
|
|
259
|
+
this.advance();
|
|
260
|
+
return token.value;
|
|
261
|
+
}
|
|
262
|
+
if (token.type === "NUMBER") {
|
|
263
|
+
this.advance();
|
|
264
|
+
return Number.parseFloat(token.value);
|
|
265
|
+
}
|
|
266
|
+
if (token.type === "BOOLEAN") {
|
|
267
|
+
this.advance();
|
|
268
|
+
return token.value === "true";
|
|
269
|
+
}
|
|
270
|
+
throw new ParseError(`Expected value but got ${token.type}`, {
|
|
271
|
+
location: token.location,
|
|
272
|
+
source: this.source,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/** Parse list of strings/identifiers (e.g., [a, "b"]) */
|
|
276
|
+
parseStringList() {
|
|
277
|
+
const items = [];
|
|
278
|
+
if (this.check("LBRACKET")) {
|
|
279
|
+
this.advance();
|
|
280
|
+
while (!this.check("RBRACKET")) {
|
|
281
|
+
if (this.check("STRING")) {
|
|
282
|
+
items.push(this.advance().value);
|
|
283
|
+
}
|
|
284
|
+
else if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
285
|
+
items.push(this.advance().value);
|
|
286
|
+
}
|
|
287
|
+
else if (this.check("VENUE_REF")) {
|
|
288
|
+
items.push(this.advance().value);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
throw new ParseError("Expected list item", {
|
|
292
|
+
location: this.current().location,
|
|
293
|
+
source: this.source,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (this.check("COMMA"))
|
|
297
|
+
this.advance();
|
|
298
|
+
}
|
|
299
|
+
this.expect("RBRACKET");
|
|
300
|
+
return items;
|
|
301
|
+
}
|
|
302
|
+
// Single item
|
|
303
|
+
if (this.check("STRING")) {
|
|
304
|
+
items.push(this.advance().value);
|
|
305
|
+
return items;
|
|
306
|
+
}
|
|
307
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
308
|
+
items.push(this.advance().value);
|
|
309
|
+
return items;
|
|
310
|
+
}
|
|
311
|
+
if (this.check("VENUE_REF")) {
|
|
312
|
+
items.push(this.advance().value);
|
|
313
|
+
return items;
|
|
314
|
+
}
|
|
315
|
+
throw new ParseError("Expected list", {
|
|
316
|
+
location: this.current().location,
|
|
317
|
+
source: this.source,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/** Parse assets: [USDC, USDT] or assets block */
|
|
321
|
+
parseAssetsSection() {
|
|
322
|
+
this.expect("KEYWORD", "assets");
|
|
323
|
+
this.expect("COLON");
|
|
324
|
+
const items = [];
|
|
325
|
+
// Inline array: [USDC, USDT, DAI]
|
|
326
|
+
if (this.check("LBRACKET")) {
|
|
327
|
+
this.advance();
|
|
328
|
+
while (!this.check("RBRACKET")) {
|
|
329
|
+
const symbol = this.expect("IDENTIFIER").value;
|
|
330
|
+
items.push({ symbol });
|
|
331
|
+
if (this.check("COMMA")) {
|
|
332
|
+
this.advance();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
this.expect("RBRACKET");
|
|
336
|
+
this.expectNewline();
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// Block form
|
|
340
|
+
this.expectNewline();
|
|
341
|
+
if (this.check("INDENT")) {
|
|
342
|
+
this.advance();
|
|
343
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
344
|
+
this.skipNewlines();
|
|
345
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
346
|
+
break;
|
|
347
|
+
const symbol = this.expect("IDENTIFIER").value;
|
|
348
|
+
const asset = { symbol };
|
|
349
|
+
this.expect("COLON");
|
|
350
|
+
if (this.check("NEWLINE")) {
|
|
351
|
+
this.expectNewline();
|
|
352
|
+
if (!this.check("INDENT")) {
|
|
353
|
+
throw new ParseError("Expected indented asset block", {
|
|
354
|
+
location: this.current().location,
|
|
355
|
+
source: this.source,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
this.advance();
|
|
359
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
360
|
+
this.skipNewlines();
|
|
361
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
362
|
+
break;
|
|
363
|
+
const key = this.expect("IDENTIFIER").value;
|
|
364
|
+
this.expect("COLON");
|
|
365
|
+
if (key === "chain") {
|
|
366
|
+
const val = this.expect("NUMBER").value;
|
|
367
|
+
asset.chain = Number.parseInt(val, 10);
|
|
368
|
+
this.expectNewline();
|
|
369
|
+
}
|
|
370
|
+
else if (key === "address") {
|
|
371
|
+
if (this.check("ADDRESS")) {
|
|
372
|
+
asset.address = this.advance().value;
|
|
373
|
+
}
|
|
374
|
+
else if (this.check("STRING")) {
|
|
375
|
+
asset.address = this.advance().value;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
throw new ParseError("Expected address value", {
|
|
379
|
+
location: this.current().location,
|
|
380
|
+
source: this.source,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
this.expectNewline();
|
|
384
|
+
}
|
|
385
|
+
else if (key === "decimals") {
|
|
386
|
+
const val = this.expect("NUMBER").value;
|
|
387
|
+
asset.decimals = Number.parseInt(val, 10);
|
|
388
|
+
this.expectNewline();
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
this.parseExpression();
|
|
392
|
+
this.expectNewline();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (this.check("DEDENT"))
|
|
396
|
+
this.advance();
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// Inline asset defaults: assets: SYMBOL: <ignored>
|
|
400
|
+
this.parseExpression();
|
|
401
|
+
this.expectNewline();
|
|
402
|
+
}
|
|
403
|
+
items.push(asset);
|
|
404
|
+
}
|
|
405
|
+
if (this.check("DEDENT"))
|
|
406
|
+
this.advance();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return { kind: "assets", items };
|
|
410
|
+
}
|
|
411
|
+
/** Parse params section */
|
|
412
|
+
parseParamsSection() {
|
|
413
|
+
this.expect("KEYWORD", "params");
|
|
414
|
+
this.expect("COLON");
|
|
415
|
+
this.expectNewline();
|
|
416
|
+
const items = [];
|
|
417
|
+
if (this.check("INDENT")) {
|
|
418
|
+
this.advance();
|
|
419
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
420
|
+
this.skipNewlines();
|
|
421
|
+
if (this.check("DEDENT"))
|
|
422
|
+
break;
|
|
423
|
+
const name = this.expect("IDENTIFIER").value;
|
|
424
|
+
this.expect("COLON");
|
|
425
|
+
const item = { name };
|
|
426
|
+
// Block form: param:
|
|
427
|
+
if (this.check("NEWLINE")) {
|
|
428
|
+
this.expectNewline();
|
|
429
|
+
if (!this.check("INDENT")) {
|
|
430
|
+
throw new ParseError("Expected indented param block", {
|
|
431
|
+
location: this.current().location,
|
|
432
|
+
source: this.source,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
this.advance();
|
|
436
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
437
|
+
this.skipNewlines();
|
|
438
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
439
|
+
break;
|
|
440
|
+
const keyToken = this.current();
|
|
441
|
+
if (!(this.check("IDENTIFIER") || this.check("KEYWORD"))) {
|
|
442
|
+
throw new ParseError(`Expected param field but got ${this.current().type} '${this.current().value}'`, { location: this.current().location, source: this.source });
|
|
443
|
+
}
|
|
444
|
+
const key = keyToken.value;
|
|
445
|
+
this.advance();
|
|
446
|
+
this.expect("COLON");
|
|
447
|
+
if (key === "type") {
|
|
448
|
+
if (this.check("STRING")) {
|
|
449
|
+
item.type = this.advance().value;
|
|
450
|
+
}
|
|
451
|
+
else if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
452
|
+
item.type = this.advance().value;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
throw new ParseError("Expected param type value", {
|
|
456
|
+
location: keyToken.location,
|
|
457
|
+
source: this.source,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
this.expectNewline();
|
|
461
|
+
}
|
|
462
|
+
else if (key === "asset") {
|
|
463
|
+
if (this.check("STRING")) {
|
|
464
|
+
item.asset = this.advance().value;
|
|
465
|
+
}
|
|
466
|
+
else if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
467
|
+
item.asset = this.advance().value;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
throw new ParseError("Expected asset identifier", {
|
|
471
|
+
location: keyToken.location,
|
|
472
|
+
source: this.source,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
this.expectNewline();
|
|
476
|
+
}
|
|
477
|
+
else if (key === "default") {
|
|
478
|
+
item.value = this.parseExpression();
|
|
479
|
+
this.expectNewline();
|
|
480
|
+
}
|
|
481
|
+
else if (key === "min") {
|
|
482
|
+
const val = this.expect("NUMBER").value;
|
|
483
|
+
item.min = Number.parseFloat(val);
|
|
484
|
+
this.expectNewline();
|
|
485
|
+
}
|
|
486
|
+
else if (key === "max") {
|
|
487
|
+
const val = this.expect("NUMBER").value;
|
|
488
|
+
item.max = Number.parseFloat(val);
|
|
489
|
+
this.expectNewline();
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// Unknown field - parse and ignore
|
|
493
|
+
this.parseExpression();
|
|
494
|
+
this.expectNewline();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (this.check("DEDENT"))
|
|
498
|
+
this.advance();
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
// Inline form: name: value
|
|
502
|
+
item.value = this.parseExpression();
|
|
503
|
+
this.expectNewline();
|
|
504
|
+
}
|
|
505
|
+
items.push(item);
|
|
506
|
+
this.skipNewlines();
|
|
507
|
+
}
|
|
508
|
+
if (this.check("DEDENT"))
|
|
509
|
+
this.advance();
|
|
510
|
+
}
|
|
511
|
+
return { kind: "params", items };
|
|
512
|
+
}
|
|
513
|
+
/** Parse limits section */
|
|
514
|
+
parseLimitsSection() {
|
|
515
|
+
this.expect("KEYWORD", "limits");
|
|
516
|
+
this.expect("COLON");
|
|
517
|
+
this.expectNewline();
|
|
518
|
+
const items = [];
|
|
519
|
+
if (this.check("INDENT")) {
|
|
520
|
+
this.advance();
|
|
521
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
522
|
+
this.skipNewlines();
|
|
523
|
+
if (this.check("DEDENT"))
|
|
524
|
+
break;
|
|
525
|
+
const name = this.expect("IDENTIFIER").value;
|
|
526
|
+
this.expect("COLON");
|
|
527
|
+
const value = this.parseExpression();
|
|
528
|
+
items.push({ name, value });
|
|
529
|
+
this.expectNewline();
|
|
530
|
+
this.skipNewlines();
|
|
531
|
+
}
|
|
532
|
+
if (this.check("DEDENT"))
|
|
533
|
+
this.advance();
|
|
534
|
+
}
|
|
535
|
+
return { kind: "limits", items };
|
|
536
|
+
}
|
|
537
|
+
/** Parse venues section */
|
|
538
|
+
parseVenuesSection() {
|
|
539
|
+
this.expect("KEYWORD", "venues");
|
|
540
|
+
this.expect("COLON");
|
|
541
|
+
this.expectNewline();
|
|
542
|
+
const groups = [];
|
|
543
|
+
if (this.check("INDENT")) {
|
|
544
|
+
this.advance();
|
|
545
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
546
|
+
this.skipNewlines();
|
|
547
|
+
if (this.check("DEDENT"))
|
|
548
|
+
break;
|
|
549
|
+
const name = this.expect("IDENTIFIER").value;
|
|
550
|
+
this.expect("COLON");
|
|
551
|
+
const venues = [];
|
|
552
|
+
// Single venue: swap: @uniswap_v3
|
|
553
|
+
if (this.check("VENUE_REF")) {
|
|
554
|
+
const venueToken = this.advance();
|
|
555
|
+
venues.push({ kind: "venue_ref", name: venueToken.value });
|
|
556
|
+
}
|
|
557
|
+
// Array of venues: lending: [@aave_v3, @morpho]
|
|
558
|
+
else if (this.check("LBRACKET")) {
|
|
559
|
+
this.advance();
|
|
560
|
+
while (!this.check("RBRACKET")) {
|
|
561
|
+
if (this.check("VENUE_REF")) {
|
|
562
|
+
const venueToken = this.advance();
|
|
563
|
+
venues.push({ kind: "venue_ref", name: venueToken.value });
|
|
564
|
+
}
|
|
565
|
+
if (this.check("COMMA")) {
|
|
566
|
+
this.advance();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
this.expect("RBRACKET");
|
|
570
|
+
}
|
|
571
|
+
groups.push({ name, venues });
|
|
572
|
+
this.expectNewline();
|
|
573
|
+
this.skipNewlines();
|
|
574
|
+
}
|
|
575
|
+
if (this.check("DEDENT"))
|
|
576
|
+
this.advance();
|
|
577
|
+
}
|
|
578
|
+
return { kind: "venues", groups };
|
|
579
|
+
}
|
|
580
|
+
/** Parse state section */
|
|
581
|
+
parseStateSection() {
|
|
582
|
+
this.expect("KEYWORD", "state");
|
|
583
|
+
this.expect("COLON");
|
|
584
|
+
this.expectNewline();
|
|
585
|
+
const persistent = [];
|
|
586
|
+
const ephemeral = [];
|
|
587
|
+
if (this.check("INDENT")) {
|
|
588
|
+
this.advance();
|
|
589
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
590
|
+
this.skipNewlines();
|
|
591
|
+
if (this.check("DEDENT"))
|
|
592
|
+
break;
|
|
593
|
+
const scope = this.expect("KEYWORD").value;
|
|
594
|
+
this.expect("COLON");
|
|
595
|
+
this.expectNewline();
|
|
596
|
+
const items = scope === "persistent" ? persistent : ephemeral;
|
|
597
|
+
if (this.check("INDENT")) {
|
|
598
|
+
this.advance();
|
|
599
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
600
|
+
this.skipNewlines();
|
|
601
|
+
if (this.check("DEDENT"))
|
|
602
|
+
break;
|
|
603
|
+
const name = this.expect("IDENTIFIER").value;
|
|
604
|
+
this.expect("COLON");
|
|
605
|
+
const initialValue = this.parseExpression();
|
|
606
|
+
items.push({ name, initialValue });
|
|
607
|
+
this.expectNewline();
|
|
608
|
+
this.skipNewlines();
|
|
609
|
+
}
|
|
610
|
+
if (this.check("DEDENT"))
|
|
611
|
+
this.advance();
|
|
612
|
+
}
|
|
613
|
+
this.skipNewlines();
|
|
614
|
+
}
|
|
615
|
+
if (this.check("DEDENT"))
|
|
616
|
+
this.advance();
|
|
617
|
+
}
|
|
618
|
+
return { kind: "state", persistent, ephemeral };
|
|
619
|
+
}
|
|
620
|
+
/** Parse skills section */
|
|
621
|
+
parseSkillsSection() {
|
|
622
|
+
const startToken = this.current();
|
|
623
|
+
this.expect("KEYWORD", "skills");
|
|
624
|
+
this.expect("COLON");
|
|
625
|
+
this.expectNewline();
|
|
626
|
+
const items = [];
|
|
627
|
+
if (this.check("INDENT")) {
|
|
628
|
+
this.advance();
|
|
629
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
630
|
+
this.skipNewlines();
|
|
631
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
632
|
+
break;
|
|
633
|
+
const name = this.expect("IDENTIFIER").value;
|
|
634
|
+
this.expect("COLON");
|
|
635
|
+
this.expectNewline();
|
|
636
|
+
let type;
|
|
637
|
+
const adapters = [];
|
|
638
|
+
let defaultMaxSlippage;
|
|
639
|
+
if (this.check("INDENT")) {
|
|
640
|
+
this.advance();
|
|
641
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
642
|
+
this.skipNewlines();
|
|
643
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
644
|
+
break;
|
|
645
|
+
const keyToken = this.current();
|
|
646
|
+
if (!(this.check("IDENTIFIER") || this.check("KEYWORD"))) {
|
|
647
|
+
throw new ParseError(`Expected skill field but got ${this.current().type} '${this.current().value}'`, { location: this.current().location, source: this.source });
|
|
648
|
+
}
|
|
649
|
+
const key = keyToken.value;
|
|
650
|
+
this.advance();
|
|
651
|
+
this.expect("COLON");
|
|
652
|
+
if (key === "type") {
|
|
653
|
+
// Accept identifier or string
|
|
654
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
655
|
+
type = this.advance().value;
|
|
656
|
+
}
|
|
657
|
+
else if (this.check("STRING")) {
|
|
658
|
+
type = this.advance().value;
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
throw new ParseError("Expected skill type value", {
|
|
662
|
+
location: this.current().location,
|
|
663
|
+
source: this.source,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
this.expectNewline();
|
|
667
|
+
}
|
|
668
|
+
else if (key === "adapters") {
|
|
669
|
+
if (this.check("LBRACKET")) {
|
|
670
|
+
this.advance();
|
|
671
|
+
while (!this.check("RBRACKET")) {
|
|
672
|
+
if (this.check("VENUE_REF")) {
|
|
673
|
+
adapters.push(this.advance().value);
|
|
674
|
+
}
|
|
675
|
+
else if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
676
|
+
adapters.push(this.advance().value);
|
|
677
|
+
}
|
|
678
|
+
else if (this.check("STRING")) {
|
|
679
|
+
adapters.push(this.advance().value);
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
throw new ParseError("Expected adapter name", {
|
|
683
|
+
location: this.current().location,
|
|
684
|
+
source: this.source,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
if (this.check("COMMA"))
|
|
688
|
+
this.advance();
|
|
689
|
+
}
|
|
690
|
+
this.expect("RBRACKET");
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
// Single adapter
|
|
694
|
+
if (this.check("VENUE_REF")) {
|
|
695
|
+
adapters.push(this.advance().value);
|
|
696
|
+
}
|
|
697
|
+
else if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
698
|
+
adapters.push(this.advance().value);
|
|
699
|
+
}
|
|
700
|
+
else if (this.check("STRING")) {
|
|
701
|
+
adapters.push(this.advance().value);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
throw new ParseError("Expected adapter name", {
|
|
705
|
+
location: this.current().location,
|
|
706
|
+
source: this.source,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
this.expectNewline();
|
|
711
|
+
}
|
|
712
|
+
else if (key === "default_constraints") {
|
|
713
|
+
this.expectNewline();
|
|
714
|
+
if (this.check("INDENT")) {
|
|
715
|
+
this.advance();
|
|
716
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
717
|
+
this.skipNewlines();
|
|
718
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
719
|
+
break;
|
|
720
|
+
const dcKey = this.expect("IDENTIFIER").value;
|
|
721
|
+
this.expect("COLON");
|
|
722
|
+
const value = this.parseExpression();
|
|
723
|
+
if (dcKey === "max_slippage") {
|
|
724
|
+
defaultMaxSlippage = value;
|
|
725
|
+
}
|
|
726
|
+
this.expectNewline();
|
|
727
|
+
}
|
|
728
|
+
if (this.check("DEDENT"))
|
|
729
|
+
this.advance();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
// Unknown field, parse and ignore
|
|
734
|
+
this.parseExpression();
|
|
735
|
+
this.expectNewline();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (this.check("DEDENT"))
|
|
739
|
+
this.advance();
|
|
740
|
+
}
|
|
741
|
+
if (!type) {
|
|
742
|
+
throw new ParseError(`Skill '${name}' missing type`, {
|
|
743
|
+
location: this.current().location,
|
|
744
|
+
source: this.source,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
items.push({
|
|
748
|
+
name,
|
|
749
|
+
type,
|
|
750
|
+
adapters,
|
|
751
|
+
defaultConstraints: defaultMaxSlippage ? { maxSlippage: defaultMaxSlippage } : undefined,
|
|
752
|
+
});
|
|
753
|
+
this.skipNewlines();
|
|
754
|
+
}
|
|
755
|
+
if (this.check("DEDENT"))
|
|
756
|
+
this.advance();
|
|
757
|
+
}
|
|
758
|
+
const node = { kind: "skills", items };
|
|
759
|
+
node.span = this.makeSpan(startToken);
|
|
760
|
+
return node;
|
|
761
|
+
}
|
|
762
|
+
/** Parse advisors section */
|
|
763
|
+
parseAdvisorsSection() {
|
|
764
|
+
const startToken = this.current();
|
|
765
|
+
this.expect("KEYWORD", "advisors");
|
|
766
|
+
this.expect("COLON");
|
|
767
|
+
this.expectNewline();
|
|
768
|
+
const items = [];
|
|
769
|
+
if (this.check("INDENT")) {
|
|
770
|
+
this.advance();
|
|
771
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
772
|
+
this.skipNewlines();
|
|
773
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
774
|
+
break;
|
|
775
|
+
const name = this.expect("IDENTIFIER").value;
|
|
776
|
+
this.expect("COLON");
|
|
777
|
+
this.expectNewline();
|
|
778
|
+
const advisor = {
|
|
779
|
+
name,
|
|
780
|
+
model: "sonnet",
|
|
781
|
+
};
|
|
782
|
+
if (this.check("INDENT")) {
|
|
783
|
+
this.advance();
|
|
784
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
785
|
+
this.skipNewlines();
|
|
786
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
787
|
+
break;
|
|
788
|
+
const keyToken = this.current();
|
|
789
|
+
if (!(this.check("IDENTIFIER") || this.check("KEYWORD"))) {
|
|
790
|
+
throw new ParseError(`Expected advisor field but got ${this.current().type} '${this.current().value}'`, { location: this.current().location, source: this.source });
|
|
791
|
+
}
|
|
792
|
+
const key = keyToken.value;
|
|
793
|
+
this.advance();
|
|
794
|
+
this.expect("COLON");
|
|
795
|
+
if (key === "model") {
|
|
796
|
+
if (this.check("STRING")) {
|
|
797
|
+
advisor.model = this.advance().value;
|
|
798
|
+
}
|
|
799
|
+
else if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
800
|
+
advisor.model = this.advance().value;
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
throw new ParseError("Expected model value", {
|
|
804
|
+
location: this.current().location,
|
|
805
|
+
source: this.source,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
this.expectNewline();
|
|
809
|
+
}
|
|
810
|
+
else if (key === "system" || key === "system_prompt") {
|
|
811
|
+
const value = this.expect("STRING").value;
|
|
812
|
+
advisor.systemPrompt = value;
|
|
813
|
+
this.expectNewline();
|
|
814
|
+
}
|
|
815
|
+
else if (key === "skills") {
|
|
816
|
+
advisor.skills = this.parseStringList();
|
|
817
|
+
this.expectNewline();
|
|
818
|
+
}
|
|
819
|
+
else if (key === "allowed_tools") {
|
|
820
|
+
advisor.allowedTools = this.parseStringList();
|
|
821
|
+
this.expectNewline();
|
|
822
|
+
}
|
|
823
|
+
else if (key === "mcp") {
|
|
824
|
+
advisor.mcp = this.parseStringList();
|
|
825
|
+
this.expectNewline();
|
|
826
|
+
}
|
|
827
|
+
else if (key === "timeout") {
|
|
828
|
+
const value = this.expect("NUMBER").value;
|
|
829
|
+
advisor.timeout = Number.parseFloat(value);
|
|
830
|
+
this.expectNewline();
|
|
831
|
+
}
|
|
832
|
+
else if (key === "fallback") {
|
|
833
|
+
if (this.check("BOOLEAN")) {
|
|
834
|
+
advisor.fallback = this.advance().value === "true";
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
throw new ParseError("Expected boolean fallback", {
|
|
838
|
+
location: this.current().location,
|
|
839
|
+
source: this.source,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
this.expectNewline();
|
|
843
|
+
}
|
|
844
|
+
else if (key === "rate_limit") {
|
|
845
|
+
this.expectNewline();
|
|
846
|
+
if (this.check("INDENT")) {
|
|
847
|
+
this.advance();
|
|
848
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
849
|
+
this.skipNewlines();
|
|
850
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
851
|
+
break;
|
|
852
|
+
const rlKey = this.expect("IDENTIFIER").value;
|
|
853
|
+
this.expect("COLON");
|
|
854
|
+
const value = this.expect("NUMBER").value;
|
|
855
|
+
if (rlKey === "max_per_run") {
|
|
856
|
+
advisor.maxPerRun = Number.parseFloat(value);
|
|
857
|
+
}
|
|
858
|
+
else if (rlKey === "max_per_hour") {
|
|
859
|
+
advisor.maxPerHour = Number.parseFloat(value);
|
|
860
|
+
}
|
|
861
|
+
this.expectNewline();
|
|
862
|
+
}
|
|
863
|
+
if (this.check("DEDENT"))
|
|
864
|
+
this.advance();
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
// Unknown field - parse and ignore
|
|
869
|
+
this.parseExpression();
|
|
870
|
+
this.expectNewline();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (this.check("DEDENT"))
|
|
874
|
+
this.advance();
|
|
875
|
+
}
|
|
876
|
+
items.push(advisor);
|
|
877
|
+
this.skipNewlines();
|
|
878
|
+
}
|
|
879
|
+
if (this.check("DEDENT"))
|
|
880
|
+
this.advance();
|
|
881
|
+
}
|
|
882
|
+
const node = { kind: "advisors", items };
|
|
883
|
+
node.span = this.makeSpan(startToken);
|
|
884
|
+
return node;
|
|
885
|
+
}
|
|
886
|
+
/** Parse guards section: guards:\n id: expression */
|
|
887
|
+
parseGuardsSection() {
|
|
888
|
+
const startToken = this.current();
|
|
889
|
+
this.expect("KEYWORD", "guards");
|
|
890
|
+
this.expect("COLON");
|
|
891
|
+
this.expectNewline();
|
|
892
|
+
const items = [];
|
|
893
|
+
if (this.check("INDENT")) {
|
|
894
|
+
this.advance(); // consume INDENT
|
|
895
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
896
|
+
this.skipNewlines();
|
|
897
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
898
|
+
break;
|
|
899
|
+
// Each line: id: expression
|
|
900
|
+
const id = this.current().value;
|
|
901
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
902
|
+
this.advance();
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
throw new ParseError(`Expected guard identifier but got ${this.current().type} '${this.current().value}'`, { location: this.current().location, source: this.source });
|
|
906
|
+
}
|
|
907
|
+
this.expect("COLON");
|
|
908
|
+
const check = this.parseExpression();
|
|
909
|
+
let severity = "halt";
|
|
910
|
+
let message;
|
|
911
|
+
let fallback;
|
|
912
|
+
if (this.check("KEYWORD", "with")) {
|
|
913
|
+
const meta = this.parseConstraintClause();
|
|
914
|
+
for (const { key, value } of meta.constraints) {
|
|
915
|
+
if (key === "severity" && value.kind === "literal") {
|
|
916
|
+
severity = String(value.value);
|
|
917
|
+
}
|
|
918
|
+
else if (key === "message" && value.kind === "literal") {
|
|
919
|
+
message = String(value.value);
|
|
920
|
+
}
|
|
921
|
+
else if (key === "fallback" && value.kind === "literal") {
|
|
922
|
+
fallback = Boolean(value.value);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
this.expectNewline();
|
|
927
|
+
items.push({
|
|
928
|
+
id,
|
|
929
|
+
check,
|
|
930
|
+
severity,
|
|
931
|
+
message,
|
|
932
|
+
fallback,
|
|
933
|
+
});
|
|
934
|
+
this.skipNewlines();
|
|
935
|
+
}
|
|
936
|
+
if (this.check("DEDENT"))
|
|
937
|
+
this.advance();
|
|
938
|
+
}
|
|
939
|
+
const node = { kind: "guards", items };
|
|
940
|
+
node.span = this.makeSpan(startToken);
|
|
941
|
+
return node;
|
|
942
|
+
}
|
|
943
|
+
// ===========================================================================
|
|
944
|
+
// TRIGGER PARSING
|
|
945
|
+
// ===========================================================================
|
|
946
|
+
/** Parse trigger handler: on manual: ... */
|
|
947
|
+
parseTriggerHandler() {
|
|
948
|
+
this.expect("KEYWORD", "on");
|
|
949
|
+
const triggerType = this.parseTriggerType();
|
|
950
|
+
this.expect("COLON");
|
|
951
|
+
this.expectNewline();
|
|
952
|
+
const body = this.parseStatementBlock();
|
|
953
|
+
return {
|
|
954
|
+
kind: "trigger_handler",
|
|
955
|
+
trigger: triggerType,
|
|
956
|
+
body,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/** Parse trigger type */
|
|
960
|
+
parseTriggerType() {
|
|
961
|
+
const token = this.current();
|
|
962
|
+
if (token.type === "KEYWORD" || token.type === "IDENTIFIER") {
|
|
963
|
+
switch (token.value) {
|
|
964
|
+
case "manual":
|
|
965
|
+
this.advance();
|
|
966
|
+
return { kind: "manual" };
|
|
967
|
+
case "hourly":
|
|
968
|
+
this.advance();
|
|
969
|
+
return { kind: "hourly" };
|
|
970
|
+
case "daily":
|
|
971
|
+
this.advance();
|
|
972
|
+
return { kind: "daily" };
|
|
973
|
+
case "condition": {
|
|
974
|
+
this.advance();
|
|
975
|
+
const expression = this.parseExpression();
|
|
976
|
+
let pollInterval;
|
|
977
|
+
if (this.check("KEYWORD", "every") ||
|
|
978
|
+
(this.check("IDENTIFIER") && this.current().value === "every")) {
|
|
979
|
+
this.advance();
|
|
980
|
+
const durationToken = this.expect("NUMBER");
|
|
981
|
+
pollInterval = Number.parseFloat(durationToken.value);
|
|
982
|
+
}
|
|
983
|
+
return { kind: "condition", expression, pollInterval };
|
|
984
|
+
}
|
|
985
|
+
case "event": {
|
|
986
|
+
this.advance();
|
|
987
|
+
let eventName = "";
|
|
988
|
+
if (this.check("STRING")) {
|
|
989
|
+
eventName = this.advance().value;
|
|
990
|
+
}
|
|
991
|
+
else if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
992
|
+
eventName = this.advance().value;
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
throw new ParseError("Expected event name", {
|
|
996
|
+
location: this.current().location,
|
|
997
|
+
source: this.source,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
let filter;
|
|
1001
|
+
if (this.check("KEYWORD", "where") ||
|
|
1002
|
+
(this.check("IDENTIFIER") && this.current().value === "where")) {
|
|
1003
|
+
this.advance();
|
|
1004
|
+
filter = this.parseExpression();
|
|
1005
|
+
}
|
|
1006
|
+
return { kind: "event", event: eventName, filter };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
// Could be a cron expression or condition
|
|
1011
|
+
if (token.type === "STRING") {
|
|
1012
|
+
const cron = token.value;
|
|
1013
|
+
this.advance();
|
|
1014
|
+
return { kind: "schedule", cron };
|
|
1015
|
+
}
|
|
1016
|
+
throw new ParseError(`Expected trigger type but got ${token.type} '${token.value}'`, {
|
|
1017
|
+
location: token.location,
|
|
1018
|
+
source: this.source,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
// ===========================================================================
|
|
1022
|
+
// STATEMENT PARSING
|
|
1023
|
+
// ===========================================================================
|
|
1024
|
+
/** Parse an indented block of statements */
|
|
1025
|
+
parseStatementBlock() {
|
|
1026
|
+
const statements = [];
|
|
1027
|
+
if (!this.check("INDENT")) {
|
|
1028
|
+
return statements;
|
|
1029
|
+
}
|
|
1030
|
+
this.advance(); // INDENT
|
|
1031
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
1032
|
+
this.skipNewlines();
|
|
1033
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
1034
|
+
break;
|
|
1035
|
+
statements.push(this.parseStatement());
|
|
1036
|
+
this.skipNewlines();
|
|
1037
|
+
}
|
|
1038
|
+
if (this.check("DEDENT")) {
|
|
1039
|
+
this.advance();
|
|
1040
|
+
}
|
|
1041
|
+
return statements;
|
|
1042
|
+
}
|
|
1043
|
+
/** Parse a single statement */
|
|
1044
|
+
parseStatement() {
|
|
1045
|
+
const token = this.current();
|
|
1046
|
+
// Keywords
|
|
1047
|
+
if (token.type === "KEYWORD") {
|
|
1048
|
+
switch (token.value) {
|
|
1049
|
+
case "if":
|
|
1050
|
+
return this.parseIfStatement();
|
|
1051
|
+
case "for":
|
|
1052
|
+
return this.parseForStatement();
|
|
1053
|
+
case "repeat":
|
|
1054
|
+
return this.parseRepeatStatement();
|
|
1055
|
+
case "loop":
|
|
1056
|
+
return this.parseUntilStatement();
|
|
1057
|
+
case "try":
|
|
1058
|
+
return this.parseTryStatement();
|
|
1059
|
+
case "parallel":
|
|
1060
|
+
return this.parseParallelStatement();
|
|
1061
|
+
case "do":
|
|
1062
|
+
return this.parseDoStatement();
|
|
1063
|
+
case "atomic":
|
|
1064
|
+
return this.parseAtomicStatement();
|
|
1065
|
+
case "emit":
|
|
1066
|
+
return this.parseEmitStatement();
|
|
1067
|
+
case "halt":
|
|
1068
|
+
return this.parseHaltStatement();
|
|
1069
|
+
case "wait":
|
|
1070
|
+
return this.parseWaitStatement();
|
|
1071
|
+
case "pass": {
|
|
1072
|
+
const startToken = this.current();
|
|
1073
|
+
this.advance();
|
|
1074
|
+
this.expectNewline();
|
|
1075
|
+
const node = { kind: "pass" };
|
|
1076
|
+
node.span = this.makeSpan(startToken);
|
|
1077
|
+
return node;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// Assignment or expression statement
|
|
1082
|
+
// Check for: identifier = ...
|
|
1083
|
+
if (token.type === "IDENTIFIER") {
|
|
1084
|
+
// Look ahead for assignment
|
|
1085
|
+
if (this.peek().type === "ASSIGN") {
|
|
1086
|
+
return this.parseAssignment();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
// Expression statement (method call, etc.)
|
|
1090
|
+
const startToken = this.current();
|
|
1091
|
+
const expr = this.parseExpression();
|
|
1092
|
+
// Pipeline expression statement
|
|
1093
|
+
if (this.check("OPERATOR") && this.current().value === "|") {
|
|
1094
|
+
const node = this.parsePipeline(expr, undefined, startToken);
|
|
1095
|
+
return node;
|
|
1096
|
+
}
|
|
1097
|
+
// If it's a method call, convert to statement
|
|
1098
|
+
if (expr.kind === "call") {
|
|
1099
|
+
const callExpr = expr;
|
|
1100
|
+
if (callExpr.callee.kind === "property_access") {
|
|
1101
|
+
const prop = callExpr.callee;
|
|
1102
|
+
const node = {
|
|
1103
|
+
kind: "method_call",
|
|
1104
|
+
object: prop.object,
|
|
1105
|
+
method: prop.property,
|
|
1106
|
+
args: callExpr.args,
|
|
1107
|
+
};
|
|
1108
|
+
// Check for using clause: ... using skill
|
|
1109
|
+
if (this.check("KEYWORD") && this.current().value === "using") {
|
|
1110
|
+
node.skill = this.parseUsingClause();
|
|
1111
|
+
}
|
|
1112
|
+
// Check for constraint clause: ... with key=value
|
|
1113
|
+
if (this.check("KEYWORD") && this.current().value === "with") {
|
|
1114
|
+
node.constraints = this.parseConstraintClause();
|
|
1115
|
+
}
|
|
1116
|
+
this.expectNewline();
|
|
1117
|
+
node.span = this.makeSpan(startToken);
|
|
1118
|
+
return node;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
this.expectNewline();
|
|
1122
|
+
// Treat as assignment with expression
|
|
1123
|
+
throw new ParseError("Unexpected expression statement", {
|
|
1124
|
+
location: token.location,
|
|
1125
|
+
source: this.source,
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
/** Parse assignment: x = expr [with key=value, ...] */
|
|
1129
|
+
parseAssignment() {
|
|
1130
|
+
const startToken = this.current();
|
|
1131
|
+
const target = this.expect("IDENTIFIER").value;
|
|
1132
|
+
this.expect("ASSIGN");
|
|
1133
|
+
// Special case: advise statement
|
|
1134
|
+
if (this.check("KEYWORD", "advise")) {
|
|
1135
|
+
const node = this.parseAdviseStatement(target, startToken);
|
|
1136
|
+
return node;
|
|
1137
|
+
}
|
|
1138
|
+
const value = this.parseExpression();
|
|
1139
|
+
// Pipeline assignment
|
|
1140
|
+
if (this.check("OPERATOR") && this.current().value === "|") {
|
|
1141
|
+
const node = this.parsePipeline(value, target, startToken);
|
|
1142
|
+
return node;
|
|
1143
|
+
}
|
|
1144
|
+
let skill;
|
|
1145
|
+
if (this.check("KEYWORD") && this.current().value === "using") {
|
|
1146
|
+
skill = this.parseUsingClause();
|
|
1147
|
+
}
|
|
1148
|
+
let constraints;
|
|
1149
|
+
if (this.check("KEYWORD") && this.current().value === "with") {
|
|
1150
|
+
constraints = this.parseConstraintClause();
|
|
1151
|
+
}
|
|
1152
|
+
this.expectNewline();
|
|
1153
|
+
const node = { kind: "assignment", target, value, constraints, skill };
|
|
1154
|
+
node.span = this.makeSpan(startToken);
|
|
1155
|
+
return node;
|
|
1156
|
+
}
|
|
1157
|
+
/** Parse advise statement (assignment form) */
|
|
1158
|
+
parseAdviseStatement(target, startToken) {
|
|
1159
|
+
this.expect("KEYWORD", "advise");
|
|
1160
|
+
const advisor = this.expect("IDENTIFIER").value;
|
|
1161
|
+
this.expect("COLON");
|
|
1162
|
+
const prompt = this.expect("STRING").value;
|
|
1163
|
+
this.expectNewline();
|
|
1164
|
+
let outputSchema;
|
|
1165
|
+
let timeout;
|
|
1166
|
+
let fallback;
|
|
1167
|
+
if (this.check("INDENT")) {
|
|
1168
|
+
this.advance();
|
|
1169
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
1170
|
+
this.skipNewlines();
|
|
1171
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
1172
|
+
break;
|
|
1173
|
+
const keyToken = this.current();
|
|
1174
|
+
if (!(this.check("IDENTIFIER") || this.check("KEYWORD"))) {
|
|
1175
|
+
throw new ParseError(`Expected advise field but got ${this.current().type} '${this.current().value}'`, { location: this.current().location, source: this.source });
|
|
1176
|
+
}
|
|
1177
|
+
const key = keyToken.value;
|
|
1178
|
+
this.advance();
|
|
1179
|
+
this.expect("COLON");
|
|
1180
|
+
if (key === "output") {
|
|
1181
|
+
outputSchema = this.parseOutputSchemaBlock(keyToken);
|
|
1182
|
+
}
|
|
1183
|
+
else if (key === "timeout") {
|
|
1184
|
+
const value = this.expect("NUMBER").value;
|
|
1185
|
+
timeout = Number.parseFloat(value);
|
|
1186
|
+
this.expectNewline();
|
|
1187
|
+
}
|
|
1188
|
+
else if (key === "fallback") {
|
|
1189
|
+
fallback = this.parseExpression();
|
|
1190
|
+
this.expectNewline();
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
this.parseExpression();
|
|
1194
|
+
this.expectNewline();
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (this.check("DEDENT"))
|
|
1198
|
+
this.advance();
|
|
1199
|
+
}
|
|
1200
|
+
if (!outputSchema || timeout === undefined || fallback === undefined) {
|
|
1201
|
+
throw new ParseError("Advise statement requires output, timeout, and fallback", {
|
|
1202
|
+
location: startToken.location,
|
|
1203
|
+
source: this.source,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
const node = {
|
|
1207
|
+
kind: "advise",
|
|
1208
|
+
target,
|
|
1209
|
+
advisor,
|
|
1210
|
+
prompt,
|
|
1211
|
+
outputSchema,
|
|
1212
|
+
timeout,
|
|
1213
|
+
fallback,
|
|
1214
|
+
};
|
|
1215
|
+
node.span = this.makeSpan(startToken);
|
|
1216
|
+
return node;
|
|
1217
|
+
}
|
|
1218
|
+
parseOutputSchemaBlock(keyToken) {
|
|
1219
|
+
if (!this.check("NEWLINE")) {
|
|
1220
|
+
// Allow inline type: output: boolean
|
|
1221
|
+
const inlineType = this.parseSchemaType();
|
|
1222
|
+
this.expectNewline();
|
|
1223
|
+
return { kind: "advisory_output_schema", type: inlineType };
|
|
1224
|
+
}
|
|
1225
|
+
this.expectNewline();
|
|
1226
|
+
if (!this.check("INDENT")) {
|
|
1227
|
+
throw new ParseError("Expected indented output block", {
|
|
1228
|
+
location: keyToken.location,
|
|
1229
|
+
source: this.source,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
this.advance();
|
|
1233
|
+
return this.parseSchemaObject(keyToken);
|
|
1234
|
+
}
|
|
1235
|
+
parseSchemaAfterColon(keyToken) {
|
|
1236
|
+
if (this.check("NEWLINE")) {
|
|
1237
|
+
this.expectNewline();
|
|
1238
|
+
if (!this.check("INDENT")) {
|
|
1239
|
+
throw new ParseError("Expected indented schema block", {
|
|
1240
|
+
location: keyToken.location,
|
|
1241
|
+
source: this.source,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
this.advance();
|
|
1245
|
+
return this.parseSchemaObject(keyToken);
|
|
1246
|
+
}
|
|
1247
|
+
const inlineType = this.parseSchemaType();
|
|
1248
|
+
this.expectNewline();
|
|
1249
|
+
return { kind: "advisory_output_schema", type: inlineType };
|
|
1250
|
+
}
|
|
1251
|
+
parseSchemaObject(keyToken) {
|
|
1252
|
+
let outType;
|
|
1253
|
+
let values;
|
|
1254
|
+
let min;
|
|
1255
|
+
let max;
|
|
1256
|
+
let minLength;
|
|
1257
|
+
let maxLength;
|
|
1258
|
+
let pattern;
|
|
1259
|
+
let fields;
|
|
1260
|
+
let items;
|
|
1261
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
1262
|
+
this.skipNewlines();
|
|
1263
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
1264
|
+
break;
|
|
1265
|
+
if (!(this.check("IDENTIFIER") || this.check("KEYWORD"))) {
|
|
1266
|
+
throw new ParseError(`Expected output schema field but got ${this.current().type} '${this.current().value}'`, { location: this.current().location, source: this.source });
|
|
1267
|
+
}
|
|
1268
|
+
const outKey = this.current().value;
|
|
1269
|
+
this.advance();
|
|
1270
|
+
this.expect("COLON");
|
|
1271
|
+
if (outKey === "type") {
|
|
1272
|
+
outType = this.parseSchemaType();
|
|
1273
|
+
this.expectNewline();
|
|
1274
|
+
}
|
|
1275
|
+
else if (outKey === "values") {
|
|
1276
|
+
values = this.parseStringList();
|
|
1277
|
+
this.expectNewline();
|
|
1278
|
+
}
|
|
1279
|
+
else if (outKey === "min") {
|
|
1280
|
+
const val = this.expect("NUMBER").value;
|
|
1281
|
+
min = Number.parseFloat(val);
|
|
1282
|
+
this.expectNewline();
|
|
1283
|
+
}
|
|
1284
|
+
else if (outKey === "max") {
|
|
1285
|
+
const val = this.expect("NUMBER").value;
|
|
1286
|
+
max = Number.parseFloat(val);
|
|
1287
|
+
this.expectNewline();
|
|
1288
|
+
}
|
|
1289
|
+
else if (outKey === "min_length") {
|
|
1290
|
+
const val = this.expect("NUMBER").value;
|
|
1291
|
+
minLength = Number.parseFloat(val);
|
|
1292
|
+
this.expectNewline();
|
|
1293
|
+
}
|
|
1294
|
+
else if (outKey === "max_length") {
|
|
1295
|
+
const val = this.expect("NUMBER").value;
|
|
1296
|
+
maxLength = Number.parseFloat(val);
|
|
1297
|
+
this.expectNewline();
|
|
1298
|
+
}
|
|
1299
|
+
else if (outKey === "pattern") {
|
|
1300
|
+
pattern = this.expect("STRING").value;
|
|
1301
|
+
this.expectNewline();
|
|
1302
|
+
}
|
|
1303
|
+
else if (outKey === "fields") {
|
|
1304
|
+
fields = this.parseSchemaFields(keyToken);
|
|
1305
|
+
}
|
|
1306
|
+
else if (outKey === "items") {
|
|
1307
|
+
items = this.parseSchemaAfterColon(keyToken);
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
this.parseExpression();
|
|
1311
|
+
this.expectNewline();
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (this.check("DEDENT"))
|
|
1315
|
+
this.advance();
|
|
1316
|
+
if (!outType) {
|
|
1317
|
+
throw new ParseError("Advisory output type is required", {
|
|
1318
|
+
location: keyToken.location,
|
|
1319
|
+
source: this.source,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
kind: "advisory_output_schema",
|
|
1324
|
+
type: outType,
|
|
1325
|
+
values,
|
|
1326
|
+
min,
|
|
1327
|
+
max,
|
|
1328
|
+
minLength,
|
|
1329
|
+
maxLength,
|
|
1330
|
+
pattern,
|
|
1331
|
+
fields,
|
|
1332
|
+
items,
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
parseSchemaFields(keyToken) {
|
|
1336
|
+
this.expectNewline();
|
|
1337
|
+
if (!this.check("INDENT")) {
|
|
1338
|
+
throw new ParseError("Expected indented fields block", {
|
|
1339
|
+
location: keyToken.location,
|
|
1340
|
+
source: this.source,
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
this.advance();
|
|
1344
|
+
const fields = {};
|
|
1345
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
1346
|
+
this.skipNewlines();
|
|
1347
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
1348
|
+
break;
|
|
1349
|
+
const fieldName = this.expect("IDENTIFIER").value;
|
|
1350
|
+
this.expect("COLON");
|
|
1351
|
+
fields[fieldName] = this.parseSchemaAfterColon(keyToken);
|
|
1352
|
+
}
|
|
1353
|
+
if (this.check("DEDENT"))
|
|
1354
|
+
this.advance();
|
|
1355
|
+
return fields;
|
|
1356
|
+
}
|
|
1357
|
+
parseSchemaType() {
|
|
1358
|
+
if (this.check("STRING")) {
|
|
1359
|
+
return this.advance().value;
|
|
1360
|
+
}
|
|
1361
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1362
|
+
return this.advance().value;
|
|
1363
|
+
}
|
|
1364
|
+
throw new ParseError("Expected output schema type", {
|
|
1365
|
+
location: this.current().location,
|
|
1366
|
+
source: this.source,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
/** Parse using clause: using skill_name */
|
|
1370
|
+
parseUsingClause() {
|
|
1371
|
+
this.expect("KEYWORD", "using");
|
|
1372
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1373
|
+
return this.advance().value;
|
|
1374
|
+
}
|
|
1375
|
+
if (this.check("STRING")) {
|
|
1376
|
+
return this.advance().value;
|
|
1377
|
+
}
|
|
1378
|
+
throw new ParseError("Expected skill name after 'using'", {
|
|
1379
|
+
location: this.current().location,
|
|
1380
|
+
source: this.source,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
/** Parse repeat statement */
|
|
1384
|
+
parseRepeatStatement() {
|
|
1385
|
+
const startToken = this.current();
|
|
1386
|
+
this.expect("KEYWORD", "repeat");
|
|
1387
|
+
const countToken = this.expect("NUMBER");
|
|
1388
|
+
const count = {
|
|
1389
|
+
kind: "literal",
|
|
1390
|
+
value: countToken.value.includes(".")
|
|
1391
|
+
? Number.parseFloat(countToken.value)
|
|
1392
|
+
: Number.parseInt(countToken.value, 10),
|
|
1393
|
+
literalType: "number",
|
|
1394
|
+
};
|
|
1395
|
+
this.expect("COLON");
|
|
1396
|
+
this.expectNewline();
|
|
1397
|
+
const body = this.parseStatementBlock();
|
|
1398
|
+
const node = { kind: "repeat", count, body };
|
|
1399
|
+
node.span = this.makeSpan(startToken);
|
|
1400
|
+
return node;
|
|
1401
|
+
}
|
|
1402
|
+
/** Parse loop-until statement */
|
|
1403
|
+
parseUntilStatement() {
|
|
1404
|
+
const startToken = this.current();
|
|
1405
|
+
this.expect("KEYWORD", "loop");
|
|
1406
|
+
this.expect("KEYWORD", "until");
|
|
1407
|
+
const condition = this.parseExpression();
|
|
1408
|
+
let maxIterations;
|
|
1409
|
+
if (this.check("KEYWORD", "max")) {
|
|
1410
|
+
this.advance();
|
|
1411
|
+
const maxToken = this.expect("NUMBER");
|
|
1412
|
+
maxIterations = Number.parseFloat(maxToken.value);
|
|
1413
|
+
}
|
|
1414
|
+
this.expect("COLON");
|
|
1415
|
+
this.expectNewline();
|
|
1416
|
+
const body = this.parseStatementBlock();
|
|
1417
|
+
const node = { kind: "until", condition, maxIterations, body };
|
|
1418
|
+
node.span = this.makeSpan(startToken);
|
|
1419
|
+
return node;
|
|
1420
|
+
}
|
|
1421
|
+
/** Parse try/catch/finally */
|
|
1422
|
+
parseTryStatement() {
|
|
1423
|
+
const startToken = this.current();
|
|
1424
|
+
this.expect("KEYWORD", "try");
|
|
1425
|
+
this.expect("COLON");
|
|
1426
|
+
this.expectNewline();
|
|
1427
|
+
const tryBody = this.parseStatementBlock();
|
|
1428
|
+
const catches = [];
|
|
1429
|
+
while (this.check("KEYWORD", "catch")) {
|
|
1430
|
+
catches.push(this.parseCatchBlock());
|
|
1431
|
+
}
|
|
1432
|
+
let finallyBody;
|
|
1433
|
+
if (this.check("KEYWORD", "finally")) {
|
|
1434
|
+
this.advance();
|
|
1435
|
+
this.expect("COLON");
|
|
1436
|
+
this.expectNewline();
|
|
1437
|
+
finallyBody = this.parseStatementBlock();
|
|
1438
|
+
}
|
|
1439
|
+
const node = { kind: "try", tryBody, catches, finallyBody };
|
|
1440
|
+
node.span = this.makeSpan(startToken);
|
|
1441
|
+
return node;
|
|
1442
|
+
}
|
|
1443
|
+
/** Parse catch block */
|
|
1444
|
+
parseCatchBlock() {
|
|
1445
|
+
const startToken = this.current();
|
|
1446
|
+
this.expect("KEYWORD", "catch");
|
|
1447
|
+
let error = "*";
|
|
1448
|
+
if (this.check("OPERATOR") && this.current().value === "*") {
|
|
1449
|
+
this.advance();
|
|
1450
|
+
}
|
|
1451
|
+
else if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1452
|
+
error = this.advance().value;
|
|
1453
|
+
}
|
|
1454
|
+
this.expect("COLON");
|
|
1455
|
+
this.expectNewline();
|
|
1456
|
+
const body = [];
|
|
1457
|
+
let action;
|
|
1458
|
+
let retry;
|
|
1459
|
+
if (this.check("INDENT")) {
|
|
1460
|
+
this.advance();
|
|
1461
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
1462
|
+
this.skipNewlines();
|
|
1463
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
1464
|
+
break;
|
|
1465
|
+
if ((this.check("IDENTIFIER") || this.check("KEYWORD")) &&
|
|
1466
|
+
this.current().value === "action") {
|
|
1467
|
+
this.advance();
|
|
1468
|
+
this.expect("COLON");
|
|
1469
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1470
|
+
action = this.advance().value;
|
|
1471
|
+
}
|
|
1472
|
+
else if (this.check("STRING")) {
|
|
1473
|
+
action = this.advance().value;
|
|
1474
|
+
}
|
|
1475
|
+
this.expectNewline();
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
if ((this.check("IDENTIFIER") || this.check("KEYWORD")) &&
|
|
1479
|
+
this.current().value === "retry") {
|
|
1480
|
+
this.advance();
|
|
1481
|
+
this.expect("COLON");
|
|
1482
|
+
this.expectNewline();
|
|
1483
|
+
retry = this.parseRetrySpec();
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
body.push(this.parseStatement());
|
|
1487
|
+
this.skipNewlines();
|
|
1488
|
+
}
|
|
1489
|
+
if (this.check("DEDENT"))
|
|
1490
|
+
this.advance();
|
|
1491
|
+
}
|
|
1492
|
+
const node = { kind: "catch", error, action, retry, body };
|
|
1493
|
+
node.span = this.makeSpan(startToken);
|
|
1494
|
+
return node;
|
|
1495
|
+
}
|
|
1496
|
+
/** Parse retry spec block */
|
|
1497
|
+
parseRetrySpec() {
|
|
1498
|
+
const startToken = this.current();
|
|
1499
|
+
let maxAttempts = 3;
|
|
1500
|
+
let backoff = "none";
|
|
1501
|
+
let backoffBase;
|
|
1502
|
+
let maxBackoff;
|
|
1503
|
+
if (this.check("INDENT")) {
|
|
1504
|
+
this.advance();
|
|
1505
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
1506
|
+
this.skipNewlines();
|
|
1507
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
1508
|
+
break;
|
|
1509
|
+
const key = this.expect("IDENTIFIER").value;
|
|
1510
|
+
this.expect("COLON");
|
|
1511
|
+
if (key === "max_attempts") {
|
|
1512
|
+
maxAttempts = Number.parseFloat(this.expect("NUMBER").value);
|
|
1513
|
+
}
|
|
1514
|
+
else if (key === "backoff") {
|
|
1515
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1516
|
+
backoff = this.advance().value;
|
|
1517
|
+
}
|
|
1518
|
+
else if (this.check("STRING")) {
|
|
1519
|
+
backoff = this.advance().value;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
else if (key === "backoff_base") {
|
|
1523
|
+
backoffBase = Number.parseFloat(this.expect("NUMBER").value);
|
|
1524
|
+
}
|
|
1525
|
+
else if (key === "max_backoff") {
|
|
1526
|
+
maxBackoff = Number.parseFloat(this.expect("NUMBER").value);
|
|
1527
|
+
}
|
|
1528
|
+
else {
|
|
1529
|
+
this.parseExpression();
|
|
1530
|
+
}
|
|
1531
|
+
this.expectNewline();
|
|
1532
|
+
}
|
|
1533
|
+
if (this.check("DEDENT"))
|
|
1534
|
+
this.advance();
|
|
1535
|
+
}
|
|
1536
|
+
const node = {
|
|
1537
|
+
kind: "retry",
|
|
1538
|
+
maxAttempts,
|
|
1539
|
+
backoff,
|
|
1540
|
+
backoffBase,
|
|
1541
|
+
maxBackoff,
|
|
1542
|
+
};
|
|
1543
|
+
node.span = this.makeSpan(startToken);
|
|
1544
|
+
return node;
|
|
1545
|
+
}
|
|
1546
|
+
/** Parse parallel statement */
|
|
1547
|
+
parseParallelStatement() {
|
|
1548
|
+
const startToken = this.current();
|
|
1549
|
+
this.expect("KEYWORD", "parallel");
|
|
1550
|
+
let join;
|
|
1551
|
+
let onFail;
|
|
1552
|
+
// Optional header config before colon: join=..., on_fail=...
|
|
1553
|
+
while (!this.check("COLON") && !this.check("NEWLINE") && !this.check("EOF")) {
|
|
1554
|
+
if (!(this.check("IDENTIFIER") || this.check("KEYWORD")))
|
|
1555
|
+
break;
|
|
1556
|
+
const key = this.advance().value;
|
|
1557
|
+
if (!this.check("ASSIGN"))
|
|
1558
|
+
break;
|
|
1559
|
+
this.advance();
|
|
1560
|
+
if (key === "join") {
|
|
1561
|
+
let joinType = "all";
|
|
1562
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1563
|
+
joinType = this.advance().value;
|
|
1564
|
+
}
|
|
1565
|
+
else if (this.check("STRING")) {
|
|
1566
|
+
joinType = this.advance().value;
|
|
1567
|
+
}
|
|
1568
|
+
join = {
|
|
1569
|
+
kind: "parallel_join",
|
|
1570
|
+
type: joinType,
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
else if (key === "on_fail") {
|
|
1574
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1575
|
+
onFail = this.advance().value;
|
|
1576
|
+
}
|
|
1577
|
+
else if (this.check("STRING")) {
|
|
1578
|
+
onFail = this.advance().value;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
else if (key === "metric") {
|
|
1582
|
+
if (!join) {
|
|
1583
|
+
join = { kind: "parallel_join", type: "best" };
|
|
1584
|
+
}
|
|
1585
|
+
join.metric = this.parseExpression();
|
|
1586
|
+
}
|
|
1587
|
+
else if (key === "order") {
|
|
1588
|
+
if (!join) {
|
|
1589
|
+
join = { kind: "parallel_join", type: "best" };
|
|
1590
|
+
}
|
|
1591
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1592
|
+
join.order = this.advance().value;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
else if (key === "count") {
|
|
1596
|
+
if (!join) {
|
|
1597
|
+
join = { kind: "parallel_join", type: "any" };
|
|
1598
|
+
}
|
|
1599
|
+
if (this.check("NUMBER")) {
|
|
1600
|
+
join.count = Number.parseFloat(this.advance().value);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
else {
|
|
1604
|
+
// Unknown header option, skip value
|
|
1605
|
+
this.parseExpression();
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
this.expect("COLON");
|
|
1609
|
+
this.expectNewline();
|
|
1610
|
+
const branches = [];
|
|
1611
|
+
if (this.check("INDENT")) {
|
|
1612
|
+
this.advance();
|
|
1613
|
+
while (!this.check("DEDENT") && !this.check("EOF")) {
|
|
1614
|
+
this.skipNewlines();
|
|
1615
|
+
if (this.check("DEDENT") || this.check("EOF"))
|
|
1616
|
+
break;
|
|
1617
|
+
const name = this.expect("IDENTIFIER").value;
|
|
1618
|
+
this.expect("COLON");
|
|
1619
|
+
this.expectNewline();
|
|
1620
|
+
const body = this.parseStatementBlock();
|
|
1621
|
+
branches.push({ kind: "parallel_branch", name, body });
|
|
1622
|
+
this.skipNewlines();
|
|
1623
|
+
}
|
|
1624
|
+
if (this.check("DEDENT"))
|
|
1625
|
+
this.advance();
|
|
1626
|
+
}
|
|
1627
|
+
const node = { kind: "parallel", join, onFail, branches };
|
|
1628
|
+
node.span = this.makeSpan(startToken);
|
|
1629
|
+
return node;
|
|
1630
|
+
}
|
|
1631
|
+
/** Parse pipeline after source expression */
|
|
1632
|
+
parsePipeline(source, outputBinding, startToken) {
|
|
1633
|
+
const stages = [];
|
|
1634
|
+
while (this.check("OPERATOR") && this.current().value === "|") {
|
|
1635
|
+
this.advance(); // consume |
|
|
1636
|
+
const opToken = this.expect("IDENTIFIER");
|
|
1637
|
+
const op = opToken.value;
|
|
1638
|
+
let initial;
|
|
1639
|
+
let count;
|
|
1640
|
+
let order;
|
|
1641
|
+
let by;
|
|
1642
|
+
// Optional args
|
|
1643
|
+
if (op === "reduce" && this.check("LPAREN")) {
|
|
1644
|
+
this.advance();
|
|
1645
|
+
initial = this.parseExpression();
|
|
1646
|
+
this.expect("RPAREN");
|
|
1647
|
+
}
|
|
1648
|
+
else if ((op === "take" || op === "skip") && this.check("NUMBER")) {
|
|
1649
|
+
count = Number.parseFloat(this.advance().value);
|
|
1650
|
+
}
|
|
1651
|
+
else if (op === "sort") {
|
|
1652
|
+
if (this.check("IDENTIFIER") && this.current().value === "by") {
|
|
1653
|
+
this.advance();
|
|
1654
|
+
by = this.parseExpression();
|
|
1655
|
+
}
|
|
1656
|
+
if (this.check("IDENTIFIER") && this.current().value === "order") {
|
|
1657
|
+
this.advance();
|
|
1658
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1659
|
+
order = this.advance().value;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
this.expect("COLON");
|
|
1664
|
+
this.expectNewline();
|
|
1665
|
+
const body = this.parseStatementBlock();
|
|
1666
|
+
if (body.length === 0 &&
|
|
1667
|
+
(op === "map" || op === "filter" || op === "reduce" || op === "pmap")) {
|
|
1668
|
+
throw new ParseError(`Pipeline stage '${op}' requires a body`, {
|
|
1669
|
+
location: opToken.location,
|
|
1670
|
+
source: this.source,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const stageStep = (body[0] ?? { kind: "pass" });
|
|
1674
|
+
stages.push({
|
|
1675
|
+
kind: "pipeline_stage",
|
|
1676
|
+
op,
|
|
1677
|
+
step: stageStep,
|
|
1678
|
+
initial,
|
|
1679
|
+
count,
|
|
1680
|
+
order,
|
|
1681
|
+
by,
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
const node = { kind: "pipeline", source, stages, outputBinding };
|
|
1685
|
+
node.span = this.makeSpan(startToken);
|
|
1686
|
+
return node;
|
|
1687
|
+
}
|
|
1688
|
+
/** Parse do statement (block invocation) */
|
|
1689
|
+
parseDoStatement() {
|
|
1690
|
+
const startToken = this.current();
|
|
1691
|
+
this.expect("KEYWORD", "do");
|
|
1692
|
+
const nameParts = [];
|
|
1693
|
+
nameParts.push(this.expect("IDENTIFIER").value);
|
|
1694
|
+
while (this.check("DOT")) {
|
|
1695
|
+
this.advance();
|
|
1696
|
+
nameParts.push(this.expect("IDENTIFIER").value);
|
|
1697
|
+
}
|
|
1698
|
+
const name = nameParts.join(".");
|
|
1699
|
+
const args = [];
|
|
1700
|
+
if (this.check("LPAREN")) {
|
|
1701
|
+
this.advance();
|
|
1702
|
+
while (!this.check("RPAREN")) {
|
|
1703
|
+
args.push(this.parseExpression());
|
|
1704
|
+
if (this.check("COMMA"))
|
|
1705
|
+
this.advance();
|
|
1706
|
+
}
|
|
1707
|
+
this.expect("RPAREN");
|
|
1708
|
+
}
|
|
1709
|
+
this.expectNewline();
|
|
1710
|
+
const node = { kind: "do", name, args };
|
|
1711
|
+
node.span = this.makeSpan(startToken);
|
|
1712
|
+
return node;
|
|
1713
|
+
}
|
|
1714
|
+
/** Parse if statement */
|
|
1715
|
+
parseIfStatement() {
|
|
1716
|
+
const startToken = this.current();
|
|
1717
|
+
this.expect("KEYWORD", "if");
|
|
1718
|
+
// Check for advisory condition: if **prompt**:
|
|
1719
|
+
let condition;
|
|
1720
|
+
if (this.check("ADVISORY")) {
|
|
1721
|
+
const prompt = this.advance().value;
|
|
1722
|
+
let advisor;
|
|
1723
|
+
if (this.check("KEYWORD", "via")) {
|
|
1724
|
+
this.advance();
|
|
1725
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1726
|
+
advisor = this.advance().value;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
condition = { kind: "advisory_expr", prompt, advisor };
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
condition = this.parseExpression();
|
|
1733
|
+
}
|
|
1734
|
+
this.expect("COLON");
|
|
1735
|
+
this.expectNewline();
|
|
1736
|
+
const thenBody = this.parseStatementBlock();
|
|
1737
|
+
const elifs = [];
|
|
1738
|
+
let elseBody = [];
|
|
1739
|
+
// Check for elif
|
|
1740
|
+
while (this.check("KEYWORD", "elif")) {
|
|
1741
|
+
this.advance();
|
|
1742
|
+
const elifCondition = this.parseExpression();
|
|
1743
|
+
this.expect("COLON");
|
|
1744
|
+
this.expectNewline();
|
|
1745
|
+
const elifBody = this.parseStatementBlock();
|
|
1746
|
+
elifs.push({ condition: elifCondition, body: elifBody });
|
|
1747
|
+
}
|
|
1748
|
+
// Check for else
|
|
1749
|
+
if (this.check("KEYWORD", "else")) {
|
|
1750
|
+
this.advance();
|
|
1751
|
+
this.expect("COLON");
|
|
1752
|
+
this.expectNewline();
|
|
1753
|
+
elseBody = this.parseStatementBlock();
|
|
1754
|
+
}
|
|
1755
|
+
const node = { kind: "if", condition, thenBody, elifs, elseBody };
|
|
1756
|
+
node.span = this.makeSpan(startToken);
|
|
1757
|
+
return node;
|
|
1758
|
+
}
|
|
1759
|
+
/** Parse for loop */
|
|
1760
|
+
parseForStatement() {
|
|
1761
|
+
const startToken = this.current();
|
|
1762
|
+
this.expect("KEYWORD", "for");
|
|
1763
|
+
const variable = this.expect("IDENTIFIER").value;
|
|
1764
|
+
this.expect("KEYWORD", "in");
|
|
1765
|
+
const iterable = this.parseExpression();
|
|
1766
|
+
this.expect("COLON");
|
|
1767
|
+
this.expectNewline();
|
|
1768
|
+
const body = this.parseStatementBlock();
|
|
1769
|
+
const node = { kind: "for", variable, iterable, body };
|
|
1770
|
+
node.span = this.makeSpan(startToken);
|
|
1771
|
+
return node;
|
|
1772
|
+
}
|
|
1773
|
+
/** Parse atomic block: atomic: / atomic skip: / atomic halt: / atomic revert: */
|
|
1774
|
+
parseAtomicStatement() {
|
|
1775
|
+
const startToken = this.current();
|
|
1776
|
+
this.expect("KEYWORD", "atomic");
|
|
1777
|
+
let onFailure;
|
|
1778
|
+
// Check for failure mode: atomic skip: / atomic halt: / atomic revert:
|
|
1779
|
+
// Note: "halt" is a keyword, "skip" and "revert" are identifiers
|
|
1780
|
+
const failureModes = ["skip", "halt", "revert"];
|
|
1781
|
+
if ((this.check("IDENTIFIER") || this.check("KEYWORD")) &&
|
|
1782
|
+
failureModes.includes(this.current().value)) {
|
|
1783
|
+
onFailure = this.current().value;
|
|
1784
|
+
this.advance();
|
|
1785
|
+
}
|
|
1786
|
+
this.expect("COLON");
|
|
1787
|
+
this.expectNewline();
|
|
1788
|
+
const body = this.parseStatementBlock();
|
|
1789
|
+
const node = { kind: "atomic", body, onFailure };
|
|
1790
|
+
node.span = this.makeSpan(startToken);
|
|
1791
|
+
return node;
|
|
1792
|
+
}
|
|
1793
|
+
/** Parse emit statement: emit event_name(key=value) */
|
|
1794
|
+
parseEmitStatement() {
|
|
1795
|
+
const startToken = this.current();
|
|
1796
|
+
this.expect("KEYWORD", "emit");
|
|
1797
|
+
const event = this.expect("IDENTIFIER").value;
|
|
1798
|
+
const data = [];
|
|
1799
|
+
if (this.check("LPAREN")) {
|
|
1800
|
+
this.advance();
|
|
1801
|
+
while (!this.check("RPAREN")) {
|
|
1802
|
+
let key;
|
|
1803
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
1804
|
+
key = this.advance().value;
|
|
1805
|
+
}
|
|
1806
|
+
else {
|
|
1807
|
+
throw new ParseError(`Expected identifier in emit but got ${this.current().type} '${this.current().value}'`, { location: this.current().location, source: this.source });
|
|
1808
|
+
}
|
|
1809
|
+
this.expect("ASSIGN");
|
|
1810
|
+
const value = this.parseExpression();
|
|
1811
|
+
data.push({ key, value });
|
|
1812
|
+
if (this.check("COMMA")) {
|
|
1813
|
+
this.advance();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
this.expect("RPAREN");
|
|
1817
|
+
}
|
|
1818
|
+
this.expectNewline();
|
|
1819
|
+
const node = { kind: "emit", event, data };
|
|
1820
|
+
node.span = this.makeSpan(startToken);
|
|
1821
|
+
return node;
|
|
1822
|
+
}
|
|
1823
|
+
/** Parse halt statement */
|
|
1824
|
+
parseHaltStatement() {
|
|
1825
|
+
const startToken = this.current();
|
|
1826
|
+
this.expect("KEYWORD", "halt");
|
|
1827
|
+
let reason = "halted";
|
|
1828
|
+
if (this.check("STRING")) {
|
|
1829
|
+
reason = this.advance().value;
|
|
1830
|
+
}
|
|
1831
|
+
this.expectNewline();
|
|
1832
|
+
const node = { kind: "halt", reason };
|
|
1833
|
+
node.span = this.makeSpan(startToken);
|
|
1834
|
+
return node;
|
|
1835
|
+
}
|
|
1836
|
+
/** Parse wait statement */
|
|
1837
|
+
parseWaitStatement() {
|
|
1838
|
+
const startToken = this.current();
|
|
1839
|
+
this.expect("KEYWORD", "wait");
|
|
1840
|
+
const durationToken = this.expect("NUMBER");
|
|
1841
|
+
const duration = Number.parseFloat(durationToken.value);
|
|
1842
|
+
this.expectNewline();
|
|
1843
|
+
const node = { kind: "wait", duration };
|
|
1844
|
+
node.span = this.makeSpan(startToken);
|
|
1845
|
+
return node;
|
|
1846
|
+
}
|
|
1847
|
+
/** Parse constraint clause: with key=value, key=value, ... */
|
|
1848
|
+
parseConstraintClause() {
|
|
1849
|
+
const startToken = this.current();
|
|
1850
|
+
this.expect("KEYWORD", "with");
|
|
1851
|
+
const constraints = [];
|
|
1852
|
+
do {
|
|
1853
|
+
const key = this.expect("IDENTIFIER").value;
|
|
1854
|
+
this.expect("ASSIGN");
|
|
1855
|
+
const value = this.parseExpression();
|
|
1856
|
+
constraints.push({ key, value });
|
|
1857
|
+
} while (this.check("COMMA") &&
|
|
1858
|
+
(() => {
|
|
1859
|
+
this.advance();
|
|
1860
|
+
return true;
|
|
1861
|
+
})());
|
|
1862
|
+
const node = { kind: "constraint_clause", constraints };
|
|
1863
|
+
node.span = this.makeSpan(startToken);
|
|
1864
|
+
return node;
|
|
1865
|
+
}
|
|
1866
|
+
// ===========================================================================
|
|
1867
|
+
// EXPRESSION PARSING
|
|
1868
|
+
// ===========================================================================
|
|
1869
|
+
/** Parse an expression */
|
|
1870
|
+
parseExpression() {
|
|
1871
|
+
return this.parseTernary();
|
|
1872
|
+
}
|
|
1873
|
+
/** Parse ternary: cond ? then : else */
|
|
1874
|
+
parseTernary() {
|
|
1875
|
+
const condition = this.parseOr();
|
|
1876
|
+
if (this.check("QUESTION")) {
|
|
1877
|
+
this.advance();
|
|
1878
|
+
const thenExpr = this.parseTernary();
|
|
1879
|
+
this.expect("COLON");
|
|
1880
|
+
const elseExpr = this.parseTernary();
|
|
1881
|
+
return {
|
|
1882
|
+
kind: "ternary",
|
|
1883
|
+
condition,
|
|
1884
|
+
thenExpr,
|
|
1885
|
+
elseExpr,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
return condition;
|
|
1889
|
+
}
|
|
1890
|
+
/** Parse or: a or b */
|
|
1891
|
+
parseOr() {
|
|
1892
|
+
let left = this.parseAnd();
|
|
1893
|
+
while (this.check("KEYWORD", "or")) {
|
|
1894
|
+
this.advance();
|
|
1895
|
+
const right = this.parseAnd();
|
|
1896
|
+
left = { kind: "binary", op: "or", left, right };
|
|
1897
|
+
}
|
|
1898
|
+
return left;
|
|
1899
|
+
}
|
|
1900
|
+
/** Parse and: a and b */
|
|
1901
|
+
parseAnd() {
|
|
1902
|
+
let left = this.parseEquality();
|
|
1903
|
+
while (this.check("KEYWORD", "and")) {
|
|
1904
|
+
this.advance();
|
|
1905
|
+
const right = this.parseEquality();
|
|
1906
|
+
left = { kind: "binary", op: "and", left, right };
|
|
1907
|
+
}
|
|
1908
|
+
return left;
|
|
1909
|
+
}
|
|
1910
|
+
/** Parse equality: == != */
|
|
1911
|
+
parseEquality() {
|
|
1912
|
+
let left = this.parseComparison();
|
|
1913
|
+
while (this.check("OPERATOR") &&
|
|
1914
|
+
(this.current().value === "==" || this.current().value === "!=")) {
|
|
1915
|
+
const op = this.advance().value;
|
|
1916
|
+
const right = this.parseComparison();
|
|
1917
|
+
left = { kind: "binary", op, left, right };
|
|
1918
|
+
}
|
|
1919
|
+
return left;
|
|
1920
|
+
}
|
|
1921
|
+
/** Parse comparison: < > <= >= */
|
|
1922
|
+
parseComparison() {
|
|
1923
|
+
let left = this.parseAdditive();
|
|
1924
|
+
while (this.check("OPERATOR") && ["<", ">", "<=", ">="].includes(this.current().value)) {
|
|
1925
|
+
const op = this.advance().value;
|
|
1926
|
+
const right = this.parseAdditive();
|
|
1927
|
+
left = { kind: "binary", op, left, right };
|
|
1928
|
+
}
|
|
1929
|
+
return left;
|
|
1930
|
+
}
|
|
1931
|
+
/** Parse additive: + - */
|
|
1932
|
+
parseAdditive() {
|
|
1933
|
+
let left = this.parseMultiplicative();
|
|
1934
|
+
while (this.check("OPERATOR") &&
|
|
1935
|
+
(this.current().value === "+" || this.current().value === "-")) {
|
|
1936
|
+
const op = this.advance().value;
|
|
1937
|
+
const right = this.parseMultiplicative();
|
|
1938
|
+
left = { kind: "binary", op, left, right };
|
|
1939
|
+
}
|
|
1940
|
+
return left;
|
|
1941
|
+
}
|
|
1942
|
+
/** Parse multiplicative: * / % */
|
|
1943
|
+
parseMultiplicative() {
|
|
1944
|
+
let left = this.parseUnary();
|
|
1945
|
+
while (this.check("OPERATOR") && ["*", "/", "%"].includes(this.current().value)) {
|
|
1946
|
+
const op = this.advance().value;
|
|
1947
|
+
const right = this.parseUnary();
|
|
1948
|
+
left = { kind: "binary", op, left, right };
|
|
1949
|
+
}
|
|
1950
|
+
return left;
|
|
1951
|
+
}
|
|
1952
|
+
/** Parse unary: not - */
|
|
1953
|
+
parseUnary() {
|
|
1954
|
+
if (this.check("KEYWORD", "not")) {
|
|
1955
|
+
this.advance();
|
|
1956
|
+
const arg = this.parseUnary();
|
|
1957
|
+
return { kind: "unary", op: "not", arg };
|
|
1958
|
+
}
|
|
1959
|
+
if (this.check("OPERATOR", "-")) {
|
|
1960
|
+
this.advance();
|
|
1961
|
+
const arg = this.parseUnary();
|
|
1962
|
+
return { kind: "unary", op: "-", arg };
|
|
1963
|
+
}
|
|
1964
|
+
return this.parsePostfix();
|
|
1965
|
+
}
|
|
1966
|
+
/** Parse postfix: . [] () */
|
|
1967
|
+
parsePostfix() {
|
|
1968
|
+
let expr = this.parsePrimary();
|
|
1969
|
+
while (true) {
|
|
1970
|
+
if (this.check("DOT")) {
|
|
1971
|
+
this.advance();
|
|
1972
|
+
const property = this.expect("IDENTIFIER").value;
|
|
1973
|
+
expr = { kind: "property_access", object: expr, property };
|
|
1974
|
+
}
|
|
1975
|
+
else if (this.check("LBRACKET")) {
|
|
1976
|
+
this.advance();
|
|
1977
|
+
const index = this.parseExpression();
|
|
1978
|
+
this.expect("RBRACKET");
|
|
1979
|
+
expr = { kind: "array_access", array: expr, index };
|
|
1980
|
+
}
|
|
1981
|
+
else if (this.check("LPAREN")) {
|
|
1982
|
+
this.advance();
|
|
1983
|
+
const args = [];
|
|
1984
|
+
const kwargs = [];
|
|
1985
|
+
while (!this.check("RPAREN")) {
|
|
1986
|
+
// Check for kwarg: key=value
|
|
1987
|
+
if (this.check("IDENTIFIER") && this.peek().type === "ASSIGN") {
|
|
1988
|
+
const key = this.advance().value;
|
|
1989
|
+
this.expect("ASSIGN");
|
|
1990
|
+
const value = this.parseExpression();
|
|
1991
|
+
kwargs.push({ key, value });
|
|
1992
|
+
}
|
|
1993
|
+
else {
|
|
1994
|
+
args.push(this.parseExpression());
|
|
1995
|
+
}
|
|
1996
|
+
if (this.check("COMMA")) {
|
|
1997
|
+
this.advance();
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
this.expect("RPAREN");
|
|
2001
|
+
expr = {
|
|
2002
|
+
kind: "call",
|
|
2003
|
+
callee: expr,
|
|
2004
|
+
args,
|
|
2005
|
+
kwargs: kwargs.length > 0 ? kwargs : undefined,
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
else {
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
return expr;
|
|
2013
|
+
}
|
|
2014
|
+
/** Parse primary expression */
|
|
2015
|
+
parsePrimary() {
|
|
2016
|
+
const token = this.current();
|
|
2017
|
+
// Number
|
|
2018
|
+
if (token.type === "NUMBER") {
|
|
2019
|
+
this.advance();
|
|
2020
|
+
const value = token.value.includes(".")
|
|
2021
|
+
? Number.parseFloat(token.value)
|
|
2022
|
+
: Number.parseInt(token.value, 10);
|
|
2023
|
+
if (this.check("IDENTIFIER")) {
|
|
2024
|
+
const unit = this.advance().value;
|
|
2025
|
+
return { kind: "unit_literal", value, unit };
|
|
2026
|
+
}
|
|
2027
|
+
return { kind: "literal", value, literalType: "number" };
|
|
2028
|
+
}
|
|
2029
|
+
// String
|
|
2030
|
+
if (token.type === "STRING") {
|
|
2031
|
+
this.advance();
|
|
2032
|
+
return { kind: "literal", value: token.value, literalType: "string" };
|
|
2033
|
+
}
|
|
2034
|
+
// Boolean
|
|
2035
|
+
if (token.type === "BOOLEAN") {
|
|
2036
|
+
this.advance();
|
|
2037
|
+
return {
|
|
2038
|
+
kind: "literal",
|
|
2039
|
+
value: token.value === "true",
|
|
2040
|
+
literalType: "boolean",
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
// Address
|
|
2044
|
+
if (token.type === "ADDRESS") {
|
|
2045
|
+
this.advance();
|
|
2046
|
+
return { kind: "literal", value: token.value, literalType: "address" };
|
|
2047
|
+
}
|
|
2048
|
+
// Percentage
|
|
2049
|
+
if (token.type === "PERCENTAGE") {
|
|
2050
|
+
this.advance();
|
|
2051
|
+
return { kind: "percentage", value: Number.parseFloat(token.value) };
|
|
2052
|
+
}
|
|
2053
|
+
// Venue reference: @name
|
|
2054
|
+
if (token.type === "VENUE_REF") {
|
|
2055
|
+
this.advance();
|
|
2056
|
+
return { kind: "venue_ref_expr", name: token.value };
|
|
2057
|
+
}
|
|
2058
|
+
// Advisory: **text**
|
|
2059
|
+
if (token.type === "ADVISORY") {
|
|
2060
|
+
this.advance();
|
|
2061
|
+
let advisor;
|
|
2062
|
+
if (this.check("KEYWORD", "via")) {
|
|
2063
|
+
this.advance();
|
|
2064
|
+
if (this.check("IDENTIFIER") || this.check("KEYWORD")) {
|
|
2065
|
+
advisor = this.advance().value;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
return { kind: "advisory_expr", prompt: token.value, advisor };
|
|
2069
|
+
}
|
|
2070
|
+
// Array literal: [a, b, c]
|
|
2071
|
+
if (token.type === "LBRACKET") {
|
|
2072
|
+
this.advance();
|
|
2073
|
+
const elements = [];
|
|
2074
|
+
while (!this.check("RBRACKET")) {
|
|
2075
|
+
elements.push(this.parseExpression());
|
|
2076
|
+
if (this.check("COMMA")) {
|
|
2077
|
+
this.advance();
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
this.expect("RBRACKET");
|
|
2081
|
+
return { kind: "array_literal", elements };
|
|
2082
|
+
}
|
|
2083
|
+
// Object literal: {key: value}
|
|
2084
|
+
if (token.type === "LBRACE") {
|
|
2085
|
+
this.advance();
|
|
2086
|
+
const entries = [];
|
|
2087
|
+
while (!this.check("RBRACE")) {
|
|
2088
|
+
const key = this.expect("IDENTIFIER").value;
|
|
2089
|
+
this.expect("COLON");
|
|
2090
|
+
const value = this.parseExpression();
|
|
2091
|
+
entries.push({ key, value });
|
|
2092
|
+
if (this.check("COMMA")) {
|
|
2093
|
+
this.advance();
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
this.expect("RBRACE");
|
|
2097
|
+
return { kind: "object_literal", entries };
|
|
2098
|
+
}
|
|
2099
|
+
// Parenthesized expression
|
|
2100
|
+
if (token.type === "LPAREN") {
|
|
2101
|
+
this.advance();
|
|
2102
|
+
const expr = this.parseExpression();
|
|
2103
|
+
this.expect("RPAREN");
|
|
2104
|
+
return expr;
|
|
2105
|
+
}
|
|
2106
|
+
// Identifier or keyword used as identifier in expression context
|
|
2107
|
+
// Many keywords can be used as identifiers when they appear in expressions
|
|
2108
|
+
const keywordsAsIdentifiers = new Set([
|
|
2109
|
+
"max",
|
|
2110
|
+
"assets",
|
|
2111
|
+
"params",
|
|
2112
|
+
"limits",
|
|
2113
|
+
"state",
|
|
2114
|
+
"venues",
|
|
2115
|
+
"lending",
|
|
2116
|
+
"swap",
|
|
2117
|
+
"persistent",
|
|
2118
|
+
"ephemeral",
|
|
2119
|
+
"version",
|
|
2120
|
+
"description",
|
|
2121
|
+
"skills",
|
|
2122
|
+
"advisors",
|
|
2123
|
+
"guards",
|
|
2124
|
+
"block",
|
|
2125
|
+
]);
|
|
2126
|
+
if (token.type === "IDENTIFIER" ||
|
|
2127
|
+
(token.type === "KEYWORD" && keywordsAsIdentifiers.has(token.value))) {
|
|
2128
|
+
this.advance();
|
|
2129
|
+
return { kind: "identifier", name: token.value };
|
|
2130
|
+
}
|
|
2131
|
+
throw new ParseError(`Unexpected token in expression: ${token.type} '${token.value}'`, {
|
|
2132
|
+
location: token.location,
|
|
2133
|
+
source: this.source,
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
// =============================================================================
|
|
2138
|
+
// PUBLIC API
|
|
2139
|
+
// =============================================================================
|
|
2140
|
+
/**
|
|
2141
|
+
* Parse Grimoire source code into AST
|
|
2142
|
+
*/
|
|
2143
|
+
export function parse(source) {
|
|
2144
|
+
const tokens = tokenize(source);
|
|
2145
|
+
const parser = new Parser(tokens, source);
|
|
2146
|
+
return parser.parseSpellFile();
|
|
2147
|
+
}
|
|
2148
|
+
//# sourceMappingURL=parser.js.map
|