@jsleekr/graft 5.7.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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/analyzer/estimator.d.ts +33 -0
- package/dist/analyzer/estimator.js +273 -0
- package/dist/analyzer/graph-checker.d.ts +13 -0
- package/dist/analyzer/graph-checker.js +153 -0
- package/dist/analyzer/scope.d.ts +21 -0
- package/dist/analyzer/scope.js +324 -0
- package/dist/analyzer/types.d.ts +17 -0
- package/dist/analyzer/types.js +323 -0
- package/dist/codegen/agents.d.ts +2 -0
- package/dist/codegen/agents.js +109 -0
- package/dist/codegen/backend.d.ts +16 -0
- package/dist/codegen/backend.js +1 -0
- package/dist/codegen/claude-backend.d.ts +9 -0
- package/dist/codegen/claude-backend.js +47 -0
- package/dist/codegen/codegen.d.ts +10 -0
- package/dist/codegen/codegen.js +57 -0
- package/dist/codegen/hooks.d.ts +2 -0
- package/dist/codegen/hooks.js +165 -0
- package/dist/codegen/orchestration.d.ts +3 -0
- package/dist/codegen/orchestration.js +250 -0
- package/dist/codegen/settings.d.ts +36 -0
- package/dist/codegen/settings.js +87 -0
- package/dist/compiler.d.ts +21 -0
- package/dist/compiler.js +101 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/errors/diagnostics.d.ts +21 -0
- package/dist/errors/diagnostics.js +25 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +181 -0
- package/dist/lexer/lexer.d.ts +23 -0
- package/dist/lexer/lexer.js +268 -0
- package/dist/lexer/tokens.d.ts +96 -0
- package/dist/lexer/tokens.js +150 -0
- package/dist/lsp/features/code-actions.d.ts +7 -0
- package/dist/lsp/features/code-actions.js +58 -0
- package/dist/lsp/features/completions.d.ts +7 -0
- package/dist/lsp/features/completions.js +271 -0
- package/dist/lsp/features/definition.d.ts +3 -0
- package/dist/lsp/features/definition.js +32 -0
- package/dist/lsp/features/diagnostics.d.ts +4 -0
- package/dist/lsp/features/diagnostics.js +33 -0
- package/dist/lsp/features/hover.d.ts +7 -0
- package/dist/lsp/features/hover.js +88 -0
- package/dist/lsp/features/index.d.ts +9 -0
- package/dist/lsp/features/index.js +9 -0
- package/dist/lsp/features/references.d.ts +7 -0
- package/dist/lsp/features/references.js +53 -0
- package/dist/lsp/features/rename.d.ts +17 -0
- package/dist/lsp/features/rename.js +198 -0
- package/dist/lsp/features/symbols.d.ts +7 -0
- package/dist/lsp/features/symbols.js +74 -0
- package/dist/lsp/features/utils.d.ts +3 -0
- package/dist/lsp/features/utils.js +65 -0
- package/dist/lsp/features.d.ts +20 -0
- package/dist/lsp/features.js +513 -0
- package/dist/lsp/server.d.ts +2 -0
- package/dist/lsp/server.js +327 -0
- package/dist/parser/ast.d.ts +244 -0
- package/dist/parser/ast.js +10 -0
- package/dist/parser/parser.d.ts +95 -0
- package/dist/parser/parser.js +1175 -0
- package/dist/program-index.d.ts +21 -0
- package/dist/program-index.js +74 -0
- package/dist/resolver/resolver.d.ts +9 -0
- package/dist/resolver/resolver.js +136 -0
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +41 -0
- package/dist/runtime/executor.d.ts +56 -0
- package/dist/runtime/executor.js +285 -0
- package/dist/runtime/expr-eval.d.ts +3 -0
- package/dist/runtime/expr-eval.js +138 -0
- package/dist/runtime/flow-runner.d.ts +21 -0
- package/dist/runtime/flow-runner.js +230 -0
- package/dist/runtime/memory.d.ts +5 -0
- package/dist/runtime/memory.js +41 -0
- package/dist/runtime/prompt-builder.d.ts +12 -0
- package/dist/runtime/prompt-builder.js +66 -0
- package/dist/runtime/subprocess.d.ts +20 -0
- package/dist/runtime/subprocess.js +99 -0
- package/dist/runtime/token-tracker.d.ts +36 -0
- package/dist/runtime/token-tracker.js +56 -0
- package/dist/runtime/transforms.d.ts +2 -0
- package/dist/runtime/transforms.js +104 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +35 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +11 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
// src/parser/parser.ts
|
|
2
|
+
import { TokenType, KEYWORDS } from '../lexer/tokens.js';
|
|
3
|
+
import { Lexer } from '../lexer/lexer.js';
|
|
4
|
+
import { GraftError } from '../errors/diagnostics.js';
|
|
5
|
+
import { BUILTIN_FUNCTIONS, } from './ast.js';
|
|
6
|
+
// Build a Set of all keyword token types for O(1) lookup.
|
|
7
|
+
// Used by expectIdentifierOrKeyword() to accept keywords as contextual identifiers.
|
|
8
|
+
const KEYWORD_TYPES = new Set(Object.values(KEYWORDS));
|
|
9
|
+
export class Parser {
|
|
10
|
+
tokens;
|
|
11
|
+
pos = 0;
|
|
12
|
+
errors = [];
|
|
13
|
+
static MAX_ERRORS = 25;
|
|
14
|
+
static DECLARATION_KEYWORDS = new Set([
|
|
15
|
+
TokenType.Context, TokenType.Node, TokenType.Memory,
|
|
16
|
+
TokenType.Graph, TokenType.Edge, TokenType.Import,
|
|
17
|
+
]);
|
|
18
|
+
constructor(tokens) {
|
|
19
|
+
this.tokens = tokens;
|
|
20
|
+
}
|
|
21
|
+
parse() {
|
|
22
|
+
const program = {
|
|
23
|
+
imports: [],
|
|
24
|
+
memories: [],
|
|
25
|
+
contexts: [],
|
|
26
|
+
nodes: [],
|
|
27
|
+
edges: [],
|
|
28
|
+
graphs: [],
|
|
29
|
+
};
|
|
30
|
+
let seenNonImport = false;
|
|
31
|
+
while (!this.isAtEnd()) {
|
|
32
|
+
if (this.errors.length >= Parser.MAX_ERRORS)
|
|
33
|
+
break;
|
|
34
|
+
const token = this.current();
|
|
35
|
+
try {
|
|
36
|
+
switch (token.type) {
|
|
37
|
+
case TokenType.Import:
|
|
38
|
+
if (seenNonImport) {
|
|
39
|
+
this.errors.push(this.error('Import declarations must appear before all other declarations'));
|
|
40
|
+
this.advance(); // advance past the Import keyword
|
|
41
|
+
this.synchronize();
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
program.imports.push(this.parseImportDecl());
|
|
45
|
+
break;
|
|
46
|
+
case TokenType.Memory:
|
|
47
|
+
seenNonImport = true;
|
|
48
|
+
program.memories.push(this.parseMemoryDecl());
|
|
49
|
+
break;
|
|
50
|
+
case TokenType.Context:
|
|
51
|
+
seenNonImport = true;
|
|
52
|
+
program.contexts.push(this.parseContext());
|
|
53
|
+
break;
|
|
54
|
+
case TokenType.Node:
|
|
55
|
+
seenNonImport = true;
|
|
56
|
+
program.nodes.push(this.parseNode());
|
|
57
|
+
break;
|
|
58
|
+
case TokenType.Edge:
|
|
59
|
+
seenNonImport = true;
|
|
60
|
+
program.edges.push(this.parseEdge());
|
|
61
|
+
break;
|
|
62
|
+
case TokenType.Graph:
|
|
63
|
+
seenNonImport = true;
|
|
64
|
+
program.graphs.push(this.parseGraph());
|
|
65
|
+
break;
|
|
66
|
+
default: {
|
|
67
|
+
this.errors.push(this.error(`Unexpected token '${token.value}', expected 'import', 'memory', 'context', 'node', 'edge', or 'graph'`));
|
|
68
|
+
const posBefore = this.pos;
|
|
69
|
+
this.synchronize();
|
|
70
|
+
if (this.pos === posBefore && !this.isAtEnd())
|
|
71
|
+
this.advance();
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
if (e instanceof GraftError) {
|
|
78
|
+
this.errors.push(e);
|
|
79
|
+
const posBefore = this.pos;
|
|
80
|
+
this.synchronize();
|
|
81
|
+
if (this.pos === posBefore && !this.isAtEnd())
|
|
82
|
+
this.advance();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { program, errors: this.errors };
|
|
90
|
+
}
|
|
91
|
+
synchronize() {
|
|
92
|
+
let braceDepth = 0;
|
|
93
|
+
while (!this.isAtEnd()) {
|
|
94
|
+
const token = this.current();
|
|
95
|
+
if (token.type === TokenType.LBrace) {
|
|
96
|
+
braceDepth++;
|
|
97
|
+
}
|
|
98
|
+
else if (token.type === TokenType.RBrace) {
|
|
99
|
+
if (braceDepth > 0) {
|
|
100
|
+
braceDepth--;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Closing brace at depth 0 — skip it and stop
|
|
104
|
+
this.advance();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (braceDepth === 0 && Parser.DECLARATION_KEYWORDS.has(token.type)) {
|
|
109
|
+
// At top level, found a declaration keyword — stop (don't consume it)
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
this.advance();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// --- Import ------------------------------------------------
|
|
116
|
+
parseImportDecl() {
|
|
117
|
+
const loc = this.current().location;
|
|
118
|
+
this.expect(TokenType.Import);
|
|
119
|
+
this.expect(TokenType.LBrace);
|
|
120
|
+
const names = [];
|
|
121
|
+
while (!this.check(TokenType.RBrace)) {
|
|
122
|
+
if (names.length > 0)
|
|
123
|
+
this.expect(TokenType.Comma);
|
|
124
|
+
names.push(this.expectIdentifier());
|
|
125
|
+
}
|
|
126
|
+
if (names.length === 0) {
|
|
127
|
+
throw this.error('Import must specify at least one name');
|
|
128
|
+
}
|
|
129
|
+
this.expect(TokenType.RBrace);
|
|
130
|
+
this.expect(TokenType.From);
|
|
131
|
+
const pathToken = this.expect(TokenType.StringLiteral);
|
|
132
|
+
const path = pathToken.value;
|
|
133
|
+
if (path === '') {
|
|
134
|
+
throw this.error('Import path cannot be empty');
|
|
135
|
+
}
|
|
136
|
+
return { names, path, location: loc };
|
|
137
|
+
}
|
|
138
|
+
// --- Memory ------------------------------------------------
|
|
139
|
+
parseMemoryDecl() {
|
|
140
|
+
const loc = this.current().location;
|
|
141
|
+
this.expect(TokenType.Memory);
|
|
142
|
+
const name = this.expectIdentifier();
|
|
143
|
+
this.expect(TokenType.LParen);
|
|
144
|
+
this.expect(TokenType.MaxTokens);
|
|
145
|
+
this.expect(TokenType.Colon);
|
|
146
|
+
const maxTokens = this.parseTokenValue();
|
|
147
|
+
let storage = 'file';
|
|
148
|
+
if (this.check(TokenType.Comma)) {
|
|
149
|
+
this.advance();
|
|
150
|
+
this.expect(TokenType.Storage);
|
|
151
|
+
this.expect(TokenType.Colon);
|
|
152
|
+
const storageValue = this.expectIdentifierOrKeyword();
|
|
153
|
+
if (storageValue !== 'file') {
|
|
154
|
+
throw this.error(`Unknown storage type '${storageValue}', expected 'file'`);
|
|
155
|
+
}
|
|
156
|
+
storage = 'file';
|
|
157
|
+
}
|
|
158
|
+
this.expect(TokenType.RParen);
|
|
159
|
+
this.expect(TokenType.LBrace);
|
|
160
|
+
const fields = this.parseFields();
|
|
161
|
+
this.expect(TokenType.RBrace);
|
|
162
|
+
return { name, maxTokens, storage, fields, location: loc };
|
|
163
|
+
}
|
|
164
|
+
// --- Context ------------------------------------------------
|
|
165
|
+
parseContext() {
|
|
166
|
+
const loc = this.current().location;
|
|
167
|
+
this.expect(TokenType.Context);
|
|
168
|
+
const name = this.expectIdentifier();
|
|
169
|
+
// Parameters: (max_tokens: <Int>)
|
|
170
|
+
this.expect(TokenType.LParen);
|
|
171
|
+
this.expect(TokenType.MaxTokens);
|
|
172
|
+
this.expect(TokenType.Colon);
|
|
173
|
+
const maxTokens = this.parseTokenValue();
|
|
174
|
+
this.expect(TokenType.RParen);
|
|
175
|
+
// Body: { fields }
|
|
176
|
+
this.expect(TokenType.LBrace);
|
|
177
|
+
const fields = this.parseFields();
|
|
178
|
+
this.expect(TokenType.RBrace);
|
|
179
|
+
return { name, maxTokens, fields, location: loc };
|
|
180
|
+
}
|
|
181
|
+
// --- Node ---------------------------------------------------
|
|
182
|
+
parseNode() {
|
|
183
|
+
const loc = this.current().location;
|
|
184
|
+
this.expect(TokenType.Node);
|
|
185
|
+
const name = this.expectIdentifier();
|
|
186
|
+
// Parameters: (model: <id>, budget: <in>/<out>)
|
|
187
|
+
this.expect(TokenType.LParen);
|
|
188
|
+
this.expect(TokenType.Model);
|
|
189
|
+
this.expect(TokenType.Colon);
|
|
190
|
+
const model = this.expectIdentifier();
|
|
191
|
+
this.expect(TokenType.Comma);
|
|
192
|
+
this.expect(TokenType.Budget);
|
|
193
|
+
this.expect(TokenType.Colon);
|
|
194
|
+
const budgetIn = this.parseTokenValue();
|
|
195
|
+
this.expect(TokenType.Slash);
|
|
196
|
+
const budgetOut = this.parseTokenValue();
|
|
197
|
+
this.expect(TokenType.RParen);
|
|
198
|
+
// Body
|
|
199
|
+
this.expect(TokenType.LBrace);
|
|
200
|
+
let reads = [];
|
|
201
|
+
let tools = [];
|
|
202
|
+
let writes = [];
|
|
203
|
+
let onFailure;
|
|
204
|
+
let produces;
|
|
205
|
+
let hasWrites = false;
|
|
206
|
+
while (!this.check(TokenType.RBrace)) {
|
|
207
|
+
if (this.check(TokenType.Reads)) {
|
|
208
|
+
this.advance();
|
|
209
|
+
this.expect(TokenType.Colon);
|
|
210
|
+
reads = this.parseContextRefList();
|
|
211
|
+
}
|
|
212
|
+
else if (this.check(TokenType.Tools)) {
|
|
213
|
+
this.advance();
|
|
214
|
+
this.expect(TokenType.Colon);
|
|
215
|
+
tools = this.parseIdentifierList();
|
|
216
|
+
}
|
|
217
|
+
else if (this.check(TokenType.Writes)) {
|
|
218
|
+
if (hasWrites) {
|
|
219
|
+
throw this.error('Duplicate writes clause in node');
|
|
220
|
+
}
|
|
221
|
+
hasWrites = true;
|
|
222
|
+
this.advance();
|
|
223
|
+
this.expect(TokenType.Colon);
|
|
224
|
+
writes = this.parseWriteRefList();
|
|
225
|
+
}
|
|
226
|
+
else if (this.check(TokenType.OnFailure)) {
|
|
227
|
+
this.advance();
|
|
228
|
+
this.expect(TokenType.Colon);
|
|
229
|
+
onFailure = this.parseFailureStrategy();
|
|
230
|
+
}
|
|
231
|
+
else if (this.check(TokenType.Produces)) {
|
|
232
|
+
produces = this.parseProduces();
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
throw this.error(`Unexpected token '${this.current().value}' in node body`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
this.expect(TokenType.RBrace);
|
|
239
|
+
if (!produces) {
|
|
240
|
+
throw new GraftError('Node must have a produces declaration', loc, 'error', 'PARSE_MISSING_FIELD');
|
|
241
|
+
}
|
|
242
|
+
return { name, model, budgetIn, budgetOut, reads, tools, writes, onFailure, produces, location: loc };
|
|
243
|
+
}
|
|
244
|
+
parseProduces() {
|
|
245
|
+
const loc = this.current().location;
|
|
246
|
+
this.expect(TokenType.Produces);
|
|
247
|
+
const name = this.expectIdentifier();
|
|
248
|
+
this.expect(TokenType.LBrace);
|
|
249
|
+
const fields = this.parseFields();
|
|
250
|
+
this.expect(TokenType.RBrace);
|
|
251
|
+
return { name, fields, location: loc };
|
|
252
|
+
}
|
|
253
|
+
parseContextRefList() {
|
|
254
|
+
this.expect(TokenType.LBracket);
|
|
255
|
+
const refs = [];
|
|
256
|
+
while (!this.check(TokenType.RBracket)) {
|
|
257
|
+
if (refs.length > 0)
|
|
258
|
+
this.expect(TokenType.Comma);
|
|
259
|
+
const loc = this.current().location;
|
|
260
|
+
const context = this.expectIdentifier();
|
|
261
|
+
let field;
|
|
262
|
+
if (this.check(TokenType.Dot)) {
|
|
263
|
+
this.advance();
|
|
264
|
+
if (this.check(TokenType.LBrace)) {
|
|
265
|
+
this.advance();
|
|
266
|
+
const fields = [];
|
|
267
|
+
while (!this.check(TokenType.RBrace)) {
|
|
268
|
+
if (fields.length > 0)
|
|
269
|
+
this.expect(TokenType.Comma);
|
|
270
|
+
fields.push(this.expectIdentifierOrKeyword());
|
|
271
|
+
}
|
|
272
|
+
if (fields.length === 0) {
|
|
273
|
+
throw this.error('Multi-field read must specify at least one field');
|
|
274
|
+
}
|
|
275
|
+
this.expect(TokenType.RBrace);
|
|
276
|
+
field = fields;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
field = [this.expectIdentifierOrKeyword()];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
refs.push({ context, field, location: loc });
|
|
283
|
+
}
|
|
284
|
+
this.expect(TokenType.RBracket);
|
|
285
|
+
return refs;
|
|
286
|
+
}
|
|
287
|
+
parseWriteRefList() {
|
|
288
|
+
this.expect(TokenType.LBracket);
|
|
289
|
+
const refs = [];
|
|
290
|
+
while (!this.check(TokenType.RBracket)) {
|
|
291
|
+
if (refs.length > 0)
|
|
292
|
+
this.expect(TokenType.Comma);
|
|
293
|
+
const loc = this.current().location;
|
|
294
|
+
const memory = this.expectIdentifier();
|
|
295
|
+
let field;
|
|
296
|
+
if (this.check(TokenType.Dot)) {
|
|
297
|
+
this.advance();
|
|
298
|
+
field = this.expectIdentifierOrKeyword();
|
|
299
|
+
}
|
|
300
|
+
refs.push({ memory, field, location: loc });
|
|
301
|
+
}
|
|
302
|
+
this.expect(TokenType.RBracket);
|
|
303
|
+
return refs;
|
|
304
|
+
}
|
|
305
|
+
parseIdentifierList() {
|
|
306
|
+
this.expect(TokenType.LBracket);
|
|
307
|
+
const ids = [];
|
|
308
|
+
while (!this.check(TokenType.RBracket)) {
|
|
309
|
+
if (ids.length > 0)
|
|
310
|
+
this.expect(TokenType.Comma);
|
|
311
|
+
ids.push(this.expectIdentifierOrKeyword());
|
|
312
|
+
}
|
|
313
|
+
this.expect(TokenType.RBracket);
|
|
314
|
+
return ids;
|
|
315
|
+
}
|
|
316
|
+
parseFailureStrategy() {
|
|
317
|
+
if (this.check(TokenType.Retry)) {
|
|
318
|
+
this.advance();
|
|
319
|
+
this.expect(TokenType.LParen);
|
|
320
|
+
const max = this.parseIntValue();
|
|
321
|
+
// Check for: retry(2, fallback(NodeName))
|
|
322
|
+
if (this.check(TokenType.Comma)) {
|
|
323
|
+
this.advance();
|
|
324
|
+
this.expect(TokenType.Fallback);
|
|
325
|
+
this.expect(TokenType.LParen);
|
|
326
|
+
const node = this.expectIdentifier();
|
|
327
|
+
this.expect(TokenType.RParen);
|
|
328
|
+
this.expect(TokenType.RParen);
|
|
329
|
+
return { type: 'retry_then_fallback', max, node };
|
|
330
|
+
}
|
|
331
|
+
this.expect(TokenType.RParen);
|
|
332
|
+
return { type: 'retry', max };
|
|
333
|
+
}
|
|
334
|
+
if (this.check(TokenType.Fallback)) {
|
|
335
|
+
this.advance();
|
|
336
|
+
this.expect(TokenType.LParen);
|
|
337
|
+
const node = this.expectIdentifier();
|
|
338
|
+
this.expect(TokenType.RParen);
|
|
339
|
+
return { type: 'fallback', node };
|
|
340
|
+
}
|
|
341
|
+
if (this.check(TokenType.Skip)) {
|
|
342
|
+
this.advance();
|
|
343
|
+
return { type: 'skip' };
|
|
344
|
+
}
|
|
345
|
+
if (this.check(TokenType.Abort)) {
|
|
346
|
+
this.advance();
|
|
347
|
+
return { type: 'abort' };
|
|
348
|
+
}
|
|
349
|
+
throw this.error('Expected failure strategy (retry, fallback, skip, abort)');
|
|
350
|
+
}
|
|
351
|
+
// --- Edge ---------------------------------------------------
|
|
352
|
+
parseEdge() {
|
|
353
|
+
const loc = this.current().location;
|
|
354
|
+
this.expect(TokenType.Edge);
|
|
355
|
+
const source = this.expectIdentifier();
|
|
356
|
+
this.expect(TokenType.Arrow);
|
|
357
|
+
let target;
|
|
358
|
+
let transforms = [];
|
|
359
|
+
if (this.check(TokenType.LBrace)) {
|
|
360
|
+
// Conditional routing
|
|
361
|
+
target = this.parseConditionalTarget();
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// Direct target
|
|
365
|
+
const node = this.expectIdentifier();
|
|
366
|
+
target = { kind: 'direct', node };
|
|
367
|
+
// Parse pipe transforms
|
|
368
|
+
while (this.check(TokenType.Pipe)) {
|
|
369
|
+
this.advance();
|
|
370
|
+
transforms.push(this.parseTransform());
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { source, target, transforms, location: loc };
|
|
374
|
+
}
|
|
375
|
+
parseConditionalTarget() {
|
|
376
|
+
this.expect(TokenType.LBrace);
|
|
377
|
+
const branches = [];
|
|
378
|
+
while (!this.check(TokenType.RBrace)) {
|
|
379
|
+
if (this.check(TokenType.When)) {
|
|
380
|
+
this.advance();
|
|
381
|
+
const condition = this.parseCondition();
|
|
382
|
+
this.expect(TokenType.Arrow);
|
|
383
|
+
const target = this.check(TokenType.Done) ? (this.advance(), 'done') : this.expectIdentifier();
|
|
384
|
+
branches.push({ condition, target });
|
|
385
|
+
}
|
|
386
|
+
else if (this.check(TokenType.Else)) {
|
|
387
|
+
this.advance();
|
|
388
|
+
this.expect(TokenType.Arrow);
|
|
389
|
+
const target = this.check(TokenType.Done) ? (this.advance(), 'done') : this.expectIdentifier();
|
|
390
|
+
branches.push({ condition: undefined, target });
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
throw this.error(`Expected 'when' or 'else' in conditional edge`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
this.expect(TokenType.RBrace);
|
|
397
|
+
return { kind: 'conditional', branches };
|
|
398
|
+
}
|
|
399
|
+
parseTransform() {
|
|
400
|
+
if (this.check(TokenType.Select)) {
|
|
401
|
+
this.advance();
|
|
402
|
+
this.expect(TokenType.LParen);
|
|
403
|
+
const fields = [];
|
|
404
|
+
fields.push(this.expectIdentifierOrKeyword());
|
|
405
|
+
while (this.check(TokenType.Comma)) {
|
|
406
|
+
this.advance();
|
|
407
|
+
fields.push(this.expectIdentifierOrKeyword());
|
|
408
|
+
}
|
|
409
|
+
this.expect(TokenType.RParen);
|
|
410
|
+
return { type: 'select', fields };
|
|
411
|
+
}
|
|
412
|
+
if (this.check(TokenType.Filter)) {
|
|
413
|
+
this.advance();
|
|
414
|
+
this.expect(TokenType.LParen);
|
|
415
|
+
const field = this.expectIdentifierOrKeyword();
|
|
416
|
+
this.expect(TokenType.Comma);
|
|
417
|
+
const condition = this.parseCondition();
|
|
418
|
+
this.expect(TokenType.RParen);
|
|
419
|
+
return { type: 'filter', field, condition };
|
|
420
|
+
}
|
|
421
|
+
if (this.check(TokenType.Drop)) {
|
|
422
|
+
this.advance();
|
|
423
|
+
this.expect(TokenType.LParen);
|
|
424
|
+
const field = this.expectIdentifierOrKeyword();
|
|
425
|
+
this.expect(TokenType.RParen);
|
|
426
|
+
return { type: 'drop', field };
|
|
427
|
+
}
|
|
428
|
+
if (this.check(TokenType.Compact)) {
|
|
429
|
+
this.advance();
|
|
430
|
+
return { type: 'compact' };
|
|
431
|
+
}
|
|
432
|
+
if (this.check(TokenType.Truncate)) {
|
|
433
|
+
this.advance();
|
|
434
|
+
this.expect(TokenType.LParen);
|
|
435
|
+
const tokens = this.parseTokenValue();
|
|
436
|
+
this.expect(TokenType.RParen);
|
|
437
|
+
return { type: 'truncate', tokens };
|
|
438
|
+
}
|
|
439
|
+
throw this.error('Expected transform operation (select, filter, drop, compact, truncate)');
|
|
440
|
+
}
|
|
441
|
+
parseCondition() {
|
|
442
|
+
const loc = this.current().location;
|
|
443
|
+
const field = this.expectIdentifierOrKeyword();
|
|
444
|
+
const left = { kind: 'field_access', segments: [field], location: loc };
|
|
445
|
+
const opToken = this.current();
|
|
446
|
+
let op;
|
|
447
|
+
switch (opToken.type) {
|
|
448
|
+
case TokenType.GreaterEqual:
|
|
449
|
+
op = '>=';
|
|
450
|
+
break;
|
|
451
|
+
case TokenType.Greater:
|
|
452
|
+
op = '>';
|
|
453
|
+
break;
|
|
454
|
+
case TokenType.LessEqual:
|
|
455
|
+
op = '<=';
|
|
456
|
+
break;
|
|
457
|
+
case TokenType.Less:
|
|
458
|
+
op = '<';
|
|
459
|
+
break;
|
|
460
|
+
case TokenType.EqualEqual:
|
|
461
|
+
op = '==';
|
|
462
|
+
break;
|
|
463
|
+
case TokenType.BangEqual:
|
|
464
|
+
op = '!=';
|
|
465
|
+
break;
|
|
466
|
+
default:
|
|
467
|
+
throw this.error(`Expected comparison operator, got '${opToken.value}'`);
|
|
468
|
+
}
|
|
469
|
+
this.advance();
|
|
470
|
+
const value = this.parseConditionValue();
|
|
471
|
+
const right = { kind: 'literal', value, location: this.tokens[this.pos - 1].location };
|
|
472
|
+
return { kind: 'binary', op, left, right, location: loc };
|
|
473
|
+
}
|
|
474
|
+
parseConditionValue() {
|
|
475
|
+
const token = this.current();
|
|
476
|
+
if (token.type === TokenType.IntegerLiteral) {
|
|
477
|
+
this.advance();
|
|
478
|
+
return parseInt(token.value, 10);
|
|
479
|
+
}
|
|
480
|
+
if (token.type === TokenType.KIntegerLiteral) {
|
|
481
|
+
this.advance();
|
|
482
|
+
// parseInt("5k", 10) returns 5 per the JS spec: parseInt stops at first non-digit.
|
|
483
|
+
return parseInt(token.value, 10) * 1000;
|
|
484
|
+
}
|
|
485
|
+
if (token.type === TokenType.FloatLiteral) {
|
|
486
|
+
this.advance();
|
|
487
|
+
return parseFloat(token.value);
|
|
488
|
+
}
|
|
489
|
+
if (token.type === TokenType.StringLiteral) {
|
|
490
|
+
this.advance();
|
|
491
|
+
return token.value;
|
|
492
|
+
}
|
|
493
|
+
if (token.type === TokenType.True) {
|
|
494
|
+
this.advance();
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
if (token.type === TokenType.False) {
|
|
498
|
+
this.advance();
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
// Allow bare identifiers as string values (e.g., severity >= medium)
|
|
502
|
+
if (token.type === TokenType.Identifier || KEYWORD_TYPES.has(token.type)) {
|
|
503
|
+
this.advance();
|
|
504
|
+
return token.value;
|
|
505
|
+
}
|
|
506
|
+
throw this.error(`Expected value in condition, got '${token.value}'`);
|
|
507
|
+
}
|
|
508
|
+
// --- Graph --------------------------------------------------
|
|
509
|
+
parseGraph() {
|
|
510
|
+
const loc = this.current().location;
|
|
511
|
+
this.expect(TokenType.Graph);
|
|
512
|
+
const name = this.expectIdentifier();
|
|
513
|
+
// Parameters: (input: X, output: Y, budget: Nk [, param: Type]*)
|
|
514
|
+
this.expect(TokenType.LParen);
|
|
515
|
+
this.expect(TokenType.Input);
|
|
516
|
+
this.expect(TokenType.Colon);
|
|
517
|
+
const input = this.expectIdentifier();
|
|
518
|
+
this.expect(TokenType.Comma);
|
|
519
|
+
this.expect(TokenType.Output);
|
|
520
|
+
this.expect(TokenType.Colon);
|
|
521
|
+
const output = this.expectIdentifier();
|
|
522
|
+
this.expect(TokenType.Comma);
|
|
523
|
+
this.expect(TokenType.Budget);
|
|
524
|
+
this.expect(TokenType.Colon);
|
|
525
|
+
const budget = this.parseTokenValue();
|
|
526
|
+
// Optional params after budget
|
|
527
|
+
const params = [];
|
|
528
|
+
while (this.check(TokenType.Comma)) {
|
|
529
|
+
this.advance(); // consume comma
|
|
530
|
+
if (this.check(TokenType.RParen))
|
|
531
|
+
break; // trailing comma
|
|
532
|
+
params.push(this.parseGraphParam());
|
|
533
|
+
}
|
|
534
|
+
this.expect(TokenType.RParen);
|
|
535
|
+
// Body: { FlowNodes -> done }
|
|
536
|
+
this.expect(TokenType.LBrace);
|
|
537
|
+
const flow = this.parseFlowNodes(/* insideBlock */ false);
|
|
538
|
+
this.expect(TokenType.RBrace);
|
|
539
|
+
return { name, input, output, budget, params, flow, location: loc };
|
|
540
|
+
}
|
|
541
|
+
parseGraphParam() {
|
|
542
|
+
const loc = this.current().location;
|
|
543
|
+
const name = this.expectIdentifierOrKeyword();
|
|
544
|
+
this.expect(TokenType.Colon);
|
|
545
|
+
// Type: Node (identifier), Int, String, Bool (keywords)
|
|
546
|
+
const typeToken = this.current();
|
|
547
|
+
let type;
|
|
548
|
+
if (typeToken.type === TokenType.Identifier && typeToken.value === 'Node') {
|
|
549
|
+
type = 'Node';
|
|
550
|
+
this.advance();
|
|
551
|
+
}
|
|
552
|
+
else if (typeToken.type === TokenType.Int) {
|
|
553
|
+
type = 'Int';
|
|
554
|
+
this.advance();
|
|
555
|
+
}
|
|
556
|
+
else if (typeToken.type === TokenType.String) {
|
|
557
|
+
type = 'String';
|
|
558
|
+
this.advance();
|
|
559
|
+
}
|
|
560
|
+
else if (typeToken.type === TokenType.Bool) {
|
|
561
|
+
type = 'Bool';
|
|
562
|
+
this.advance();
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
throw this.error(`Expected param type (Node, Int, String, Bool), got '${typeToken.value}'`);
|
|
566
|
+
}
|
|
567
|
+
// Optional default value
|
|
568
|
+
let defaultVal;
|
|
569
|
+
if (this.check(TokenType.Equals)) {
|
|
570
|
+
this.advance();
|
|
571
|
+
defaultVal = this.parseConditionValue();
|
|
572
|
+
}
|
|
573
|
+
return { name, type, default: defaultVal, location: loc };
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Parse a sequence of flow nodes separated by arrows.
|
|
577
|
+
* When insideBlock=true, stops when no more arrows (next token should be RBrace).
|
|
578
|
+
* When insideBlock=false, expects -> done to terminate.
|
|
579
|
+
*/
|
|
580
|
+
parseFlowNodes(insideBlock) {
|
|
581
|
+
const steps = [];
|
|
582
|
+
// Parse first step
|
|
583
|
+
steps.push(this.parseFlowNode());
|
|
584
|
+
while (this.check(TokenType.Arrow)) {
|
|
585
|
+
this.advance(); // consume ->
|
|
586
|
+
// Check for 'done'
|
|
587
|
+
if (this.check(TokenType.Done)) {
|
|
588
|
+
this.advance();
|
|
589
|
+
if (insideBlock) {
|
|
590
|
+
throw this.error("'done' is not allowed inside a foreach or parallel block");
|
|
591
|
+
}
|
|
592
|
+
return steps;
|
|
593
|
+
}
|
|
594
|
+
// Check for RBrace -- end of foreach body after arrow would be an error
|
|
595
|
+
if (this.check(TokenType.RBrace)) {
|
|
596
|
+
throw this.error("Expected flow step after '->'");
|
|
597
|
+
}
|
|
598
|
+
steps.push(this.parseFlowNode());
|
|
599
|
+
}
|
|
600
|
+
// If we get here without 'done' at top level, that's an error
|
|
601
|
+
if (!insideBlock) {
|
|
602
|
+
throw this.error("Expected '-> done' to terminate graph flow");
|
|
603
|
+
}
|
|
604
|
+
// insideBlock: we stop when no more arrows (next token should be RBrace)
|
|
605
|
+
return steps;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Parse a single flow node: identifier, parallel block, foreach block,
|
|
609
|
+
* let binding, or graph call.
|
|
610
|
+
*/
|
|
611
|
+
parseFlowNode() {
|
|
612
|
+
if (this.check(TokenType.Parallel)) {
|
|
613
|
+
return this.parseParallelStep();
|
|
614
|
+
}
|
|
615
|
+
if (this.check(TokenType.Foreach)) {
|
|
616
|
+
return this.parseForeachStep();
|
|
617
|
+
}
|
|
618
|
+
if (this.check(TokenType.Let)) {
|
|
619
|
+
return this.parseLetStep();
|
|
620
|
+
}
|
|
621
|
+
// Identifier: could be a node reference or a graph call
|
|
622
|
+
// Disambiguate: Identifier followed by LParen is a graph call
|
|
623
|
+
const loc = this.current().location;
|
|
624
|
+
const name = this.expectIdentifier();
|
|
625
|
+
if (this.check(TokenType.LParen)) {
|
|
626
|
+
return this.parseGraphCall(name, loc);
|
|
627
|
+
}
|
|
628
|
+
return { kind: 'node', name, location: loc };
|
|
629
|
+
}
|
|
630
|
+
parseLetStep() {
|
|
631
|
+
const loc = this.current().location;
|
|
632
|
+
this.expect(TokenType.Let);
|
|
633
|
+
const name = this.expectIdentifierOrKeyword();
|
|
634
|
+
this.expect(TokenType.Equals);
|
|
635
|
+
const value = this.parseExpr();
|
|
636
|
+
return { kind: 'let', name, value, location: loc };
|
|
637
|
+
}
|
|
638
|
+
parseGraphCall(name, location) {
|
|
639
|
+
this.expect(TokenType.LParen);
|
|
640
|
+
const args = [];
|
|
641
|
+
while (!this.check(TokenType.RParen)) {
|
|
642
|
+
if (args.length > 0)
|
|
643
|
+
this.expect(TokenType.Comma);
|
|
644
|
+
const argLoc = this.current().location;
|
|
645
|
+
const argName = this.expectIdentifierOrKeyword();
|
|
646
|
+
this.expect(TokenType.Colon);
|
|
647
|
+
const value = this.parseExpr();
|
|
648
|
+
args.push({ name: argName, value, location: argLoc });
|
|
649
|
+
}
|
|
650
|
+
this.expect(TokenType.RParen);
|
|
651
|
+
return { kind: 'graph_call', name, args, location };
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* parallel { SecurityReviewer PerformanceReviewer StyleReviewer }
|
|
655
|
+
*
|
|
656
|
+
* Branches are whitespace-separated identifiers (no commas required).
|
|
657
|
+
* Optional commas are accepted for user convenience.
|
|
658
|
+
*/
|
|
659
|
+
parseParallelStep() {
|
|
660
|
+
const loc = this.current().location;
|
|
661
|
+
this.expect(TokenType.Parallel);
|
|
662
|
+
this.expect(TokenType.LBrace);
|
|
663
|
+
const branches = [];
|
|
664
|
+
while (!this.check(TokenType.RBrace)) {
|
|
665
|
+
if (branches.length > 0 && this.check(TokenType.Comma)) {
|
|
666
|
+
this.advance(); // optional comma
|
|
667
|
+
}
|
|
668
|
+
branches.push(this.expectIdentifier());
|
|
669
|
+
}
|
|
670
|
+
this.expect(TokenType.RBrace);
|
|
671
|
+
if (branches.length < 2) {
|
|
672
|
+
throw this.error('parallel block must contain at least 2 branches');
|
|
673
|
+
}
|
|
674
|
+
return { kind: 'parallel', branches, location: loc };
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* foreach(Planner.output.steps as step, max_iterations: 5) {
|
|
678
|
+
* Implementer -> Verifier
|
|
679
|
+
* }
|
|
680
|
+
*/
|
|
681
|
+
parseForeachStep() {
|
|
682
|
+
const loc = this.current().location;
|
|
683
|
+
this.expect(TokenType.Foreach);
|
|
684
|
+
this.expect(TokenType.LParen);
|
|
685
|
+
// Source: Planner.output.steps
|
|
686
|
+
const source = this.expectIdentifier(); // "Planner"
|
|
687
|
+
this.expect(TokenType.Dot);
|
|
688
|
+
this.expect(TokenType.Output); // "output" keyword token
|
|
689
|
+
this.expect(TokenType.Dot);
|
|
690
|
+
const field = this.expectIdentifierOrKeyword(); // "steps"
|
|
691
|
+
// Binding: as step
|
|
692
|
+
this.expect(TokenType.As);
|
|
693
|
+
const binding = this.expectIdentifierOrKeyword(); // "step"
|
|
694
|
+
// max_iterations: 5
|
|
695
|
+
this.expect(TokenType.Comma);
|
|
696
|
+
this.expect(TokenType.MaxIterations);
|
|
697
|
+
this.expect(TokenType.Colon);
|
|
698
|
+
const maxIterations = this.parseIntValue();
|
|
699
|
+
if (maxIterations < 1) {
|
|
700
|
+
throw this.error('max_iterations must be at least 1');
|
|
701
|
+
}
|
|
702
|
+
this.expect(TokenType.RParen);
|
|
703
|
+
// Body: { Implementer -> Verifier }
|
|
704
|
+
this.expect(TokenType.LBrace);
|
|
705
|
+
const body = this.parseFlowNodes(/* insideBlock */ true);
|
|
706
|
+
this.expect(TokenType.RBrace);
|
|
707
|
+
if (body.length === 0) {
|
|
708
|
+
throw this.error('foreach body must contain at least one step');
|
|
709
|
+
}
|
|
710
|
+
// v1.1+: enforce no nesting (body must contain only 'node', 'let', or 'graph_call' entries)
|
|
711
|
+
for (const step of body) {
|
|
712
|
+
if (step.kind === 'parallel' || step.kind === 'foreach') {
|
|
713
|
+
throw this.error('Nested parallel or foreach inside foreach is not supported');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return { kind: 'foreach', source, field, binding, maxIterations, body, location: loc };
|
|
717
|
+
}
|
|
718
|
+
// --- Expressions --------------------------------------------
|
|
719
|
+
parseExpr() {
|
|
720
|
+
return this.parseNullCoalesce();
|
|
721
|
+
}
|
|
722
|
+
parseNullCoalesce() {
|
|
723
|
+
let left = this.parseLogicalOr();
|
|
724
|
+
while (this.check(TokenType.QuestionQuestion)) {
|
|
725
|
+
this.advance();
|
|
726
|
+
const right = this.parseLogicalOr();
|
|
727
|
+
left = { kind: 'binary', op: '??', left, right, location: left.location };
|
|
728
|
+
}
|
|
729
|
+
return left;
|
|
730
|
+
}
|
|
731
|
+
parseLogicalOr() {
|
|
732
|
+
let left = this.parseLogicalAnd();
|
|
733
|
+
while (this.check(TokenType.PipePipe)) {
|
|
734
|
+
this.advance();
|
|
735
|
+
const right = this.parseLogicalAnd();
|
|
736
|
+
left = { kind: 'binary', op: '||', left, right, location: left.location };
|
|
737
|
+
}
|
|
738
|
+
return left;
|
|
739
|
+
}
|
|
740
|
+
parseLogicalAnd() {
|
|
741
|
+
let left = this.parseComparison();
|
|
742
|
+
while (this.check(TokenType.AmpAmp)) {
|
|
743
|
+
this.advance();
|
|
744
|
+
const right = this.parseComparison();
|
|
745
|
+
left = { kind: 'binary', op: '&&', left, right, location: left.location };
|
|
746
|
+
}
|
|
747
|
+
return left;
|
|
748
|
+
}
|
|
749
|
+
parseComparison() {
|
|
750
|
+
let left = this.parseAdditive();
|
|
751
|
+
while (this.check(TokenType.Greater) || this.check(TokenType.Less) ||
|
|
752
|
+
this.check(TokenType.GreaterEqual) || this.check(TokenType.LessEqual) ||
|
|
753
|
+
this.check(TokenType.EqualEqual) || this.check(TokenType.BangEqual)) {
|
|
754
|
+
const opToken = this.current();
|
|
755
|
+
let op;
|
|
756
|
+
switch (opToken.type) {
|
|
757
|
+
case TokenType.Greater:
|
|
758
|
+
op = '>';
|
|
759
|
+
break;
|
|
760
|
+
case TokenType.Less:
|
|
761
|
+
op = '<';
|
|
762
|
+
break;
|
|
763
|
+
case TokenType.GreaterEqual:
|
|
764
|
+
op = '>=';
|
|
765
|
+
break;
|
|
766
|
+
case TokenType.LessEqual:
|
|
767
|
+
op = '<=';
|
|
768
|
+
break;
|
|
769
|
+
case TokenType.EqualEqual:
|
|
770
|
+
op = '==';
|
|
771
|
+
break;
|
|
772
|
+
case TokenType.BangEqual:
|
|
773
|
+
op = '!=';
|
|
774
|
+
break;
|
|
775
|
+
default: throw this.error(`Unexpected operator '${opToken.value}'`);
|
|
776
|
+
}
|
|
777
|
+
this.advance();
|
|
778
|
+
const right = this.parseAdditive();
|
|
779
|
+
left = { kind: 'binary', op, left, right, location: left.location };
|
|
780
|
+
}
|
|
781
|
+
return left;
|
|
782
|
+
}
|
|
783
|
+
parseAdditive() {
|
|
784
|
+
let left = this.parseMultiplicative();
|
|
785
|
+
while (this.check(TokenType.Plus) || this.check(TokenType.Minus)) {
|
|
786
|
+
const opToken = this.current();
|
|
787
|
+
const op = opToken.type === TokenType.Plus ? '+' : '-';
|
|
788
|
+
this.advance();
|
|
789
|
+
const right = this.parseMultiplicative();
|
|
790
|
+
left = { kind: 'binary', op, left, right, location: left.location };
|
|
791
|
+
}
|
|
792
|
+
return left;
|
|
793
|
+
}
|
|
794
|
+
parseMultiplicative() {
|
|
795
|
+
let left = this.parseUnary();
|
|
796
|
+
while (this.check(TokenType.Star) || this.check(TokenType.Percent) || this.check(TokenType.Slash)) {
|
|
797
|
+
const opToken = this.current();
|
|
798
|
+
let op;
|
|
799
|
+
switch (opToken.type) {
|
|
800
|
+
case TokenType.Star:
|
|
801
|
+
op = '*';
|
|
802
|
+
break;
|
|
803
|
+
case TokenType.Percent:
|
|
804
|
+
op = '%';
|
|
805
|
+
break;
|
|
806
|
+
case TokenType.Slash:
|
|
807
|
+
op = '/';
|
|
808
|
+
break;
|
|
809
|
+
default: throw this.error(`Unexpected operator '${opToken.value}'`);
|
|
810
|
+
}
|
|
811
|
+
this.advance();
|
|
812
|
+
const right = this.parseUnary();
|
|
813
|
+
left = { kind: 'binary', op, left, right, location: left.location };
|
|
814
|
+
}
|
|
815
|
+
return left;
|
|
816
|
+
}
|
|
817
|
+
parseUnary() {
|
|
818
|
+
if (this.check(TokenType.Minus)) {
|
|
819
|
+
const loc = this.current().location;
|
|
820
|
+
this.advance();
|
|
821
|
+
const operand = this.parseUnary();
|
|
822
|
+
return { kind: 'unary', op: '-', operand, location: loc };
|
|
823
|
+
}
|
|
824
|
+
if (this.check(TokenType.Bang)) {
|
|
825
|
+
const loc = this.current().location;
|
|
826
|
+
this.advance();
|
|
827
|
+
const operand = this.parseUnary();
|
|
828
|
+
return { kind: 'unary', op: '!', operand, location: loc };
|
|
829
|
+
}
|
|
830
|
+
return this.parsePrimary();
|
|
831
|
+
}
|
|
832
|
+
parseTemplateParts(raw, loc) {
|
|
833
|
+
const parts = [];
|
|
834
|
+
let i = 0;
|
|
835
|
+
while (i < raw.length) {
|
|
836
|
+
const dollarIdx = raw.indexOf('${', i);
|
|
837
|
+
if (dollarIdx === -1) {
|
|
838
|
+
// Rest is plain text
|
|
839
|
+
if (i < raw.length) {
|
|
840
|
+
parts.push({ kind: 'text', value: raw.slice(i) });
|
|
841
|
+
}
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
// Text before ${
|
|
845
|
+
if (dollarIdx > i) {
|
|
846
|
+
parts.push({ kind: 'text', value: raw.slice(i, dollarIdx) });
|
|
847
|
+
}
|
|
848
|
+
// Find matching } by counting brace depth
|
|
849
|
+
let depth = 1;
|
|
850
|
+
let j = dollarIdx + 2;
|
|
851
|
+
while (j < raw.length && depth > 0) {
|
|
852
|
+
if (raw[j] === '{')
|
|
853
|
+
depth++;
|
|
854
|
+
else if (raw[j] === '}')
|
|
855
|
+
depth--;
|
|
856
|
+
if (depth > 0)
|
|
857
|
+
j++;
|
|
858
|
+
}
|
|
859
|
+
if (depth !== 0) {
|
|
860
|
+
throw new GraftError('Unterminated template interpolation', loc);
|
|
861
|
+
}
|
|
862
|
+
// Parse the expression inside ${ ... }
|
|
863
|
+
const exprSource = raw.slice(dollarIdx + 2, j);
|
|
864
|
+
const innerLexer = new Lexer(exprSource);
|
|
865
|
+
const innerTokens = innerLexer.tokenize();
|
|
866
|
+
// Remove EOF token for parsing
|
|
867
|
+
const exprTokens = innerTokens.filter(t => t.type !== TokenType.EOF);
|
|
868
|
+
// Re-add EOF
|
|
869
|
+
exprTokens.push({ type: TokenType.EOF, value: '', location: loc });
|
|
870
|
+
const innerParser = new Parser(exprTokens);
|
|
871
|
+
const expr = innerParser.parseExpr();
|
|
872
|
+
parts.push({ kind: 'expr', value: expr });
|
|
873
|
+
i = j + 1; // skip past }
|
|
874
|
+
}
|
|
875
|
+
return parts;
|
|
876
|
+
}
|
|
877
|
+
parsePrimary() {
|
|
878
|
+
const token = this.current();
|
|
879
|
+
const loc = token.location;
|
|
880
|
+
// Integer literal
|
|
881
|
+
if (token.type === TokenType.IntegerLiteral) {
|
|
882
|
+
this.advance();
|
|
883
|
+
return { kind: 'literal', value: parseInt(token.value, 10), location: loc };
|
|
884
|
+
}
|
|
885
|
+
// K-integer literal
|
|
886
|
+
if (token.type === TokenType.KIntegerLiteral) {
|
|
887
|
+
this.advance();
|
|
888
|
+
return { kind: 'literal', value: parseInt(token.value, 10) * 1000, location: loc };
|
|
889
|
+
}
|
|
890
|
+
// Float literal
|
|
891
|
+
if (token.type === TokenType.FloatLiteral) {
|
|
892
|
+
this.advance();
|
|
893
|
+
return { kind: 'literal', value: parseFloat(token.value), location: loc };
|
|
894
|
+
}
|
|
895
|
+
// String literal
|
|
896
|
+
if (token.type === TokenType.StringLiteral) {
|
|
897
|
+
this.advance();
|
|
898
|
+
return { kind: 'literal', value: token.value, location: loc };
|
|
899
|
+
}
|
|
900
|
+
// Template string: "text ${expr} text"
|
|
901
|
+
if (token.type === TokenType.TemplateString) {
|
|
902
|
+
this.advance();
|
|
903
|
+
const parts = this.parseTemplateParts(token.value, loc);
|
|
904
|
+
return { kind: 'template', parts, location: loc };
|
|
905
|
+
}
|
|
906
|
+
// Boolean literals
|
|
907
|
+
if (token.type === TokenType.True) {
|
|
908
|
+
this.advance();
|
|
909
|
+
return { kind: 'literal', value: true, location: loc };
|
|
910
|
+
}
|
|
911
|
+
if (token.type === TokenType.False) {
|
|
912
|
+
this.advance();
|
|
913
|
+
return { kind: 'literal', value: false, location: loc };
|
|
914
|
+
}
|
|
915
|
+
// Grouped expression
|
|
916
|
+
if (token.type === TokenType.LParen) {
|
|
917
|
+
this.advance();
|
|
918
|
+
const inner = this.parseExpr();
|
|
919
|
+
this.expect(TokenType.RParen);
|
|
920
|
+
return { kind: 'group', inner, location: loc };
|
|
921
|
+
}
|
|
922
|
+
// Built-in function call: len(...), max(...), min(...), str(...)
|
|
923
|
+
if (token.type === TokenType.Identifier && token.value in BUILTIN_FUNCTIONS && this.peekType(1) === TokenType.LParen) {
|
|
924
|
+
const name = token.value;
|
|
925
|
+
this.advance(); // consume function name
|
|
926
|
+
this.advance(); // consume LParen
|
|
927
|
+
const args = [];
|
|
928
|
+
if (!this.check(TokenType.RParen)) {
|
|
929
|
+
args.push(this.parseExpr());
|
|
930
|
+
while (this.check(TokenType.Comma)) {
|
|
931
|
+
this.advance();
|
|
932
|
+
args.push(this.parseExpr());
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
this.expect(TokenType.RParen);
|
|
936
|
+
return { kind: 'call', name, args, location: loc };
|
|
937
|
+
}
|
|
938
|
+
// Conditional expression: if <expr> then <expr> else <expr>
|
|
939
|
+
if (token.type === TokenType.If) {
|
|
940
|
+
this.advance();
|
|
941
|
+
const condition = this.parseExpr();
|
|
942
|
+
this.expect(TokenType.Then);
|
|
943
|
+
const consequent = this.parseExpr();
|
|
944
|
+
this.expect(TokenType.Else);
|
|
945
|
+
const alternate = this.parseExpr();
|
|
946
|
+
return { kind: 'conditional', condition, consequent, alternate, location: loc };
|
|
947
|
+
}
|
|
948
|
+
// Field access: identifier (or keyword) followed by optional dots
|
|
949
|
+
if (token.type === TokenType.Identifier || KEYWORD_TYPES.has(token.type)) {
|
|
950
|
+
this.advance();
|
|
951
|
+
const segments = [token.value];
|
|
952
|
+
while (this.check(TokenType.Dot)) {
|
|
953
|
+
this.advance();
|
|
954
|
+
const seg = this.expectIdentifierOrKeyword();
|
|
955
|
+
segments.push(seg);
|
|
956
|
+
}
|
|
957
|
+
return { kind: 'field_access', segments, location: loc };
|
|
958
|
+
}
|
|
959
|
+
throw this.error(`Expected expression, got '${token.value}' (${token.type})`);
|
|
960
|
+
}
|
|
961
|
+
// --- Types --------------------------------------------------
|
|
962
|
+
parseType() {
|
|
963
|
+
const token = this.current();
|
|
964
|
+
// List<T>
|
|
965
|
+
if (token.type === TokenType.List) {
|
|
966
|
+
this.advance();
|
|
967
|
+
this.expect(TokenType.Less);
|
|
968
|
+
const element = this.parseTypeOrInlineStruct();
|
|
969
|
+
this.expect(TokenType.Greater);
|
|
970
|
+
return { kind: 'list', element };
|
|
971
|
+
}
|
|
972
|
+
// Map<K, V>
|
|
973
|
+
if (token.type === TokenType.Map) {
|
|
974
|
+
this.advance();
|
|
975
|
+
this.expect(TokenType.Less);
|
|
976
|
+
const key = this.parseType();
|
|
977
|
+
this.expect(TokenType.Comma);
|
|
978
|
+
const value = this.parseType();
|
|
979
|
+
this.expect(TokenType.Greater);
|
|
980
|
+
return { kind: 'map', key, value };
|
|
981
|
+
}
|
|
982
|
+
// Optional<T>
|
|
983
|
+
if (token.type === TokenType.Optional) {
|
|
984
|
+
this.advance();
|
|
985
|
+
this.expect(TokenType.Less);
|
|
986
|
+
const inner = this.parseTypeOrInlineStruct();
|
|
987
|
+
this.expect(TokenType.Greater);
|
|
988
|
+
return { kind: 'optional', inner };
|
|
989
|
+
}
|
|
990
|
+
// TokenBounded<T, max>
|
|
991
|
+
if (token.type === TokenType.TokenBounded) {
|
|
992
|
+
this.advance();
|
|
993
|
+
this.expect(TokenType.Less);
|
|
994
|
+
const inner = this.parseType();
|
|
995
|
+
this.expect(TokenType.Comma);
|
|
996
|
+
const max = this.parseIntValue();
|
|
997
|
+
this.expect(TokenType.Greater);
|
|
998
|
+
return { kind: 'token_bounded', inner, max };
|
|
999
|
+
}
|
|
1000
|
+
// Float -- could be Float or Float(min..max)
|
|
1001
|
+
if (token.type === TokenType.Float) {
|
|
1002
|
+
this.advance();
|
|
1003
|
+
if (this.check(TokenType.LParen)) {
|
|
1004
|
+
this.advance();
|
|
1005
|
+
const min = this.parseNumericValue();
|
|
1006
|
+
this.expect(TokenType.DotDot);
|
|
1007
|
+
const max = this.parseNumericValue();
|
|
1008
|
+
this.expect(TokenType.RParen);
|
|
1009
|
+
return { kind: 'primitive_range', name: 'Float', min, max };
|
|
1010
|
+
}
|
|
1011
|
+
return { kind: 'primitive', name: 'Float' };
|
|
1012
|
+
}
|
|
1013
|
+
// enum(val1, val2, ...)
|
|
1014
|
+
if (token.type === TokenType.Enum) {
|
|
1015
|
+
this.advance();
|
|
1016
|
+
this.expect(TokenType.LParen);
|
|
1017
|
+
const values = [];
|
|
1018
|
+
values.push(this.expectIdentifierOrKeyword());
|
|
1019
|
+
while (this.check(TokenType.Comma)) {
|
|
1020
|
+
this.advance();
|
|
1021
|
+
values.push(this.expectIdentifierOrKeyword());
|
|
1022
|
+
}
|
|
1023
|
+
this.expect(TokenType.RParen);
|
|
1024
|
+
return { kind: 'enum', values };
|
|
1025
|
+
}
|
|
1026
|
+
// Primitives
|
|
1027
|
+
if (token.type === TokenType.String) {
|
|
1028
|
+
this.advance();
|
|
1029
|
+
return { kind: 'primitive', name: 'String' };
|
|
1030
|
+
}
|
|
1031
|
+
if (token.type === TokenType.Int) {
|
|
1032
|
+
this.advance();
|
|
1033
|
+
return { kind: 'primitive', name: 'Int' };
|
|
1034
|
+
}
|
|
1035
|
+
if (token.type === TokenType.Bool) {
|
|
1036
|
+
this.advance();
|
|
1037
|
+
return { kind: 'primitive', name: 'Bool' };
|
|
1038
|
+
}
|
|
1039
|
+
// Domain types
|
|
1040
|
+
if (token.type === TokenType.FilePath) {
|
|
1041
|
+
this.advance();
|
|
1042
|
+
return { kind: 'domain', name: 'FilePath' };
|
|
1043
|
+
}
|
|
1044
|
+
if (token.type === TokenType.FileDiff) {
|
|
1045
|
+
this.advance();
|
|
1046
|
+
return { kind: 'domain', name: 'FileDiff' };
|
|
1047
|
+
}
|
|
1048
|
+
if (token.type === TokenType.TestFile) {
|
|
1049
|
+
this.advance();
|
|
1050
|
+
return { kind: 'domain', name: 'TestFile' };
|
|
1051
|
+
}
|
|
1052
|
+
if (token.type === TokenType.IssueRef) {
|
|
1053
|
+
this.advance();
|
|
1054
|
+
return { kind: 'domain', name: 'IssueRef' };
|
|
1055
|
+
}
|
|
1056
|
+
throw this.error(`Expected type, got '${token.value}'`);
|
|
1057
|
+
}
|
|
1058
|
+
// Parses a type that could be an inline struct: Name { fields }
|
|
1059
|
+
parseTypeOrInlineStruct() {
|
|
1060
|
+
// Check for Identifier followed by '{' -- inline struct
|
|
1061
|
+
if (this.current().type === TokenType.Identifier && this.peekType(1) === TokenType.LBrace) {
|
|
1062
|
+
const name = this.expectIdentifier();
|
|
1063
|
+
this.expect(TokenType.LBrace);
|
|
1064
|
+
const fields = this.parseFields();
|
|
1065
|
+
this.expect(TokenType.RBrace);
|
|
1066
|
+
return { kind: 'struct', name, fields };
|
|
1067
|
+
}
|
|
1068
|
+
return this.parseType();
|
|
1069
|
+
}
|
|
1070
|
+
// --- Fields -------------------------------------------------
|
|
1071
|
+
parseFields() {
|
|
1072
|
+
const fields = [];
|
|
1073
|
+
while (!this.check(TokenType.RBrace)) {
|
|
1074
|
+
const loc = this.current().location;
|
|
1075
|
+
const name = this.expectIdentifierOrKeyword();
|
|
1076
|
+
this.expect(TokenType.Colon);
|
|
1077
|
+
const type = this.parseTypeOrInlineStruct();
|
|
1078
|
+
fields.push({ name, type, location: loc });
|
|
1079
|
+
}
|
|
1080
|
+
return fields;
|
|
1081
|
+
}
|
|
1082
|
+
// --- Helpers ------------------------------------------------
|
|
1083
|
+
parseTokenValue() {
|
|
1084
|
+
const token = this.current();
|
|
1085
|
+
if (token.type === TokenType.IntegerLiteral) {
|
|
1086
|
+
this.advance();
|
|
1087
|
+
return parseInt(token.value, 10);
|
|
1088
|
+
}
|
|
1089
|
+
if (token.type === TokenType.KIntegerLiteral) {
|
|
1090
|
+
this.advance();
|
|
1091
|
+
// parseInt("5k", 10) returns 5 per the JS spec: parseInt stops at first non-digit.
|
|
1092
|
+
return parseInt(token.value, 10) * 1000;
|
|
1093
|
+
}
|
|
1094
|
+
throw this.error(`Expected integer or k-integer, got '${token.value}'`);
|
|
1095
|
+
}
|
|
1096
|
+
parseIntValue() {
|
|
1097
|
+
const token = this.current();
|
|
1098
|
+
if (token.type === TokenType.IntegerLiteral) {
|
|
1099
|
+
this.advance();
|
|
1100
|
+
return parseInt(token.value, 10);
|
|
1101
|
+
}
|
|
1102
|
+
throw this.error(`Expected integer, got '${token.value}'`);
|
|
1103
|
+
}
|
|
1104
|
+
parseNumericValue() {
|
|
1105
|
+
const token = this.current();
|
|
1106
|
+
if (token.type === TokenType.IntegerLiteral) {
|
|
1107
|
+
this.advance();
|
|
1108
|
+
return parseInt(token.value, 10);
|
|
1109
|
+
}
|
|
1110
|
+
if (token.type === TokenType.FloatLiteral) {
|
|
1111
|
+
this.advance();
|
|
1112
|
+
return parseFloat(token.value);
|
|
1113
|
+
}
|
|
1114
|
+
throw this.error(`Expected number, got '${token.value}'`);
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Strict identifier: only accepts TokenType.Identifier.
|
|
1118
|
+
* Used for declaration names (context, node, edge, graph), produces names,
|
|
1119
|
+
* context ref context-part, graph flow nodes, edge source/target.
|
|
1120
|
+
* These are PascalCase by convention and must not collide with keywords.
|
|
1121
|
+
*/
|
|
1122
|
+
expectIdentifier() {
|
|
1123
|
+
const token = this.current();
|
|
1124
|
+
if (token.type === TokenType.Identifier) {
|
|
1125
|
+
this.advance();
|
|
1126
|
+
return token.value;
|
|
1127
|
+
}
|
|
1128
|
+
throw this.error(`Expected identifier, got '${token.value}' (${token.type})`);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Permissive identifier: accepts TokenType.Identifier OR any keyword token.
|
|
1132
|
+
* Used for field names, tool names, enum values, transform field arguments,
|
|
1133
|
+
* condition field names -- positions where a keyword-like word is valid as a name.
|
|
1134
|
+
*/
|
|
1135
|
+
expectIdentifierOrKeyword() {
|
|
1136
|
+
const token = this.current();
|
|
1137
|
+
if (token.type === TokenType.Identifier || KEYWORD_TYPES.has(token.type)) {
|
|
1138
|
+
this.advance();
|
|
1139
|
+
return token.value;
|
|
1140
|
+
}
|
|
1141
|
+
throw this.error(`Expected identifier, got '${token.value}' (${token.type})`);
|
|
1142
|
+
}
|
|
1143
|
+
expect(type) {
|
|
1144
|
+
const token = this.current();
|
|
1145
|
+
if (token.type !== type) {
|
|
1146
|
+
throw this.error(`Expected '${type}', got '${token.value}' (${token.type})`, 'PARSE_UNEXPECTED_TOKEN');
|
|
1147
|
+
}
|
|
1148
|
+
this.advance();
|
|
1149
|
+
return token;
|
|
1150
|
+
}
|
|
1151
|
+
check(type) {
|
|
1152
|
+
return this.current().type === type;
|
|
1153
|
+
}
|
|
1154
|
+
current() {
|
|
1155
|
+
return this.tokens[this.pos];
|
|
1156
|
+
}
|
|
1157
|
+
advance() {
|
|
1158
|
+
const token = this.tokens[this.pos];
|
|
1159
|
+
if (!this.isAtEnd())
|
|
1160
|
+
this.pos++;
|
|
1161
|
+
return token;
|
|
1162
|
+
}
|
|
1163
|
+
peekType(offset) {
|
|
1164
|
+
const idx = this.pos + offset;
|
|
1165
|
+
if (idx < this.tokens.length)
|
|
1166
|
+
return this.tokens[idx].type;
|
|
1167
|
+
return undefined;
|
|
1168
|
+
}
|
|
1169
|
+
isAtEnd() {
|
|
1170
|
+
return this.current().type === TokenType.EOF;
|
|
1171
|
+
}
|
|
1172
|
+
error(message, code = 'PARSE_UNEXPECTED_TOKEN') {
|
|
1173
|
+
return new GraftError(message, this.current().location, 'error', code);
|
|
1174
|
+
}
|
|
1175
|
+
}
|