flow-lang 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.
@@ -0,0 +1,942 @@
1
+ import { TokenType, } from "../types/index.js";
2
+ import { createError } from "../errors/index.js";
3
+ // ============================================================
4
+ // Parser error class
5
+ // ============================================================
6
+ export class ParserError extends Error {
7
+ flowError;
8
+ constructor(flowError) {
9
+ super(flowError.message);
10
+ this.name = "ParserError";
11
+ this.flowError = flowError;
12
+ }
13
+ }
14
+ // ============================================================
15
+ // Keyword sets for dispatch
16
+ // ============================================================
17
+ const COMPARISON_OPERATORS = new Set([
18
+ "is", "is not", "is above", "is below",
19
+ "is at least", "is at most", "contains",
20
+ "is empty", "is not empty", "exists", "does not exist",
21
+ ]);
22
+ const UNARY_COMPARISON_OPERATORS = new Set([
23
+ "is empty", "is not empty", "exists", "does not exist",
24
+ ]);
25
+ const MATH_OPERATORS = new Set([
26
+ "plus", "minus", "times", "divided by", "rounded to",
27
+ ]);
28
+ // ============================================================
29
+ // Parser
30
+ // ============================================================
31
+ export function parse(tokens, source, fileName = "<input>") {
32
+ let pos = 0;
33
+ const errors = [];
34
+ // --------------------------------------------------------
35
+ // Token navigation
36
+ // --------------------------------------------------------
37
+ function current() {
38
+ return tokens[pos] ?? { type: TokenType.EOF, value: "", line: 0, column: 0 };
39
+ }
40
+ function peek() {
41
+ return tokens[pos] ?? { type: TokenType.EOF, value: "", line: 0, column: 0 };
42
+ }
43
+ function peekNext() {
44
+ return tokens[pos + 1] ?? { type: TokenType.EOF, value: "", line: 0, column: 0 };
45
+ }
46
+ function advance() {
47
+ const tok = current();
48
+ if (tok.type !== TokenType.EOF) {
49
+ pos++;
50
+ }
51
+ return tok;
52
+ }
53
+ function loc() {
54
+ const tok = current();
55
+ return { line: tok.line, column: tok.column };
56
+ }
57
+ function atEnd() {
58
+ return current().type === TokenType.EOF;
59
+ }
60
+ function check(type, value) {
61
+ const tok = current();
62
+ if (tok.type !== type)
63
+ return false;
64
+ if (value !== undefined && tok.value !== value)
65
+ return false;
66
+ return true;
67
+ }
68
+ function match(type, value) {
69
+ if (check(type, value)) {
70
+ return advance();
71
+ }
72
+ return null;
73
+ }
74
+ function expect(type, value, context) {
75
+ const tok = current();
76
+ if (tok.type === type && (value === undefined || tok.value === value)) {
77
+ return advance();
78
+ }
79
+ const expected = value ? `"${value}"` : type;
80
+ const ctx = context ? ` ${context}` : "";
81
+ addError(tok, `Expected ${expected}${ctx}, but found "${tok.value}" (${tok.type}).`);
82
+ // Return a synthetic token so parsing can continue
83
+ return { type, value: value ?? "", line: tok.line, column: tok.column };
84
+ }
85
+ // --------------------------------------------------------
86
+ // Error handling
87
+ // --------------------------------------------------------
88
+ function addError(tok, message, suggestion, hint) {
89
+ errors.push(createError(fileName, tok.line, tok.column, message, source, { suggestion, hint }));
90
+ }
91
+ function skipToNextStatement() {
92
+ // Skip tokens until we reach a NEWLINE at the current or lower indent level,
93
+ // or DEDENT, or EOF
94
+ while (!atEnd()) {
95
+ const tok = current();
96
+ if (tok.type === TokenType.NEWLINE || tok.type === TokenType.DEDENT || tok.type === TokenType.EOF) {
97
+ return;
98
+ }
99
+ advance();
100
+ }
101
+ }
102
+ // --------------------------------------------------------
103
+ // Newline consumption
104
+ // --------------------------------------------------------
105
+ function skipNewlines() {
106
+ while (match(TokenType.NEWLINE)) { /* skip */ }
107
+ }
108
+ function expectNewline() {
109
+ if (!match(TokenType.NEWLINE) && !atEnd() && !check(TokenType.DEDENT)) {
110
+ addError(current(), "Expected end of line.");
111
+ skipToNextStatement();
112
+ }
113
+ }
114
+ // --------------------------------------------------------
115
+ // Top-level: Program
116
+ // --------------------------------------------------------
117
+ function parseProgram() {
118
+ const location = loc();
119
+ let config = null;
120
+ let services = null;
121
+ let workflow = null;
122
+ skipNewlines();
123
+ while (!atEnd()) {
124
+ const tok = current();
125
+ if (tok.type === TokenType.KEYWORD && tok.value === "config") {
126
+ if (config !== null) {
127
+ addError(tok, "Duplicate config block. You can only have one config block per file.");
128
+ }
129
+ config = parseConfigBlock();
130
+ }
131
+ else if (tok.type === TokenType.KEYWORD && tok.value === "services") {
132
+ if (services !== null) {
133
+ addError(tok, "Duplicate services block. You can only have one services block per file.");
134
+ }
135
+ services = parseServicesBlock();
136
+ }
137
+ else if (tok.type === TokenType.KEYWORD && tok.value === "workflow") {
138
+ if (workflow !== null) {
139
+ addError(tok, "Duplicate workflow block. You can only have one workflow block per file.");
140
+ }
141
+ workflow = parseWorkflowBlock();
142
+ }
143
+ else if (tok.type === TokenType.NEWLINE || tok.type === TokenType.INDENT || tok.type === TokenType.DEDENT) {
144
+ advance();
145
+ }
146
+ else {
147
+ addError(tok, `I found "${tok.value}" at the top level, but Flow files can only have "config:", "services:", or "workflow:" blocks at the top level.`, "Check your indentation — this might be a statement that should be inside a block.", 'A Flow file looks like:\n config:\n ...\n services:\n ...\n workflow:\n ...');
148
+ skipToNextStatement();
149
+ skipNewlines();
150
+ }
151
+ }
152
+ return { kind: "Program", config, services, workflow, loc: location };
153
+ }
154
+ // --------------------------------------------------------
155
+ // Config block
156
+ // --------------------------------------------------------
157
+ function parseConfigBlock() {
158
+ const location = loc();
159
+ expect(TokenType.KEYWORD, "config");
160
+ expect(TokenType.COLON);
161
+ expectNewline();
162
+ const entries = [];
163
+ if (!match(TokenType.INDENT)) {
164
+ addError(current(), "Expected indented config entries after \"config:\".", "Add your config settings indented under config:", 'Example:\n config:\n name: "My Workflow"\n version: 1');
165
+ return { kind: "ConfigBlock", entries, loc: location };
166
+ }
167
+ while (!check(TokenType.DEDENT) && !atEnd()) {
168
+ skipNewlines();
169
+ if (check(TokenType.DEDENT) || atEnd())
170
+ break;
171
+ const entry = parseConfigEntry();
172
+ if (entry)
173
+ entries.push(entry);
174
+ skipNewlines();
175
+ }
176
+ match(TokenType.DEDENT);
177
+ return { kind: "ConfigBlock", entries, loc: location };
178
+ }
179
+ function parseConfigEntry() {
180
+ const location = loc();
181
+ const keyToken = current();
182
+ if (keyToken.type !== TokenType.KEYWORD && keyToken.type !== TokenType.IDENTIFIER) {
183
+ addError(keyToken, `Expected a config key (like "name", "version", or "timeout"), but found "${keyToken.value}".`);
184
+ skipToNextStatement();
185
+ return null;
186
+ }
187
+ const key = advance().value;
188
+ expect(TokenType.COLON, undefined, "after config key");
189
+ // Value can be a string, number, or number + unit (like "5 minutes")
190
+ const valTok = current();
191
+ let value;
192
+ if (valTok.type === TokenType.STRING) {
193
+ value = advance().value;
194
+ }
195
+ else if (valTok.type === TokenType.NUMBER) {
196
+ const numStr = advance().value;
197
+ // Check if there's a unit after the number (e.g., "5 minutes")
198
+ if (!check(TokenType.NEWLINE) && !check(TokenType.DEDENT) && !atEnd()) {
199
+ let text = numStr;
200
+ while (!check(TokenType.NEWLINE) && !check(TokenType.DEDENT) && !atEnd()) {
201
+ text += " " + advance().value;
202
+ }
203
+ value = text;
204
+ }
205
+ else {
206
+ value = Number(numStr);
207
+ }
208
+ }
209
+ else if (valTok.type === TokenType.IDENTIFIER || valTok.type === TokenType.KEYWORD) {
210
+ // Could be something like "5 minutes" — collect rest of line as string
211
+ let text = "";
212
+ while (!check(TokenType.NEWLINE) && !check(TokenType.DEDENT) && !atEnd()) {
213
+ if (text.length > 0)
214
+ text += " ";
215
+ text += advance().value;
216
+ }
217
+ value = text;
218
+ }
219
+ else {
220
+ addError(valTok, `Expected a value after "${key}:", but found "${valTok.value}".`);
221
+ skipToNextStatement();
222
+ return null;
223
+ }
224
+ expectNewline();
225
+ return { kind: "ConfigEntry", key, value, loc: location };
226
+ }
227
+ // --------------------------------------------------------
228
+ // Services block
229
+ // --------------------------------------------------------
230
+ function parseServicesBlock() {
231
+ const location = loc();
232
+ expect(TokenType.KEYWORD, "services");
233
+ expect(TokenType.COLON);
234
+ expectNewline();
235
+ const declarations = [];
236
+ if (!match(TokenType.INDENT)) {
237
+ addError(current(), "Expected indented service declarations after \"services:\".", "Add your services indented under services:", 'Example:\n services:\n MyAPI is an API at "https://..."');
238
+ return { kind: "ServicesBlock", declarations, loc: location };
239
+ }
240
+ while (!check(TokenType.DEDENT) && !atEnd()) {
241
+ skipNewlines();
242
+ if (check(TokenType.DEDENT) || atEnd())
243
+ break;
244
+ const decl = parseServiceDeclaration();
245
+ if (decl)
246
+ declarations.push(decl);
247
+ skipNewlines();
248
+ }
249
+ match(TokenType.DEDENT);
250
+ return { kind: "ServicesBlock", declarations, loc: location };
251
+ }
252
+ function parseServiceDeclaration() {
253
+ const location = loc();
254
+ const nameTok = current();
255
+ if (nameTok.type !== TokenType.IDENTIFIER) {
256
+ addError(nameTok, `Expected a service name (like "Stripe" or "EmailVerifier"), but found "${nameTok.value}".`, "Service names should start with a capital letter.", 'Example: Stripe is a plugin "flow-connector-stripe"');
257
+ skipToNextStatement();
258
+ return null;
259
+ }
260
+ const name = advance().value;
261
+ // Expect "is"
262
+ if (!match(TokenType.KEYWORD, "is")) {
263
+ addError(current(), `Expected "is" after service name "${name}".`, undefined, `Example: ${name} is an API at "https://..."`);
264
+ skipToNextStatement();
265
+ return null;
266
+ }
267
+ // Determine service type: "an API at", "an AI using", "a plugin", "a webhook at"
268
+ let serviceType;
269
+ const article = current();
270
+ // "an" is an identifier (not a keyword)
271
+ if (article.value === "an") {
272
+ advance();
273
+ const typeTok = current();
274
+ if (typeTok.type === TokenType.IDENTIFIER && typeTok.value === "API") {
275
+ advance();
276
+ serviceType = "api";
277
+ expect(TokenType.KEYWORD, "at", "after \"API\"");
278
+ }
279
+ else if (typeTok.type === TokenType.IDENTIFIER && typeTok.value === "AI") {
280
+ advance();
281
+ serviceType = "ai";
282
+ expect(TokenType.KEYWORD, "using", "after \"AI\"");
283
+ }
284
+ else {
285
+ addError(typeTok, `Expected "API" or "AI" after "is an", but found "${typeTok.value}".`, undefined, `Valid service types:\n ${name} is an API at "https://..."\n ${name} is an AI using "model-name"`);
286
+ skipToNextStatement();
287
+ return null;
288
+ }
289
+ }
290
+ else if (article.value === "a") {
291
+ advance();
292
+ const typeTok = current();
293
+ if (typeTok.type === TokenType.IDENTIFIER && typeTok.value === "plugin") {
294
+ advance();
295
+ serviceType = "plugin";
296
+ }
297
+ else if (typeTok.type === TokenType.IDENTIFIER && typeTok.value === "webhook") {
298
+ advance();
299
+ serviceType = "webhook";
300
+ expect(TokenType.KEYWORD, "at", "after \"webhook\"");
301
+ }
302
+ else {
303
+ addError(typeTok, `Expected "plugin" or "webhook" after "is a", but found "${typeTok.value}".`, undefined, `Valid service types:\n ${name} is a plugin "package-name"\n ${name} is a webhook at "/path"`);
304
+ skipToNextStatement();
305
+ return null;
306
+ }
307
+ }
308
+ else {
309
+ addError(article, `Expected "an" or "a" after "is" in service declaration, but found "${article.value}".`, undefined, `Valid service types:\n ${name} is an API at "https://..."\n ${name} is an AI using "model-name"\n ${name} is a plugin "package-name"\n ${name} is a webhook at "/path"`);
310
+ skipToNextStatement();
311
+ return null;
312
+ }
313
+ // Parse the target (URL, model name, package name, or path)
314
+ const targetTok = current();
315
+ if (targetTok.type !== TokenType.STRING) {
316
+ addError(targetTok, `Expected a quoted string for the service target, but found "${targetTok.value}".`, 'Wrap the value in double quotes.', `Example: ${name} is ${serviceType === "api" ? 'an API at' : serviceType === "ai" ? 'an AI using' : serviceType === "plugin" ? 'a plugin' : 'a webhook at'} "value-here"`);
317
+ skipToNextStatement();
318
+ return null;
319
+ }
320
+ const target = advance().value;
321
+ expectNewline();
322
+ return { kind: "ServiceDeclaration", name, serviceType, target, loc: location };
323
+ }
324
+ // --------------------------------------------------------
325
+ // Workflow block
326
+ // --------------------------------------------------------
327
+ function parseWorkflowBlock() {
328
+ const location = loc();
329
+ expect(TokenType.KEYWORD, "workflow");
330
+ expect(TokenType.COLON);
331
+ expectNewline();
332
+ let trigger = null;
333
+ const body = [];
334
+ if (!match(TokenType.INDENT)) {
335
+ addError(current(), "Expected indented workflow content after \"workflow:\".");
336
+ return { kind: "WorkflowBlock", trigger, body, loc: location };
337
+ }
338
+ while (!check(TokenType.DEDENT) && !atEnd()) {
339
+ skipNewlines();
340
+ if (check(TokenType.DEDENT) || atEnd())
341
+ break;
342
+ // Check for trigger
343
+ if (check(TokenType.KEYWORD, "trigger")) {
344
+ trigger = parseTriggerDeclaration();
345
+ continue;
346
+ }
347
+ const stmt = parseStatement();
348
+ if (stmt)
349
+ body.push(stmt);
350
+ skipNewlines();
351
+ }
352
+ match(TokenType.DEDENT);
353
+ return { kind: "WorkflowBlock", trigger, body, loc: location };
354
+ }
355
+ function parseTriggerDeclaration() {
356
+ const location = loc();
357
+ expect(TokenType.KEYWORD, "trigger");
358
+ expect(TokenType.COLON);
359
+ // Collect the rest of the line as the trigger description
360
+ let description = "";
361
+ while (!check(TokenType.NEWLINE) && !check(TokenType.DEDENT) && !atEnd()) {
362
+ if (description.length > 0)
363
+ description += " ";
364
+ description += advance().value;
365
+ }
366
+ expectNewline();
367
+ return { kind: "TriggerDeclaration", description, loc: location };
368
+ }
369
+ // --------------------------------------------------------
370
+ // Statement dispatch
371
+ // --------------------------------------------------------
372
+ function parseStatement() {
373
+ const tok = current();
374
+ try {
375
+ // Named step block
376
+ if (tok.type === TokenType.KEYWORD && tok.value === "step") {
377
+ return parseStepBlock();
378
+ }
379
+ // If / otherwise
380
+ if (tok.type === TokenType.KEYWORD && tok.value === "if") {
381
+ return parseIfStatement();
382
+ }
383
+ // For each
384
+ if (tok.type === TokenType.KEYWORD_COMPOUND && tok.value === "for each") {
385
+ return parseForEachStatement();
386
+ }
387
+ // Set variable
388
+ if (tok.type === TokenType.KEYWORD && tok.value === "set") {
389
+ return parseSetStatement();
390
+ }
391
+ // Ask AI agent
392
+ if (tok.type === TokenType.KEYWORD && tok.value === "ask") {
393
+ return parseAskStatement();
394
+ }
395
+ // Complete
396
+ if (tok.type === TokenType.KEYWORD && tok.value === "complete") {
397
+ return parseCompleteStatement();
398
+ }
399
+ // Reject
400
+ if (tok.type === TokenType.KEYWORD && tok.value === "reject") {
401
+ return parseRejectStatement();
402
+ }
403
+ // Log
404
+ if (tok.type === TokenType.KEYWORD && tok.value === "log") {
405
+ return parseLogStatement();
406
+ }
407
+ // Default: service call (verb + description + using + service)
408
+ if (tok.type === TokenType.IDENTIFIER || tok.type === TokenType.KEYWORD) {
409
+ return parseServiceCall();
410
+ }
411
+ addError(tok, `I don't understand "${tok.value}" here. Expected a statement like "set", "if", "log", "step", etc.`);
412
+ skipToNextStatement();
413
+ skipNewlines();
414
+ return null;
415
+ }
416
+ catch (e) {
417
+ if (e instanceof ParserError) {
418
+ errors.push(e.flowError);
419
+ skipToNextStatement();
420
+ skipNewlines();
421
+ return null;
422
+ }
423
+ throw e;
424
+ }
425
+ }
426
+ // --------------------------------------------------------
427
+ // Step block
428
+ // --------------------------------------------------------
429
+ function parseStepBlock() {
430
+ const location = loc();
431
+ expect(TokenType.KEYWORD, "step");
432
+ // Collect step name (can be multiple words)
433
+ let name = "";
434
+ while (!check(TokenType.COLON) && !check(TokenType.NEWLINE) && !atEnd()) {
435
+ if (name.length > 0)
436
+ name += " ";
437
+ name += advance().value;
438
+ }
439
+ if (name.length === 0) {
440
+ addError(current(), "Expected a name for this step.", undefined, 'Example: step Verify Email:');
441
+ }
442
+ expect(TokenType.COLON, undefined, "after step name");
443
+ expectNewline();
444
+ const body = parseBlock();
445
+ return { kind: "StepBlock", name, body, loc: location };
446
+ }
447
+ // --------------------------------------------------------
448
+ // If / otherwise if / otherwise
449
+ // --------------------------------------------------------
450
+ function parseIfStatement() {
451
+ const location = loc();
452
+ expect(TokenType.KEYWORD, "if");
453
+ const condition = parseConditionExpression();
454
+ expect(TokenType.COLON, undefined, "after condition");
455
+ expectNewline();
456
+ const body = parseBlock();
457
+ const otherwiseIfs = [];
458
+ let otherwise = null;
459
+ // Check for "otherwise if" chains
460
+ while (check(TokenType.KEYWORD_COMPOUND, "otherwise if")) {
461
+ const oiLoc = loc();
462
+ advance(); // consume "otherwise if"
463
+ const oiCondition = parseConditionExpression();
464
+ expect(TokenType.COLON, undefined, "after condition");
465
+ expectNewline();
466
+ const oiBody = parseBlock();
467
+ otherwiseIfs.push({ kind: "OtherwiseIf", condition: oiCondition, body: oiBody, loc: oiLoc });
468
+ }
469
+ // Check for "otherwise"
470
+ if (check(TokenType.KEYWORD, "otherwise")) {
471
+ advance();
472
+ expect(TokenType.COLON);
473
+ expectNewline();
474
+ otherwise = parseBlock();
475
+ }
476
+ return { kind: "IfStatement", condition, body, otherwiseIfs, otherwise, loc: location };
477
+ }
478
+ // --------------------------------------------------------
479
+ // For each
480
+ // --------------------------------------------------------
481
+ function parseForEachStatement() {
482
+ const location = loc();
483
+ expect(TokenType.KEYWORD_COMPOUND, "for each");
484
+ const itemTok = current();
485
+ if (itemTok.type !== TokenType.IDENTIFIER) {
486
+ addError(itemTok, `Expected a variable name after "for each", but found "${itemTok.value}".`, undefined, 'Example: for each item in order.items:');
487
+ skipToNextStatement();
488
+ return { kind: "ForEachStatement", itemName: "_error", collection: makeErrorExpr(), body: [], loc: location };
489
+ }
490
+ const itemName = advance().value;
491
+ expect(TokenType.KEYWORD, "in", "after loop variable name");
492
+ const collection = parseAtomExpression();
493
+ expect(TokenType.COLON, undefined, "after collection");
494
+ expectNewline();
495
+ const body = parseBlock();
496
+ return { kind: "ForEachStatement", itemName, collection, body, loc: location };
497
+ }
498
+ // --------------------------------------------------------
499
+ // Set statement
500
+ // --------------------------------------------------------
501
+ function parseSetStatement() {
502
+ const location = loc();
503
+ expect(TokenType.KEYWORD, "set");
504
+ const varTok = current();
505
+ if (varTok.type !== TokenType.IDENTIFIER && varTok.type !== TokenType.KEYWORD) {
506
+ addError(varTok, `Expected a variable name after "set", but found "${varTok.value}".`, undefined, 'Example: set greeting to "Hello"');
507
+ }
508
+ const variable = advance().value;
509
+ expect(TokenType.KEYWORD, "to", "after variable name");
510
+ const value = parseExpression();
511
+ expectNewline();
512
+ return { kind: "SetStatement", variable, value, loc: location };
513
+ }
514
+ // --------------------------------------------------------
515
+ // Ask statement
516
+ // --------------------------------------------------------
517
+ function parseAskStatement() {
518
+ const location = loc();
519
+ expect(TokenType.KEYWORD, "ask");
520
+ const agentTok = current();
521
+ const agent = advance().value;
522
+ expect(TokenType.KEYWORD, "to", "after agent name");
523
+ // Collect the instruction (rest of the line)
524
+ let instruction = "";
525
+ while (!check(TokenType.NEWLINE) && !check(TokenType.DEDENT) && !atEnd()) {
526
+ if (instruction.length > 0)
527
+ instruction += " ";
528
+ instruction += advance().value;
529
+ }
530
+ expectNewline();
531
+ // Check for indented save directives
532
+ let resultVar = null;
533
+ let confidenceVar = null;
534
+ if (match(TokenType.INDENT)) {
535
+ while (!check(TokenType.DEDENT) && !atEnd()) {
536
+ skipNewlines();
537
+ if (check(TokenType.DEDENT) || atEnd())
538
+ break;
539
+ if (check(TokenType.KEYWORD_COMPOUND, "save the result as")) {
540
+ advance();
541
+ resultVar = advance().value;
542
+ expectNewline();
543
+ }
544
+ else if (check(TokenType.KEYWORD_COMPOUND, "save the confidence as")) {
545
+ advance();
546
+ confidenceVar = advance().value;
547
+ expectNewline();
548
+ }
549
+ else {
550
+ break;
551
+ }
552
+ }
553
+ match(TokenType.DEDENT);
554
+ }
555
+ return { kind: "AskStatement", agent, instruction, resultVar, confidenceVar, loc: location };
556
+ }
557
+ // --------------------------------------------------------
558
+ // Complete statement
559
+ // --------------------------------------------------------
560
+ function parseCompleteStatement() {
561
+ const location = loc();
562
+ expect(TokenType.KEYWORD, "complete");
563
+ const outputs = [];
564
+ if (match(TokenType.KEYWORD, "with")) {
565
+ // Parse key-value pairs: key expr [and key expr ...]
566
+ do {
567
+ const paramLoc = loc();
568
+ const nameTok = current();
569
+ const name = advance().value;
570
+ const value = parseAtomExpression();
571
+ outputs.push({ kind: "Parameter", name, value, loc: paramLoc });
572
+ } while (match(TokenType.KEYWORD, "and"));
573
+ }
574
+ expectNewline();
575
+ return { kind: "CompleteStatement", outputs, loc: location };
576
+ }
577
+ // --------------------------------------------------------
578
+ // Reject statement
579
+ // --------------------------------------------------------
580
+ function parseRejectStatement() {
581
+ const location = loc();
582
+ expect(TokenType.KEYWORD, "reject");
583
+ expect(TokenType.KEYWORD, "with", "after \"reject\"");
584
+ const message = parseAtomExpression();
585
+ expectNewline();
586
+ return { kind: "RejectStatement", message, loc: location };
587
+ }
588
+ // --------------------------------------------------------
589
+ // Log statement
590
+ // --------------------------------------------------------
591
+ function parseLogStatement() {
592
+ const location = loc();
593
+ expect(TokenType.KEYWORD, "log");
594
+ const expression = parseAtomExpression();
595
+ expectNewline();
596
+ return { kind: "LogStatement", expression, loc: location };
597
+ }
598
+ // --------------------------------------------------------
599
+ // Service call (the default catch-all)
600
+ // --------------------------------------------------------
601
+ function parseServiceCall() {
602
+ const location = loc();
603
+ // verb is the first word
604
+ const verb = advance().value;
605
+ // description: everything up to "using"
606
+ let description = "";
607
+ while (!atEnd() &&
608
+ !check(TokenType.NEWLINE) &&
609
+ !check(TokenType.DEDENT) &&
610
+ !(check(TokenType.KEYWORD, "using"))) {
611
+ if (description.length > 0)
612
+ description += " ";
613
+ description += advance().value;
614
+ }
615
+ let service = "";
616
+ let path = null;
617
+ const parameters = [];
618
+ if (match(TokenType.KEYWORD, "using")) {
619
+ const serviceTok = current();
620
+ service = advance().value;
621
+ // Optional "at" path: using Service at "/endpoint"
622
+ if (match(TokenType.KEYWORD, "at")) {
623
+ path = parseAtomExpression();
624
+ }
625
+ // Optional "with" parameters: key expr [and key expr ...]
626
+ // Or "to" target
627
+ if (match(TokenType.KEYWORD, "with")) {
628
+ do {
629
+ const paramLoc = loc();
630
+ const name = advance().value;
631
+ const value = parseAtomExpression();
632
+ parameters.push({ kind: "Parameter", name, value, loc: paramLoc });
633
+ } while (match(TokenType.KEYWORD, "and"));
634
+ }
635
+ else if (match(TokenType.KEYWORD, "to")) {
636
+ const paramLoc = loc();
637
+ const value = parseAtomExpression();
638
+ parameters.push({ kind: "Parameter", name: "to", value, loc: paramLoc });
639
+ }
640
+ }
641
+ expectNewline();
642
+ // Check for error handler block
643
+ let errorHandler = null;
644
+ if (match(TokenType.INDENT)) {
645
+ if (check(TokenType.KEYWORD_COMPOUND, "on failure") || check(TokenType.KEYWORD_COMPOUND, "on timeout")) {
646
+ errorHandler = parseErrorHandler();
647
+ }
648
+ // If there was an INDENT but no error handler, it might be wrong — handle gracefully
649
+ if (!errorHandler) {
650
+ // Skip any remaining content in this indented block
651
+ while (!check(TokenType.DEDENT) && !atEnd()) {
652
+ advance();
653
+ }
654
+ }
655
+ match(TokenType.DEDENT);
656
+ }
657
+ return { kind: "ServiceCall", verb, description, service, path, parameters, errorHandler, loc: location };
658
+ }
659
+ // --------------------------------------------------------
660
+ // Error handler
661
+ // --------------------------------------------------------
662
+ function parseErrorHandler() {
663
+ const location = loc();
664
+ // consume "on failure:" or "on timeout:"
665
+ advance();
666
+ expect(TokenType.COLON);
667
+ expectNewline();
668
+ let retryCount = null;
669
+ let retryWaitSeconds = null;
670
+ let fallback = null;
671
+ if (!match(TokenType.INDENT)) {
672
+ return { kind: "ErrorHandler", retryCount, retryWaitSeconds, fallback, loc: location };
673
+ }
674
+ while (!check(TokenType.DEDENT) && !atEnd()) {
675
+ skipNewlines();
676
+ if (check(TokenType.DEDENT) || atEnd())
677
+ break;
678
+ // "retry N times waiting N seconds"
679
+ if (check(TokenType.KEYWORD, "retry")) {
680
+ advance(); // consume "retry"
681
+ const countTok = current();
682
+ if (countTok.type === TokenType.NUMBER) {
683
+ retryCount = Number(advance().value);
684
+ }
685
+ match(TokenType.KEYWORD, "times");
686
+ if (match(TokenType.KEYWORD, "waiting")) {
687
+ const waitTok = current();
688
+ if (waitTok.type === TokenType.NUMBER) {
689
+ retryWaitSeconds = Number(advance().value);
690
+ }
691
+ // consume "seconds" or "minutes" as identifier
692
+ if (check(TokenType.IDENTIFIER) || check(TokenType.KEYWORD)) {
693
+ const unitTok = advance();
694
+ if (unitTok.value === "minutes") {
695
+ retryWaitSeconds = (retryWaitSeconds ?? 0) * 60;
696
+ }
697
+ }
698
+ }
699
+ expectNewline();
700
+ }
701
+ // "if still failing:"
702
+ else if (check(TokenType.KEYWORD_COMPOUND, "if still failing")) {
703
+ advance();
704
+ expect(TokenType.COLON);
705
+ expectNewline();
706
+ fallback = parseBlock();
707
+ }
708
+ else {
709
+ // Unknown content in error handler — skip line
710
+ skipToNextStatement();
711
+ skipNewlines();
712
+ }
713
+ }
714
+ match(TokenType.DEDENT);
715
+ return { kind: "ErrorHandler", retryCount, retryWaitSeconds, fallback, loc: location };
716
+ }
717
+ // --------------------------------------------------------
718
+ // Block parsing (indented block of statements)
719
+ // --------------------------------------------------------
720
+ function parseBlock() {
721
+ const statements = [];
722
+ if (!match(TokenType.INDENT)) {
723
+ addError(current(), "Expected an indented block here.");
724
+ return statements;
725
+ }
726
+ while (!check(TokenType.DEDENT) && !atEnd()) {
727
+ skipNewlines();
728
+ if (check(TokenType.DEDENT) || atEnd())
729
+ break;
730
+ const stmt = parseStatement();
731
+ if (stmt)
732
+ statements.push(stmt);
733
+ skipNewlines();
734
+ }
735
+ match(TokenType.DEDENT);
736
+ return statements;
737
+ }
738
+ // --------------------------------------------------------
739
+ // Expression parsing
740
+ // --------------------------------------------------------
741
+ function parseExpression() {
742
+ return parseLogicalExpression();
743
+ }
744
+ function parseLogicalExpression() {
745
+ let left = parseComparisonOrMathExpression();
746
+ while (check(TokenType.KEYWORD, "and") || check(TokenType.KEYWORD, "or")) {
747
+ const op = advance().value;
748
+ const right = parseComparisonOrMathExpression();
749
+ left = {
750
+ kind: "LogicalExpression",
751
+ operator: op,
752
+ left,
753
+ right,
754
+ loc: left.loc,
755
+ };
756
+ }
757
+ return left;
758
+ }
759
+ function parseComparisonOrMathExpression() {
760
+ let left = parseAtomExpression();
761
+ // Check for comparison operator
762
+ const tok = current();
763
+ const compoundValue = tok.type === TokenType.KEYWORD_COMPOUND ? tok.value : null;
764
+ const singleValue = tok.type === TokenType.KEYWORD ? tok.value : null;
765
+ const opValue = compoundValue ?? singleValue;
766
+ if (opValue && COMPARISON_OPERATORS.has(opValue)) {
767
+ advance();
768
+ const operator = opValue;
769
+ if (UNARY_COMPARISON_OPERATORS.has(opValue)) {
770
+ return {
771
+ kind: "ComparisonExpression",
772
+ left,
773
+ operator,
774
+ right: null,
775
+ loc: left.loc,
776
+ };
777
+ }
778
+ const right = parseAtomExpression();
779
+ return {
780
+ kind: "ComparisonExpression",
781
+ left,
782
+ operator,
783
+ right,
784
+ loc: left.loc,
785
+ };
786
+ }
787
+ // Check for math operator
788
+ if (opValue && MATH_OPERATORS.has(opValue)) {
789
+ advance();
790
+ const operator = opValue;
791
+ const right = parseAtomExpression();
792
+ left = {
793
+ kind: "MathExpression",
794
+ left,
795
+ operator,
796
+ right,
797
+ loc: left.loc,
798
+ };
799
+ // Allow chaining: a plus b minus c
800
+ while (true) {
801
+ const nextTok = current();
802
+ const nextOp = nextTok.type === TokenType.KEYWORD_COMPOUND
803
+ ? nextTok.value
804
+ : nextTok.type === TokenType.KEYWORD
805
+ ? nextTok.value
806
+ : null;
807
+ if (nextOp && MATH_OPERATORS.has(nextOp)) {
808
+ advance();
809
+ const chainRight = parseAtomExpression();
810
+ left = {
811
+ kind: "MathExpression",
812
+ left,
813
+ operator: nextOp,
814
+ right: chainRight,
815
+ loc: left.loc,
816
+ };
817
+ }
818
+ else {
819
+ break;
820
+ }
821
+ }
822
+ }
823
+ return left;
824
+ }
825
+ function parseAtomExpression() {
826
+ const tok = current();
827
+ // String literal
828
+ if (tok.type === TokenType.STRING) {
829
+ advance();
830
+ return { kind: "StringLiteral", value: tok.value, loc: { line: tok.line, column: tok.column } };
831
+ }
832
+ // Interpolated string (starts with STRING_PART)
833
+ if (tok.type === TokenType.STRING_PART) {
834
+ return parseInterpolatedString();
835
+ }
836
+ // Number literal
837
+ if (tok.type === TokenType.NUMBER) {
838
+ advance();
839
+ return { kind: "NumberLiteral", value: Number(tok.value), loc: { line: tok.line, column: tok.column } };
840
+ }
841
+ // Boolean literal
842
+ if (tok.type === TokenType.BOOLEAN) {
843
+ advance();
844
+ return { kind: "BooleanLiteral", value: tok.value === "true", loc: { line: tok.line, column: tok.column } };
845
+ }
846
+ // Identifier (possibly with dot access)
847
+ if (tok.type === TokenType.IDENTIFIER) {
848
+ return parseIdentifierOrDotAccess();
849
+ }
850
+ // Keyword used as identifier in expression context (e.g., "status" as a key name)
851
+ if (tok.type === TokenType.KEYWORD && !isStatementKeyword(tok.value)) {
852
+ advance();
853
+ let expr = { kind: "Identifier", name: tok.value, loc: { line: tok.line, column: tok.column } };
854
+ // Handle dot access
855
+ while (check(TokenType.DOT)) {
856
+ advance();
857
+ const propTok = current();
858
+ const prop = advance().value;
859
+ expr = { kind: "DotAccess", object: expr, property: prop, loc: expr.loc };
860
+ }
861
+ return expr;
862
+ }
863
+ // env keyword (env.VAR_NAME)
864
+ if (tok.type === TokenType.KEYWORD && tok.value === "env") {
865
+ advance();
866
+ let expr = { kind: "Identifier", name: "env", loc: { line: tok.line, column: tok.column } };
867
+ while (check(TokenType.DOT)) {
868
+ advance();
869
+ const propTok = current();
870
+ const prop = advance().value;
871
+ expr = { kind: "DotAccess", object: expr, property: prop, loc: expr.loc };
872
+ }
873
+ return expr;
874
+ }
875
+ addError(tok, `Expected a value (string, number, or variable name), but found "${tok.value}".`);
876
+ advance();
877
+ return makeErrorExpr();
878
+ }
879
+ function parseIdentifierOrDotAccess() {
880
+ const tok = advance();
881
+ let expr = { kind: "Identifier", name: tok.value, loc: { line: tok.line, column: tok.column } };
882
+ while (check(TokenType.DOT)) {
883
+ advance(); // consume dot
884
+ const propTok = current();
885
+ if (propTok.type !== TokenType.IDENTIFIER && propTok.type !== TokenType.KEYWORD) {
886
+ addError(propTok, `Expected a property name after ".", but found "${propTok.value}".`);
887
+ break;
888
+ }
889
+ const prop = advance().value;
890
+ expr = { kind: "DotAccess", object: expr, property: prop, loc: expr.loc };
891
+ }
892
+ return expr;
893
+ }
894
+ function parseInterpolatedString() {
895
+ const location = loc();
896
+ const parts = [];
897
+ while (check(TokenType.STRING_PART)) {
898
+ const textTok = advance();
899
+ if (textTok.value.length > 0) {
900
+ parts.push({ kind: "text", value: textTok.value });
901
+ }
902
+ if (match(TokenType.INTERP_START)) {
903
+ const expr = parseIdentifierOrDotAccess();
904
+ parts.push({ kind: "expression", value: expr });
905
+ expect(TokenType.INTERP_END);
906
+ }
907
+ }
908
+ // Handle trailing empty STRING_PART
909
+ if (parts.length === 0) {
910
+ parts.push({ kind: "text", value: "" });
911
+ }
912
+ return { kind: "InterpolatedString", parts, loc: location };
913
+ }
914
+ // --------------------------------------------------------
915
+ // Condition expression (used by if statements)
916
+ // --------------------------------------------------------
917
+ function parseConditionExpression() {
918
+ // Check for "not" prefix
919
+ if (check(TokenType.KEYWORD, "not")) {
920
+ const notLoc = loc();
921
+ advance();
922
+ const operand = parseConditionExpression();
923
+ return { kind: "LogicalExpression", operator: "not", left: operand, right: null, loc: notLoc };
924
+ }
925
+ return parseExpression();
926
+ }
927
+ // --------------------------------------------------------
928
+ // Helpers
929
+ // --------------------------------------------------------
930
+ function isStatementKeyword(value) {
931
+ return ["if", "set", "ask", "step", "complete", "reject", "log", "otherwise", "workflow", "config", "services", "trigger"].includes(value);
932
+ }
933
+ function makeErrorExpr() {
934
+ return { kind: "StringLiteral", value: "<error>", loc: loc() };
935
+ }
936
+ // --------------------------------------------------------
937
+ // Run the parser
938
+ // --------------------------------------------------------
939
+ const program = parseProgram();
940
+ return { program, errors };
941
+ }
942
+ //# sourceMappingURL=index.js.map