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.
- package/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/analyzer/index.d.ts +3 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +268 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +241 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/errors/index.d.ts +40 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +110 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/lexer/index.d.ts +7 -0
- package/dist/lexer/index.d.ts.map +1 -0
- package/dist/lexer/index.js +443 -0
- package/dist/lexer/index.js.map +1 -0
- package/dist/parser/index.d.ts +11 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +942 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/runtime/index.d.ts +92 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +972 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/types/index.d.ts +269 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +29 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +59 -0
|
@@ -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
|