kimchilang 1.0.1
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/.github/workflows/ci.yml +66 -0
- package/README.md +1547 -0
- package/create-kimchi-app/README.md +44 -0
- package/create-kimchi-app/index.js +214 -0
- package/create-kimchi-app/package.json +22 -0
- package/editors/README.md +121 -0
- package/editors/sublime/KimchiLang.sublime-syntax +138 -0
- package/editors/vscode/README.md +90 -0
- package/editors/vscode/kimchilang-1.1.0.vsix +0 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +55 -0
- package/editors/vscode/src/extension.js +354 -0
- package/editors/vscode/syntaxes/kimchi.tmLanguage.json +215 -0
- package/examples/api/client.km +36 -0
- package/examples/async_pipe.km +58 -0
- package/examples/basic.kimchi +109 -0
- package/examples/cli_framework/README.md +92 -0
- package/examples/cli_framework/calculator.km +61 -0
- package/examples/cli_framework/deploy.km +126 -0
- package/examples/cli_framework/greeter.km +26 -0
- package/examples/config.static +27 -0
- package/examples/config.static.js +10 -0
- package/examples/env_test.km +37 -0
- package/examples/fibonacci.kimchi +17 -0
- package/examples/greeter.km +15 -0
- package/examples/hello.js +1 -0
- package/examples/hello.kimchi +3 -0
- package/examples/js_interop.km +42 -0
- package/examples/logger_example.km +34 -0
- package/examples/memo_fibonacci.km +17 -0
- package/examples/myapp/lib/http.js +14 -0
- package/examples/myapp/lib/http.km +16 -0
- package/examples/myapp/main.km +16 -0
- package/examples/myapp/main_with_mock.km +42 -0
- package/examples/myapp/services/api.js +18 -0
- package/examples/myapp/services/api.km +18 -0
- package/examples/new_features.kimchi +52 -0
- package/examples/project_example.static +20 -0
- package/examples/readme_examples.km +240 -0
- package/examples/reduce_pattern_match.km +85 -0
- package/examples/regex_match.km +46 -0
- package/examples/sample.js +45 -0
- package/examples/sample.km +39 -0
- package/examples/secrets.static +35 -0
- package/examples/secrets.static.js +30 -0
- package/examples/shell-example.mjs +144 -0
- package/examples/shell_example.km +19 -0
- package/examples/stdlib_test.km +22 -0
- package/examples/test_example.km +69 -0
- package/examples/testing/README.md +88 -0
- package/examples/testing/http_client.km +18 -0
- package/examples/testing/math.km +48 -0
- package/examples/testing/math.test.km +93 -0
- package/examples/testing/user_service.km +29 -0
- package/examples/testing/user_service.test.km +72 -0
- package/examples/use-config.mjs +141 -0
- package/examples/use_config.km +13 -0
- package/install.sh +59 -0
- package/package.json +29 -0
- package/pantry/acorn/index.km +1 -0
- package/pantry/is_number/index.km +1 -0
- package/pantry/is_odd/index.km +2 -0
- package/project.static +6 -0
- package/src/cli.js +1245 -0
- package/src/generator.js +1241 -0
- package/src/index.js +141 -0
- package/src/js2km.js +568 -0
- package/src/lexer.js +822 -0
- package/src/linter.js +810 -0
- package/src/package-manager.js +307 -0
- package/src/parser.js +1876 -0
- package/src/static-parser.js +500 -0
- package/src/typechecker.js +950 -0
- package/stdlib/array.km +0 -0
- package/stdlib/bitwise.km +38 -0
- package/stdlib/console.km +49 -0
- package/stdlib/date.km +97 -0
- package/stdlib/function.km +44 -0
- package/stdlib/http.km +197 -0
- package/stdlib/http.md +333 -0
- package/stdlib/index.km +26 -0
- package/stdlib/json.km +17 -0
- package/stdlib/logger.js +114 -0
- package/stdlib/logger.km +104 -0
- package/stdlib/math.km +120 -0
- package/stdlib/object.km +41 -0
- package/stdlib/promise.km +33 -0
- package/stdlib/string.km +93 -0
- package/stdlib/testing.md +265 -0
- package/test/test.js +599 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,1876 @@
|
|
|
1
|
+
// KimchiLang Parser - Converts tokens into an Abstract Syntax Tree (AST)
|
|
2
|
+
|
|
3
|
+
import { TokenType, Lexer } from './lexer.js';
|
|
4
|
+
|
|
5
|
+
// AST Node Types
|
|
6
|
+
export const NodeType = {
|
|
7
|
+
Program: 'Program',
|
|
8
|
+
|
|
9
|
+
// Declarations
|
|
10
|
+
DecDeclaration: 'DecDeclaration',
|
|
11
|
+
FunctionDeclaration: 'FunctionDeclaration',
|
|
12
|
+
|
|
13
|
+
// Statements
|
|
14
|
+
ExpressionStatement: 'ExpressionStatement',
|
|
15
|
+
BlockStatement: 'BlockStatement',
|
|
16
|
+
IfStatement: 'IfStatement',
|
|
17
|
+
WhileStatement: 'WhileStatement',
|
|
18
|
+
ForStatement: 'ForStatement',
|
|
19
|
+
ForInStatement: 'ForInStatement',
|
|
20
|
+
ReturnStatement: 'ReturnStatement',
|
|
21
|
+
BreakStatement: 'BreakStatement',
|
|
22
|
+
ContinueStatement: 'ContinueStatement',
|
|
23
|
+
TryStatement: 'TryStatement',
|
|
24
|
+
ThrowStatement: 'ThrowStatement',
|
|
25
|
+
PatternMatch: 'PatternMatch',
|
|
26
|
+
PrintStatement: 'PrintStatement',
|
|
27
|
+
DepStatement: 'DepStatement',
|
|
28
|
+
ArgDeclaration: 'ArgDeclaration',
|
|
29
|
+
EnvDeclaration: 'EnvDeclaration',
|
|
30
|
+
|
|
31
|
+
// Expressions
|
|
32
|
+
Identifier: 'Identifier',
|
|
33
|
+
Literal: 'Literal',
|
|
34
|
+
BinaryExpression: 'BinaryExpression',
|
|
35
|
+
UnaryExpression: 'UnaryExpression',
|
|
36
|
+
AssignmentExpression: 'AssignmentExpression',
|
|
37
|
+
CallExpression: 'CallExpression',
|
|
38
|
+
MemberExpression: 'MemberExpression',
|
|
39
|
+
ArrayExpression: 'ArrayExpression',
|
|
40
|
+
ObjectExpression: 'ObjectExpression',
|
|
41
|
+
ArrowFunctionExpression: 'ArrowFunctionExpression',
|
|
42
|
+
ConditionalExpression: 'ConditionalExpression',
|
|
43
|
+
AwaitExpression: 'AwaitExpression',
|
|
44
|
+
SpreadElement: 'SpreadElement',
|
|
45
|
+
RangeExpression: 'RangeExpression',
|
|
46
|
+
FlowExpression: 'FlowExpression',
|
|
47
|
+
PipeExpression: 'PipeExpression',
|
|
48
|
+
TemplateLiteral: 'TemplateLiteral',
|
|
49
|
+
|
|
50
|
+
// Patterns
|
|
51
|
+
Property: 'Property',
|
|
52
|
+
MatchCase: 'MatchCase',
|
|
53
|
+
ObjectPattern: 'ObjectPattern',
|
|
54
|
+
ArrayPattern: 'ArrayPattern',
|
|
55
|
+
EnumDeclaration: 'EnumDeclaration',
|
|
56
|
+
RegexLiteral: 'RegexLiteral',
|
|
57
|
+
MatchExpression: 'MatchExpression',
|
|
58
|
+
|
|
59
|
+
// Interop
|
|
60
|
+
JSBlock: 'JSBlock',
|
|
61
|
+
ShellBlock: 'ShellBlock',
|
|
62
|
+
|
|
63
|
+
// Testing
|
|
64
|
+
TestBlock: 'TestBlock',
|
|
65
|
+
DescribeBlock: 'DescribeBlock',
|
|
66
|
+
ExpectStatement: 'ExpectStatement',
|
|
67
|
+
AssertStatement: 'AssertStatement',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
class ParseError extends Error {
|
|
71
|
+
constructor(message, token) {
|
|
72
|
+
super(`Parse Error at ${token.line}:${token.column}: ${message}`);
|
|
73
|
+
this.token = token;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class Parser {
|
|
78
|
+
constructor(tokens) {
|
|
79
|
+
this.tokens = tokens.filter(t => t.type !== TokenType.NEWLINE || this.isSignificantNewline(t));
|
|
80
|
+
this.pos = 0;
|
|
81
|
+
this.decVariables = new Set(); // Track deeply immutable variables
|
|
82
|
+
this.secretVariables = new Set(); // Track secret variables
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
isSignificantNewline(token) {
|
|
86
|
+
return false; // For now, ignore all newlines (use semicolons or braces)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
error(message) {
|
|
90
|
+
throw new ParseError(message, this.peek());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
errorAt(message, token) {
|
|
94
|
+
throw new ParseError(message, token);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
peek(offset = 0) {
|
|
98
|
+
const pos = this.pos + offset;
|
|
99
|
+
if (pos >= this.tokens.length) {
|
|
100
|
+
return this.tokens[this.tokens.length - 1]; // EOF
|
|
101
|
+
}
|
|
102
|
+
return this.tokens[pos];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
advance() {
|
|
106
|
+
const token = this.peek();
|
|
107
|
+
if (token.type !== TokenType.EOF) {
|
|
108
|
+
this.pos++;
|
|
109
|
+
}
|
|
110
|
+
return token;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
check(type) {
|
|
114
|
+
return this.peek().type === type;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
match(...types) {
|
|
118
|
+
for (const type of types) {
|
|
119
|
+
if (this.check(type)) {
|
|
120
|
+
this.advance();
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
expect(type, message) {
|
|
128
|
+
if (this.check(type)) {
|
|
129
|
+
return this.advance();
|
|
130
|
+
}
|
|
131
|
+
this.error(message || `Expected ${type}, got ${this.peek().type}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
skipNewlines() {
|
|
135
|
+
while (this.match(TokenType.NEWLINE)) {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Attach position info from a token to a node
|
|
139
|
+
withPosition(node, token = null) {
|
|
140
|
+
const t = token || this.peek();
|
|
141
|
+
node.line = t.line;
|
|
142
|
+
node.column = t.column;
|
|
143
|
+
return node;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Main parsing entry point
|
|
147
|
+
parse() {
|
|
148
|
+
const body = [];
|
|
149
|
+
|
|
150
|
+
while (!this.check(TokenType.EOF)) {
|
|
151
|
+
this.skipNewlines();
|
|
152
|
+
if (this.check(TokenType.EOF)) break;
|
|
153
|
+
|
|
154
|
+
const stmt = this.parseStatement();
|
|
155
|
+
if (stmt) body.push(stmt);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
type: NodeType.Program,
|
|
160
|
+
body,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
parseStatement() {
|
|
165
|
+
this.skipNewlines();
|
|
166
|
+
|
|
167
|
+
// Check for expose modifier
|
|
168
|
+
let exposed = false;
|
|
169
|
+
if (this.check(TokenType.EXPOSE)) {
|
|
170
|
+
this.advance();
|
|
171
|
+
exposed = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for secret modifier
|
|
175
|
+
let secret = false;
|
|
176
|
+
if (this.check(TokenType.SECRET)) {
|
|
177
|
+
this.advance();
|
|
178
|
+
secret = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Declarations
|
|
182
|
+
if (this.check(TokenType.DEC)) {
|
|
183
|
+
const decl = this.parseDecDeclaration();
|
|
184
|
+
decl.exposed = exposed;
|
|
185
|
+
decl.secret = secret;
|
|
186
|
+
// Track secret variables
|
|
187
|
+
if (secret && decl.name) {
|
|
188
|
+
this.secretVariables.add(decl.name);
|
|
189
|
+
}
|
|
190
|
+
return decl;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (this.check(TokenType.ASYNC)) {
|
|
194
|
+
this.advance();
|
|
195
|
+
if (this.check(TokenType.FN)) {
|
|
196
|
+
const decl = this.parseFunctionDeclaration();
|
|
197
|
+
decl.async = true;
|
|
198
|
+
decl.exposed = exposed;
|
|
199
|
+
return decl;
|
|
200
|
+
}
|
|
201
|
+
if (this.check(TokenType.MEMO)) {
|
|
202
|
+
const decl = this.parseMemoDeclaration();
|
|
203
|
+
decl.async = true;
|
|
204
|
+
decl.exposed = exposed;
|
|
205
|
+
return decl;
|
|
206
|
+
}
|
|
207
|
+
this.error('async must be followed by fn or memo');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.check(TokenType.FN)) {
|
|
211
|
+
const decl = this.parseFunctionDeclaration();
|
|
212
|
+
decl.exposed = exposed;
|
|
213
|
+
return decl;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (this.check(TokenType.MEMO)) {
|
|
217
|
+
const decl = this.parseMemoDeclaration();
|
|
218
|
+
decl.exposed = exposed;
|
|
219
|
+
return decl;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.check(TokenType.ENUM)) {
|
|
223
|
+
const decl = this.parseEnumDeclaration();
|
|
224
|
+
decl.exposed = exposed;
|
|
225
|
+
return decl;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Env declaration: env <name>, !env <name>, secret env <name>
|
|
229
|
+
if (this.check(TokenType.ENV) || (this.check(TokenType.NOT) && this.peek(1).type === TokenType.ENV)) {
|
|
230
|
+
const decl = this.parseEnvDeclaration();
|
|
231
|
+
decl.secret = secret;
|
|
232
|
+
if (secret) {
|
|
233
|
+
this.secretVariables.add(decl.name);
|
|
234
|
+
}
|
|
235
|
+
return decl;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Arg declaration: arg <name>, !arg <name>, arg <name> = <default>, secret arg <name>
|
|
239
|
+
if (this.check(TokenType.ARG) || (this.check(TokenType.NOT) && this.peek(1).type === TokenType.ARG)) {
|
|
240
|
+
const decl = this.parseArgDeclaration();
|
|
241
|
+
decl.secret = secret;
|
|
242
|
+
if (secret) {
|
|
243
|
+
this.secretVariables.add(decl.name);
|
|
244
|
+
}
|
|
245
|
+
return decl;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// If expose was used but not followed by a valid declaration
|
|
249
|
+
if (exposed) {
|
|
250
|
+
this.error('expose must be followed by dec, fn, memo, or enum');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// If secret was used but not followed by dec, env, or arg
|
|
254
|
+
if (secret) {
|
|
255
|
+
this.error('secret must be followed by dec, env, or arg');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Control flow
|
|
259
|
+
if (this.check(TokenType.IF)) {
|
|
260
|
+
return this.parseIfStatement();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (this.check(TokenType.WHILE)) {
|
|
264
|
+
return this.parseWhileStatement();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (this.check(TokenType.FOR)) {
|
|
268
|
+
return this.parseForStatement();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (this.check(TokenType.RETURN)) {
|
|
272
|
+
return this.parseReturnStatement();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.check(TokenType.BREAK)) {
|
|
276
|
+
this.advance();
|
|
277
|
+
return { type: NodeType.BreakStatement };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.check(TokenType.CONTINUE)) {
|
|
281
|
+
this.advance();
|
|
282
|
+
return { type: NodeType.ContinueStatement };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (this.check(TokenType.TRY)) {
|
|
286
|
+
return this.parseTryStatement();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.check(TokenType.THROW)) {
|
|
290
|
+
return this.parseThrowStatement();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Pattern matching: |condition| => code
|
|
294
|
+
if (this.check(TokenType.BITOR)) {
|
|
295
|
+
return this.parsePatternMatch();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Print (convenience)
|
|
299
|
+
if (this.check(TokenType.PRINT)) {
|
|
300
|
+
return this.parsePrintStatement();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Dependency declaration: as <alias> dep <path>
|
|
304
|
+
if (this.check(TokenType.AS)) {
|
|
305
|
+
return this.parseDepStatement();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// JS interop block: js { ... } or js(args) { ... }
|
|
309
|
+
if (this.check(TokenType.JS)) {
|
|
310
|
+
return this.parseJSBlock();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Shell interop block: shell { ... } or shell(args) { ... }
|
|
314
|
+
if (this.check(TokenType.SHELL)) {
|
|
315
|
+
return this.parseShellBlock();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Test block: test "name" { ... }
|
|
319
|
+
if (this.check(TokenType.TEST)) {
|
|
320
|
+
return this.parseTestBlock();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Describe block: describe "name" { ... }
|
|
324
|
+
if (this.check(TokenType.DESCRIBE)) {
|
|
325
|
+
return this.parseDescribeBlock();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Expect statement: expect(value).toBe(expected)
|
|
329
|
+
if (this.check(TokenType.EXPECT)) {
|
|
330
|
+
return this.parseExpectStatement();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Assert statement: assert condition, "message"
|
|
334
|
+
if (this.check(TokenType.ASSERT)) {
|
|
335
|
+
return this.parseAssertStatement();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Expression statement
|
|
339
|
+
return this.parseExpressionStatement();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
parseDecDeclaration() {
|
|
343
|
+
this.expect(TokenType.DEC, 'Expected dec');
|
|
344
|
+
|
|
345
|
+
// Check for destructuring pattern
|
|
346
|
+
if (this.check(TokenType.LBRACE)) {
|
|
347
|
+
// Object destructuring: dec { a, b } = obj
|
|
348
|
+
const pattern = this.parseObjectPattern();
|
|
349
|
+
this.expect(TokenType.ASSIGN, 'dec requires initialization');
|
|
350
|
+
const init = this.parseExpression();
|
|
351
|
+
|
|
352
|
+
// Register all destructured variables as immutable
|
|
353
|
+
for (const prop of pattern.properties) {
|
|
354
|
+
this.decVariables.add(prop.key);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
type: NodeType.DecDeclaration,
|
|
359
|
+
pattern,
|
|
360
|
+
init,
|
|
361
|
+
destructuring: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
366
|
+
// Array destructuring: dec [x, y] = arr
|
|
367
|
+
const pattern = this.parseArrayPattern();
|
|
368
|
+
this.expect(TokenType.ASSIGN, 'dec requires initialization');
|
|
369
|
+
const init = this.parseExpression();
|
|
370
|
+
|
|
371
|
+
// Register all destructured variables as immutable
|
|
372
|
+
for (const elem of pattern.elements) {
|
|
373
|
+
if (elem && elem.type === NodeType.Identifier) {
|
|
374
|
+
this.decVariables.add(elem.name);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
type: NodeType.DecDeclaration,
|
|
380
|
+
pattern,
|
|
381
|
+
init,
|
|
382
|
+
destructuring: true,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected variable name').value;
|
|
387
|
+
|
|
388
|
+
// Register this variable as deeply immutable
|
|
389
|
+
this.decVariables.add(name);
|
|
390
|
+
|
|
391
|
+
this.expect(TokenType.ASSIGN, 'dec requires initialization');
|
|
392
|
+
const init = this.parseExpression();
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
type: NodeType.DecDeclaration,
|
|
396
|
+
name,
|
|
397
|
+
init,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
parseObjectPattern() {
|
|
402
|
+
this.expect(TokenType.LBRACE, 'Expected {');
|
|
403
|
+
const properties = [];
|
|
404
|
+
|
|
405
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
406
|
+
do {
|
|
407
|
+
this.skipNewlines();
|
|
408
|
+
if (this.check(TokenType.RBRACE)) break;
|
|
409
|
+
|
|
410
|
+
const key = this.expect(TokenType.IDENTIFIER, 'Expected property name').value;
|
|
411
|
+
|
|
412
|
+
// Check for renaming: { oldName: newName }
|
|
413
|
+
let value = key;
|
|
414
|
+
if (this.match(TokenType.COLON)) {
|
|
415
|
+
value = this.expect(TokenType.IDENTIFIER, 'Expected variable name').value;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
properties.push({ key, value });
|
|
419
|
+
} while (this.match(TokenType.COMMA));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.skipNewlines();
|
|
423
|
+
this.expect(TokenType.RBRACE, 'Expected }');
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
type: NodeType.ObjectPattern,
|
|
427
|
+
properties,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
parseArrayPattern() {
|
|
432
|
+
this.expect(TokenType.LBRACKET, 'Expected [');
|
|
433
|
+
const elements = [];
|
|
434
|
+
|
|
435
|
+
if (!this.check(TokenType.RBRACKET)) {
|
|
436
|
+
do {
|
|
437
|
+
this.skipNewlines();
|
|
438
|
+
if (this.check(TokenType.RBRACKET)) break;
|
|
439
|
+
|
|
440
|
+
// Allow holes in array destructuring: [a, , b]
|
|
441
|
+
if (this.check(TokenType.COMMA)) {
|
|
442
|
+
elements.push(null);
|
|
443
|
+
} else {
|
|
444
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected variable name').value;
|
|
445
|
+
elements.push({ type: NodeType.Identifier, name });
|
|
446
|
+
}
|
|
447
|
+
} while (this.match(TokenType.COMMA));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
this.skipNewlines();
|
|
451
|
+
this.expect(TokenType.RBRACKET, 'Expected ]');
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
type: NodeType.ArrayPattern,
|
|
455
|
+
elements,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
parseFunctionDeclaration() {
|
|
460
|
+
this.expect(TokenType.FN, 'Expected fn');
|
|
461
|
+
const async = false; // TODO: handle async
|
|
462
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected function name').value;
|
|
463
|
+
|
|
464
|
+
this.expect(TokenType.LPAREN, 'Expected (');
|
|
465
|
+
const params = this.parseParameterList();
|
|
466
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
467
|
+
|
|
468
|
+
const body = this.parseBlock();
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
type: NodeType.FunctionDeclaration,
|
|
472
|
+
name,
|
|
473
|
+
params,
|
|
474
|
+
body,
|
|
475
|
+
async,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
parseMemoDeclaration() {
|
|
480
|
+
this.expect(TokenType.MEMO, 'Expected memo');
|
|
481
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected function name').value;
|
|
482
|
+
|
|
483
|
+
this.expect(TokenType.LPAREN, 'Expected (');
|
|
484
|
+
const params = this.parseParameterList();
|
|
485
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
486
|
+
|
|
487
|
+
const body = this.parseBlock();
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
type: NodeType.FunctionDeclaration,
|
|
491
|
+
name,
|
|
492
|
+
params,
|
|
493
|
+
body,
|
|
494
|
+
async: false,
|
|
495
|
+
memoized: true,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
parseEnumDeclaration() {
|
|
500
|
+
this.expect(TokenType.ENUM, 'Expected enum');
|
|
501
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected enum name').value;
|
|
502
|
+
|
|
503
|
+
this.expect(TokenType.LBRACE, 'Expected {');
|
|
504
|
+
const members = [];
|
|
505
|
+
|
|
506
|
+
this.skipNewlines();
|
|
507
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
508
|
+
do {
|
|
509
|
+
this.skipNewlines();
|
|
510
|
+
if (this.check(TokenType.RBRACE)) break;
|
|
511
|
+
|
|
512
|
+
const memberName = this.expect(TokenType.IDENTIFIER, 'Expected enum member name').value;
|
|
513
|
+
|
|
514
|
+
// Check for explicit value assignment
|
|
515
|
+
let value = null;
|
|
516
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
517
|
+
value = this.parseExpression();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
members.push({ name: memberName, value });
|
|
521
|
+
} while (this.match(TokenType.COMMA));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
this.skipNewlines();
|
|
525
|
+
this.expect(TokenType.RBRACE, 'Expected }');
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
type: NodeType.EnumDeclaration,
|
|
529
|
+
name,
|
|
530
|
+
members,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
parseParameterList() {
|
|
535
|
+
const params = [];
|
|
536
|
+
|
|
537
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
538
|
+
do {
|
|
539
|
+
if (this.match(TokenType.SPREAD)) {
|
|
540
|
+
params.push({
|
|
541
|
+
type: 'RestElement',
|
|
542
|
+
argument: this.expect(TokenType.IDENTIFIER, 'Expected parameter name').value,
|
|
543
|
+
});
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected parameter name').value;
|
|
548
|
+
let defaultValue = null;
|
|
549
|
+
|
|
550
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
551
|
+
defaultValue = this.parseExpression();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
params.push({ name, defaultValue });
|
|
555
|
+
} while (this.match(TokenType.COMMA));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return params;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
parseBlock() {
|
|
562
|
+
this.expect(TokenType.LBRACE, 'Expected {');
|
|
563
|
+
const body = [];
|
|
564
|
+
|
|
565
|
+
while (!this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
|
|
566
|
+
this.skipNewlines();
|
|
567
|
+
if (this.check(TokenType.RBRACE)) break;
|
|
568
|
+
|
|
569
|
+
const stmt = this.parseStatement();
|
|
570
|
+
if (stmt) body.push(stmt);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
this.expect(TokenType.RBRACE, 'Expected }');
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
type: NodeType.BlockStatement,
|
|
577
|
+
body,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
parseIfStatement() {
|
|
582
|
+
this.expect(TokenType.IF, 'Expected if');
|
|
583
|
+
const test = this.parseExpression();
|
|
584
|
+
const consequent = this.parseBlock();
|
|
585
|
+
|
|
586
|
+
let alternate = null;
|
|
587
|
+
if (this.match(TokenType.ELIF)) {
|
|
588
|
+
this.pos--; // Put elif back
|
|
589
|
+
this.tokens[this.pos] = { ...this.tokens[this.pos], type: TokenType.IF };
|
|
590
|
+
alternate = this.parseIfStatement();
|
|
591
|
+
} else if (this.match(TokenType.ELSE)) {
|
|
592
|
+
if (this.check(TokenType.IF)) {
|
|
593
|
+
alternate = this.parseIfStatement();
|
|
594
|
+
} else {
|
|
595
|
+
alternate = this.parseBlock();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
type: NodeType.IfStatement,
|
|
601
|
+
test,
|
|
602
|
+
consequent,
|
|
603
|
+
alternate,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
parseWhileStatement() {
|
|
608
|
+
this.expect(TokenType.WHILE, 'Expected while');
|
|
609
|
+
const test = this.parseExpression();
|
|
610
|
+
const body = this.parseBlock();
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
type: NodeType.WhileStatement,
|
|
614
|
+
test,
|
|
615
|
+
body,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
parseForStatement() {
|
|
620
|
+
this.expect(TokenType.FOR, 'Expected for');
|
|
621
|
+
const variable = this.expect(TokenType.IDENTIFIER, 'Expected variable name').value;
|
|
622
|
+
|
|
623
|
+
this.expect(TokenType.IN, 'Expected in');
|
|
624
|
+
const iterable = this.parseExpression();
|
|
625
|
+
const body = this.parseBlock();
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
type: NodeType.ForInStatement,
|
|
629
|
+
variable,
|
|
630
|
+
iterable,
|
|
631
|
+
body,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
parseReturnStatement() {
|
|
636
|
+
this.expect(TokenType.RETURN, 'Expected return');
|
|
637
|
+
|
|
638
|
+
let argument = null;
|
|
639
|
+
if (!this.check(TokenType.RBRACE) && !this.check(TokenType.NEWLINE) && !this.check(TokenType.EOF)) {
|
|
640
|
+
argument = this.parseExpression();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
type: NodeType.ReturnStatement,
|
|
645
|
+
argument,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
parseTryStatement() {
|
|
650
|
+
this.expect(TokenType.TRY, 'Expected try');
|
|
651
|
+
const block = this.parseBlock();
|
|
652
|
+
|
|
653
|
+
let handler = null;
|
|
654
|
+
if (this.match(TokenType.CATCH)) {
|
|
655
|
+
let param = null;
|
|
656
|
+
if (this.match(TokenType.LPAREN)) {
|
|
657
|
+
param = this.expect(TokenType.IDENTIFIER, 'Expected catch parameter').value;
|
|
658
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
659
|
+
}
|
|
660
|
+
const catchBody = this.parseBlock();
|
|
661
|
+
handler = { param, body: catchBody };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let finalizer = null;
|
|
665
|
+
if (this.match(TokenType.FINALLY)) {
|
|
666
|
+
finalizer = this.parseBlock();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
type: NodeType.TryStatement,
|
|
671
|
+
block,
|
|
672
|
+
handler,
|
|
673
|
+
finalizer,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
parseThrowStatement() {
|
|
678
|
+
this.expect(TokenType.THROW, 'Expected throw');
|
|
679
|
+
const argument = this.parseExpression();
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
type: NodeType.ThrowStatement,
|
|
683
|
+
argument,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
parsePatternMatch() {
|
|
688
|
+
// Standalone pattern matching: |condition| => code
|
|
689
|
+
// Parse consecutive pattern cases
|
|
690
|
+
const cases = [];
|
|
691
|
+
|
|
692
|
+
while (this.check(TokenType.BITOR)) {
|
|
693
|
+
this.expect(TokenType.BITOR, 'Expected |');
|
|
694
|
+
// Parse condition without bitwise OR to avoid consuming the closing |
|
|
695
|
+
const test = this.parsePatternCondition();
|
|
696
|
+
this.expect(TokenType.BITOR, 'Expected |');
|
|
697
|
+
this.expect(TokenType.FAT_ARROW, 'Expected =>');
|
|
698
|
+
|
|
699
|
+
let consequent;
|
|
700
|
+
if (this.check(TokenType.LBRACE)) {
|
|
701
|
+
consequent = this.parseBlock();
|
|
702
|
+
} else {
|
|
703
|
+
consequent = this.parseStatement();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
cases.push({
|
|
707
|
+
type: NodeType.MatchCase,
|
|
708
|
+
test,
|
|
709
|
+
consequent,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
this.skipNewlines();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return {
|
|
716
|
+
type: NodeType.PatternMatch,
|
|
717
|
+
cases,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
parsePatternCondition() {
|
|
722
|
+
// Parse expression but stop before bitwise OR (|) to allow pattern delimiters
|
|
723
|
+
// Skip directly to comparison level, bypassing bitwise operators
|
|
724
|
+
return this.parsePatternOr();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
parsePatternOr() {
|
|
728
|
+
let left = this.parsePatternAnd();
|
|
729
|
+
|
|
730
|
+
while (this.match(TokenType.OR)) {
|
|
731
|
+
const right = this.parsePatternAnd();
|
|
732
|
+
left = {
|
|
733
|
+
type: NodeType.BinaryExpression,
|
|
734
|
+
operator: '||',
|
|
735
|
+
left,
|
|
736
|
+
right,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return left;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
parsePatternAnd() {
|
|
744
|
+
// Skip bitwise OR - go directly to comparison
|
|
745
|
+
let left = this.parseEquality();
|
|
746
|
+
|
|
747
|
+
while (this.match(TokenType.AND)) {
|
|
748
|
+
const right = this.parseEquality();
|
|
749
|
+
left = {
|
|
750
|
+
type: NodeType.BinaryExpression,
|
|
751
|
+
operator: '&&',
|
|
752
|
+
left,
|
|
753
|
+
right,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return left;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
parsePrintStatement() {
|
|
761
|
+
this.expect(TokenType.PRINT, 'Expected print');
|
|
762
|
+
const argument = this.parseExpression();
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
type: NodeType.PrintStatement,
|
|
766
|
+
argument,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
parseDepStatement() {
|
|
771
|
+
// Syntax: as <alias> dep <dotted.path>
|
|
772
|
+
// Or: as <alias> dep <dotted.path>({overrides})
|
|
773
|
+
// Or: as <alias> dep @<dotted.path> (external module from .km_modules)
|
|
774
|
+
this.expect(TokenType.AS, 'Expected as');
|
|
775
|
+
const alias = this.expect(TokenType.IDENTIFIER, 'Expected dependency alias').value;
|
|
776
|
+
this.expect(TokenType.DEP, 'Expected dep');
|
|
777
|
+
|
|
778
|
+
// Check for @ prefix indicating external module
|
|
779
|
+
const isExternal = this.match(TokenType.AT);
|
|
780
|
+
|
|
781
|
+
// Parse dotted path (e.g., project_name.salesforce.client)
|
|
782
|
+
const pathParts = [];
|
|
783
|
+
pathParts.push(this.expect(TokenType.IDENTIFIER, 'Expected dependency path').value);
|
|
784
|
+
|
|
785
|
+
while (this.match(TokenType.DOT)) {
|
|
786
|
+
pathParts.push(this.expect(TokenType.IDENTIFIER, 'Expected path segment').value);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const path = pathParts.join('.');
|
|
790
|
+
|
|
791
|
+
// Check for dependency injection overrides: dep path({...})
|
|
792
|
+
let overrides = null;
|
|
793
|
+
if (this.match(TokenType.LPAREN)) {
|
|
794
|
+
overrides = this.parseExpression();
|
|
795
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
type: NodeType.DepStatement,
|
|
800
|
+
alias,
|
|
801
|
+
path,
|
|
802
|
+
pathParts,
|
|
803
|
+
overrides,
|
|
804
|
+
isExternal,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
parseArgDeclaration() {
|
|
809
|
+
// Syntax: arg <name> - optional arg
|
|
810
|
+
// !arg <name> - required arg
|
|
811
|
+
// arg <name> = <value> - optional arg with default
|
|
812
|
+
let required = false;
|
|
813
|
+
|
|
814
|
+
if (this.match(TokenType.NOT)) {
|
|
815
|
+
required = true;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
this.expect(TokenType.ARG, 'Expected arg');
|
|
819
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected argument name').value;
|
|
820
|
+
|
|
821
|
+
let defaultValue = null;
|
|
822
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
823
|
+
defaultValue = this.parseExpression();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
type: NodeType.ArgDeclaration,
|
|
828
|
+
name,
|
|
829
|
+
required,
|
|
830
|
+
defaultValue,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
parseEnvDeclaration() {
|
|
835
|
+
// Syntax: env <name> - optional env var (undefined if not set)
|
|
836
|
+
// !env <name> - required env var (throws if not set)
|
|
837
|
+
// env <name> = <value> - optional env var with default
|
|
838
|
+
let required = false;
|
|
839
|
+
|
|
840
|
+
if (this.match(TokenType.NOT)) {
|
|
841
|
+
required = true;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
this.expect(TokenType.ENV, 'Expected env');
|
|
845
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected environment variable name').value;
|
|
846
|
+
|
|
847
|
+
let defaultValue = null;
|
|
848
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
849
|
+
defaultValue = this.parseExpression();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return {
|
|
853
|
+
type: NodeType.EnvDeclaration,
|
|
854
|
+
name,
|
|
855
|
+
required,
|
|
856
|
+
defaultValue,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
parseJSBlock() {
|
|
861
|
+
// Syntax: js { ... } - raw JS block
|
|
862
|
+
// js(a, b) { ... } - JS block with inputs from kimchi scope
|
|
863
|
+
this.expect(TokenType.JS, 'Expected js');
|
|
864
|
+
|
|
865
|
+
const inputs = [];
|
|
866
|
+
|
|
867
|
+
// Check for optional input parameters
|
|
868
|
+
if (this.match(TokenType.LPAREN)) {
|
|
869
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
870
|
+
do {
|
|
871
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected identifier').value;
|
|
872
|
+
inputs.push(name);
|
|
873
|
+
} while (this.match(TokenType.COMMA));
|
|
874
|
+
}
|
|
875
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
this.skipNewlines();
|
|
879
|
+
this.expect(TokenType.LBRACE, 'Expected { after js');
|
|
880
|
+
|
|
881
|
+
// Get the raw JS content from the JS_CONTENT token
|
|
882
|
+
let jsCode = '';
|
|
883
|
+
if (this.check(TokenType.JS_CONTENT)) {
|
|
884
|
+
jsCode = this.advance().value;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
this.expect(TokenType.RBRACE, 'Expected } to close js block');
|
|
888
|
+
|
|
889
|
+
// Check for secrets being passed to console.log
|
|
890
|
+
const secretInputs = inputs.filter(input => this.secretVariables.has(input));
|
|
891
|
+
if (secretInputs.length > 0) {
|
|
892
|
+
for (const secretInput of secretInputs) {
|
|
893
|
+
const consolePattern = new RegExp(`console\\s*\\.\\s*(log|error|warn|info|debug|trace)\\s*\\([^)]*\\b${secretInput}\\b`, 'g');
|
|
894
|
+
if (consolePattern.test(jsCode)) {
|
|
895
|
+
// Use the first console token for error location, or fall back to current position
|
|
896
|
+
const errorToken = consoleTokens.length > 0 ? consoleTokens[0] : this.peek();
|
|
897
|
+
this.errorAt(`Cannot pass secret '${secretInput}' to console.log in JS block - secrets must not be logged`, errorToken);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
type: NodeType.JSBlock,
|
|
904
|
+
inputs,
|
|
905
|
+
code: jsCode.trim(),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
parseShellBlock() {
|
|
910
|
+
// Syntax: shell { ... } - raw shell block
|
|
911
|
+
// shell(a, b) { ... } - shell block with inputs from kimchi scope
|
|
912
|
+
// Note: The lexer handles shell specially - it captures raw content as SHELL_CONTENT token
|
|
913
|
+
this.expect(TokenType.SHELL, 'Expected shell');
|
|
914
|
+
|
|
915
|
+
const inputs = [];
|
|
916
|
+
|
|
917
|
+
// Check for optional input parameters
|
|
918
|
+
if (this.match(TokenType.LPAREN)) {
|
|
919
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
920
|
+
do {
|
|
921
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected identifier').value;
|
|
922
|
+
inputs.push(name);
|
|
923
|
+
} while (this.match(TokenType.COMMA));
|
|
924
|
+
}
|
|
925
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
this.skipNewlines();
|
|
929
|
+
this.expect(TokenType.LBRACE, 'Expected { after shell');
|
|
930
|
+
|
|
931
|
+
// The lexer provides raw shell content as a single SHELL_CONTENT token
|
|
932
|
+
const contentToken = this.expect(TokenType.SHELL_CONTENT, 'Expected shell command');
|
|
933
|
+
const shellCode = contentToken.value;
|
|
934
|
+
|
|
935
|
+
this.expect(TokenType.RBRACE, 'Expected } to close shell block');
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
type: NodeType.ShellBlock,
|
|
939
|
+
inputs,
|
|
940
|
+
command: shellCode,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
parseShellBlockExpression() {
|
|
945
|
+
// Same as parseShellBlock but returns as an expression node
|
|
946
|
+
this.expect(TokenType.SHELL, 'Expected shell');
|
|
947
|
+
|
|
948
|
+
const inputs = [];
|
|
949
|
+
|
|
950
|
+
if (this.match(TokenType.LPAREN)) {
|
|
951
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
952
|
+
do {
|
|
953
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected identifier').value;
|
|
954
|
+
inputs.push(name);
|
|
955
|
+
} while (this.match(TokenType.COMMA));
|
|
956
|
+
}
|
|
957
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
this.skipNewlines();
|
|
961
|
+
this.expect(TokenType.LBRACE, 'Expected { after shell');
|
|
962
|
+
|
|
963
|
+
// The lexer provides raw shell content as a single SHELL_CONTENT token
|
|
964
|
+
const contentToken = this.expect(TokenType.SHELL_CONTENT, 'Expected shell command');
|
|
965
|
+
const shellCode = contentToken.value;
|
|
966
|
+
|
|
967
|
+
this.expect(TokenType.RBRACE, 'Expected } to close shell block');
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
type: NodeType.ShellBlock,
|
|
971
|
+
inputs,
|
|
972
|
+
command: shellCode,
|
|
973
|
+
isExpression: true,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
tokenToSource(token) {
|
|
978
|
+
// Convert a token back to source code representation
|
|
979
|
+
switch (token.type) {
|
|
980
|
+
case TokenType.STRING:
|
|
981
|
+
// Check if it's a backtick string (starts with backtick)
|
|
982
|
+
if (token.value.startsWith('`')) {
|
|
983
|
+
return token.value; // Already includes backticks
|
|
984
|
+
}
|
|
985
|
+
return `"${token.value}"`;
|
|
986
|
+
case TokenType.TEMPLATE_STRING:
|
|
987
|
+
// Reconstruct the template string, converting interpolation markers back to ${...}
|
|
988
|
+
let templateValue = token.value;
|
|
989
|
+
templateValue = templateValue.replace(/\x00INTERP_START\x00/g, '${');
|
|
990
|
+
templateValue = templateValue.replace(/\x00INTERP_END\x00/g, '}');
|
|
991
|
+
return `\`${templateValue}\``;
|
|
992
|
+
case TokenType.NUMBER:
|
|
993
|
+
case TokenType.IDENTIFIER:
|
|
994
|
+
case TokenType.BOOLEAN:
|
|
995
|
+
return String(token.value);
|
|
996
|
+
case TokenType.NULL:
|
|
997
|
+
return 'null';
|
|
998
|
+
case TokenType.PLUS:
|
|
999
|
+
return '+';
|
|
1000
|
+
case TokenType.MINUS:
|
|
1001
|
+
return '-';
|
|
1002
|
+
case TokenType.STAR:
|
|
1003
|
+
return '*';
|
|
1004
|
+
case TokenType.SLASH:
|
|
1005
|
+
return '/';
|
|
1006
|
+
case TokenType.PERCENT:
|
|
1007
|
+
return '%';
|
|
1008
|
+
case TokenType.ASSIGN:
|
|
1009
|
+
return '=';
|
|
1010
|
+
case TokenType.EQ:
|
|
1011
|
+
return '==';
|
|
1012
|
+
case TokenType.NEQ:
|
|
1013
|
+
return '!=';
|
|
1014
|
+
case TokenType.LT:
|
|
1015
|
+
return '<';
|
|
1016
|
+
case TokenType.GT:
|
|
1017
|
+
return '>';
|
|
1018
|
+
case TokenType.LTE:
|
|
1019
|
+
return '<=';
|
|
1020
|
+
case TokenType.GTE:
|
|
1021
|
+
return '>=';
|
|
1022
|
+
case TokenType.AND:
|
|
1023
|
+
return '&&';
|
|
1024
|
+
case TokenType.OR:
|
|
1025
|
+
return '||';
|
|
1026
|
+
case TokenType.NOT:
|
|
1027
|
+
return '!';
|
|
1028
|
+
case TokenType.LPAREN:
|
|
1029
|
+
return '(';
|
|
1030
|
+
case TokenType.RPAREN:
|
|
1031
|
+
return ')';
|
|
1032
|
+
case TokenType.LBRACKET:
|
|
1033
|
+
return '[';
|
|
1034
|
+
case TokenType.RBRACKET:
|
|
1035
|
+
return ']';
|
|
1036
|
+
case TokenType.COMMA:
|
|
1037
|
+
return ',';
|
|
1038
|
+
case TokenType.DOT:
|
|
1039
|
+
return '.';
|
|
1040
|
+
case TokenType.COLON:
|
|
1041
|
+
return ':';
|
|
1042
|
+
case TokenType.SEMICOLON:
|
|
1043
|
+
return ';';
|
|
1044
|
+
case TokenType.ARROW:
|
|
1045
|
+
return '->';
|
|
1046
|
+
case TokenType.FAT_ARROW:
|
|
1047
|
+
return '=>';
|
|
1048
|
+
case TokenType.QUESTION:
|
|
1049
|
+
return '?';
|
|
1050
|
+
case TokenType.SPREAD:
|
|
1051
|
+
return '...';
|
|
1052
|
+
default:
|
|
1053
|
+
return token.value !== undefined ? String(token.value) : '';
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
parseJSBlockExpression() {
|
|
1058
|
+
// Same as parseJSBlock but returns as an expression node
|
|
1059
|
+
// Used for: dec result = js(a, b) { return a + b; }
|
|
1060
|
+
this.expect(TokenType.JS, 'Expected js');
|
|
1061
|
+
|
|
1062
|
+
const inputs = [];
|
|
1063
|
+
|
|
1064
|
+
if (this.match(TokenType.LPAREN)) {
|
|
1065
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
1066
|
+
do {
|
|
1067
|
+
const name = this.expect(TokenType.IDENTIFIER, 'Expected identifier').value;
|
|
1068
|
+
inputs.push(name);
|
|
1069
|
+
} while (this.match(TokenType.COMMA));
|
|
1070
|
+
}
|
|
1071
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
this.skipNewlines();
|
|
1075
|
+
this.expect(TokenType.LBRACE, 'Expected { after js');
|
|
1076
|
+
|
|
1077
|
+
// Get the raw JS content from the JS_CONTENT token
|
|
1078
|
+
let jsCode = '';
|
|
1079
|
+
if (this.check(TokenType.JS_CONTENT)) {
|
|
1080
|
+
jsCode = this.advance().value;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
this.expect(TokenType.RBRACE, 'Expected } to close js block');
|
|
1084
|
+
|
|
1085
|
+
// Check for secrets being passed to console.log
|
|
1086
|
+
const secretInputs = inputs.filter(input => this.secretVariables.has(input));
|
|
1087
|
+
if (secretInputs.length > 0) {
|
|
1088
|
+
// Check if the JS code contains console.log with any of the secret inputs
|
|
1089
|
+
for (const secretInput of secretInputs) {
|
|
1090
|
+
// Match console.log, console.error, console.warn, console.info with the secret variable
|
|
1091
|
+
const consolePattern = new RegExp(`console\\s*\\.\\s*(log|error|warn|info|debug|trace)\\s*\\([^)]*\\b${secretInput}\\b`, 'g');
|
|
1092
|
+
if (consolePattern.test(jsCode)) {
|
|
1093
|
+
// Use the first console token for error location, or fall back to current position
|
|
1094
|
+
const errorToken = consoleTokens.length > 0 ? consoleTokens[0] : this.peek();
|
|
1095
|
+
this.errorAt(`Cannot pass secret '${secretInput}' to console.log in JS block - secrets must not be logged`, errorToken);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return {
|
|
1101
|
+
type: NodeType.JSBlock,
|
|
1102
|
+
inputs,
|
|
1103
|
+
code: jsCode.trim(),
|
|
1104
|
+
isExpression: true,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
parseExpressionStatement() {
|
|
1109
|
+
const expression = this.parseExpression();
|
|
1110
|
+
return {
|
|
1111
|
+
type: NodeType.ExpressionStatement,
|
|
1112
|
+
expression,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Expression parsing with precedence climbing
|
|
1117
|
+
parseExpression() {
|
|
1118
|
+
return this.parseAssignment();
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
parseAssignment() {
|
|
1122
|
+
const left = this.parseTernary();
|
|
1123
|
+
|
|
1124
|
+
if (this.match(TokenType.ASSIGN, TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN,
|
|
1125
|
+
TokenType.STAR_ASSIGN, TokenType.SLASH_ASSIGN)) {
|
|
1126
|
+
const operator = this.tokens[this.pos - 1].value;
|
|
1127
|
+
|
|
1128
|
+
// Check for immutability violations on dec variables
|
|
1129
|
+
this.checkDecImmutability(left);
|
|
1130
|
+
|
|
1131
|
+
const right = this.parseAssignment();
|
|
1132
|
+
return {
|
|
1133
|
+
type: NodeType.AssignmentExpression,
|
|
1134
|
+
operator,
|
|
1135
|
+
left,
|
|
1136
|
+
right,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return left;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
checkDecImmutability(node) {
|
|
1144
|
+
// Get the root variable name from the assignment target
|
|
1145
|
+
const rootName = this.getRootIdentifier(node);
|
|
1146
|
+
if (rootName && this.decVariables.has(rootName)) {
|
|
1147
|
+
this.error(`Cannot reassign '${this.getFullPath(node)}': variable '${rootName}' is deeply immutable (declared with dec)`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
getRootIdentifier(node) {
|
|
1152
|
+
if (node.type === NodeType.Identifier) {
|
|
1153
|
+
return node.name;
|
|
1154
|
+
}
|
|
1155
|
+
if (node.type === NodeType.MemberExpression) {
|
|
1156
|
+
return this.getRootIdentifier(node.object);
|
|
1157
|
+
}
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
getFullPath(node) {
|
|
1162
|
+
if (node.type === NodeType.Identifier) {
|
|
1163
|
+
return node.name;
|
|
1164
|
+
}
|
|
1165
|
+
if (node.type === NodeType.MemberExpression) {
|
|
1166
|
+
const objectPath = this.getFullPath(node.object);
|
|
1167
|
+
if (node.computed) {
|
|
1168
|
+
return `${objectPath}[${node.property.name || node.property.value}]`;
|
|
1169
|
+
}
|
|
1170
|
+
return `${objectPath}.${node.property.name || node.property}`;
|
|
1171
|
+
}
|
|
1172
|
+
return '<unknown>';
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
parseTernary() {
|
|
1176
|
+
let test = this.parseFlow();
|
|
1177
|
+
|
|
1178
|
+
if (this.match(TokenType.QUESTION)) {
|
|
1179
|
+
const consequent = this.parseExpression();
|
|
1180
|
+
this.expect(TokenType.COLON, 'Expected :');
|
|
1181
|
+
const alternate = this.parseTernary();
|
|
1182
|
+
return {
|
|
1183
|
+
type: NodeType.ConditionalExpression,
|
|
1184
|
+
test,
|
|
1185
|
+
consequent,
|
|
1186
|
+
alternate,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return test;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
parseFlow() {
|
|
1194
|
+
// New syntax: composedFn >> fn1 fn2 fn3
|
|
1195
|
+
// This creates a composed function that can be called later
|
|
1196
|
+
// We need to check if we have: IDENTIFIER >> ...
|
|
1197
|
+
|
|
1198
|
+
// First parse the left side (potential function name)
|
|
1199
|
+
let left = this.parsePipe();
|
|
1200
|
+
|
|
1201
|
+
// Check if followed by >> (flow operator)
|
|
1202
|
+
if (this.match(TokenType.FLOW)) {
|
|
1203
|
+
// left should be an identifier (the name of the composed function variable)
|
|
1204
|
+
if (left.type !== NodeType.Identifier) {
|
|
1205
|
+
this.error('Left side of >> must be an identifier');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const name = left.name;
|
|
1209
|
+
const functions = [];
|
|
1210
|
+
|
|
1211
|
+
// Parse function identifiers until we hit end of expression
|
|
1212
|
+
while (this.check(TokenType.IDENTIFIER)) {
|
|
1213
|
+
functions.push(this.advance().value);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (functions.length === 0) {
|
|
1217
|
+
this.error('Expected at least one function after >>');
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return {
|
|
1221
|
+
type: NodeType.FlowExpression,
|
|
1222
|
+
name,
|
|
1223
|
+
functions,
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
return left;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
parsePipe() {
|
|
1231
|
+
// Pipe expression: value ~> fn1 ~> fn2
|
|
1232
|
+
// Left-associative: (value ~> fn1) ~> fn2
|
|
1233
|
+
let left = this.parseMatch();
|
|
1234
|
+
|
|
1235
|
+
while (this.match(TokenType.PIPE)) {
|
|
1236
|
+
// The right side should be a function (identifier or member expression)
|
|
1237
|
+
const right = this.parseMatch();
|
|
1238
|
+
left = {
|
|
1239
|
+
type: NodeType.PipeExpression,
|
|
1240
|
+
left,
|
|
1241
|
+
right,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return left;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
parseMatch() {
|
|
1249
|
+
// Match expression: expr ~ /regex/ or expr ~ /regex/ => { body }
|
|
1250
|
+
let left = this.parseOr();
|
|
1251
|
+
|
|
1252
|
+
if (this.match(TokenType.MATCH)) {
|
|
1253
|
+
if (!this.check(TokenType.REGEX)) {
|
|
1254
|
+
this.error('Expected regex pattern after ~');
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const regexToken = this.advance();
|
|
1258
|
+
const regex = {
|
|
1259
|
+
type: NodeType.RegexLiteral,
|
|
1260
|
+
pattern: regexToken.value.pattern,
|
|
1261
|
+
flags: regexToken.value.flags,
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
// Check for optional => { body } for transformation
|
|
1265
|
+
let body = null;
|
|
1266
|
+
if (this.match(TokenType.FAT_ARROW)) {
|
|
1267
|
+
if (this.check(TokenType.LBRACE)) {
|
|
1268
|
+
body = this.parseBlock();
|
|
1269
|
+
} else {
|
|
1270
|
+
// Single expression body
|
|
1271
|
+
body = this.parseExpression();
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return {
|
|
1276
|
+
type: NodeType.MatchExpression,
|
|
1277
|
+
subject: left,
|
|
1278
|
+
pattern: regex,
|
|
1279
|
+
body,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return left;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
parseOr() {
|
|
1287
|
+
let left = this.parseAnd();
|
|
1288
|
+
|
|
1289
|
+
while (this.match(TokenType.OR)) {
|
|
1290
|
+
const right = this.parseAnd();
|
|
1291
|
+
left = {
|
|
1292
|
+
type: NodeType.BinaryExpression,
|
|
1293
|
+
operator: '||',
|
|
1294
|
+
left,
|
|
1295
|
+
right,
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return left;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
parseAnd() {
|
|
1303
|
+
let left = this.parseEquality();
|
|
1304
|
+
|
|
1305
|
+
while (this.match(TokenType.AND)) {
|
|
1306
|
+
const right = this.parseEquality();
|
|
1307
|
+
left = {
|
|
1308
|
+
type: NodeType.BinaryExpression,
|
|
1309
|
+
operator: '&&',
|
|
1310
|
+
left,
|
|
1311
|
+
right,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
return left;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
parseEquality() {
|
|
1319
|
+
let left = this.parseComparison();
|
|
1320
|
+
|
|
1321
|
+
while (this.match(TokenType.EQ, TokenType.NEQ, TokenType.IS, TokenType.NOT)) {
|
|
1322
|
+
const token = this.tokens[this.pos - 1];
|
|
1323
|
+
let operator;
|
|
1324
|
+
if (token.type === TokenType.IS) {
|
|
1325
|
+
// Check for 'is not' combination
|
|
1326
|
+
if (this.match(TokenType.NOT)) {
|
|
1327
|
+
operator = 'is not';
|
|
1328
|
+
} else {
|
|
1329
|
+
operator = 'is';
|
|
1330
|
+
}
|
|
1331
|
+
} else if (token.type === TokenType.NOT) {
|
|
1332
|
+
// Standalone 'not' at equality level means != (like Python's 'not in' pattern)
|
|
1333
|
+
// But we need 'is' before it, so this shouldn't happen in normal flow
|
|
1334
|
+
// Rewind and let unary handle it
|
|
1335
|
+
this.pos--;
|
|
1336
|
+
break;
|
|
1337
|
+
} else {
|
|
1338
|
+
operator = token.value === '==' ? '===' : '!==';
|
|
1339
|
+
}
|
|
1340
|
+
const right = this.parseComparison();
|
|
1341
|
+
left = {
|
|
1342
|
+
type: NodeType.BinaryExpression,
|
|
1343
|
+
operator,
|
|
1344
|
+
left,
|
|
1345
|
+
right,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
return left;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
parseComparison() {
|
|
1353
|
+
let left = this.parseShift();
|
|
1354
|
+
|
|
1355
|
+
while (this.match(TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE)) {
|
|
1356
|
+
const operator = this.tokens[this.pos - 1].value;
|
|
1357
|
+
const right = this.parseShift();
|
|
1358
|
+
left = {
|
|
1359
|
+
type: NodeType.BinaryExpression,
|
|
1360
|
+
operator,
|
|
1361
|
+
left,
|
|
1362
|
+
right,
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
return left;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
parseShift() {
|
|
1370
|
+
let left = this.parseRange();
|
|
1371
|
+
|
|
1372
|
+
while (this.match(TokenType.LSHIFT, TokenType.RSHIFT)) {
|
|
1373
|
+
const operator = this.tokens[this.pos - 1].value;
|
|
1374
|
+
const right = this.parseRange();
|
|
1375
|
+
left = {
|
|
1376
|
+
type: NodeType.BinaryExpression,
|
|
1377
|
+
operator,
|
|
1378
|
+
left,
|
|
1379
|
+
right,
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
return left;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
parseRange() {
|
|
1387
|
+
let left = this.parseAdditive();
|
|
1388
|
+
|
|
1389
|
+
if (this.match(TokenType.RANGE)) {
|
|
1390
|
+
const right = this.parseAdditive();
|
|
1391
|
+
return {
|
|
1392
|
+
type: NodeType.RangeExpression,
|
|
1393
|
+
start: left,
|
|
1394
|
+
end: right,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return left;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
parseAdditive() {
|
|
1402
|
+
let left = this.parseMultiplicative();
|
|
1403
|
+
|
|
1404
|
+
while (this.match(TokenType.PLUS, TokenType.MINUS)) {
|
|
1405
|
+
const operator = this.tokens[this.pos - 1].value;
|
|
1406
|
+
const right = this.parseMultiplicative();
|
|
1407
|
+
left = {
|
|
1408
|
+
type: NodeType.BinaryExpression,
|
|
1409
|
+
operator,
|
|
1410
|
+
left,
|
|
1411
|
+
right,
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return left;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
parseMultiplicative() {
|
|
1419
|
+
let left = this.parsePower();
|
|
1420
|
+
|
|
1421
|
+
while (this.match(TokenType.STAR, TokenType.SLASH, TokenType.PERCENT)) {
|
|
1422
|
+
const operator = this.tokens[this.pos - 1].value;
|
|
1423
|
+
const right = this.parsePower();
|
|
1424
|
+
left = {
|
|
1425
|
+
type: NodeType.BinaryExpression,
|
|
1426
|
+
operator,
|
|
1427
|
+
left,
|
|
1428
|
+
right,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
return left;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
parsePower() {
|
|
1436
|
+
let left = this.parseUnary();
|
|
1437
|
+
|
|
1438
|
+
if (this.match(TokenType.POWER)) {
|
|
1439
|
+
const right = this.parsePower(); // Right associative
|
|
1440
|
+
left = {
|
|
1441
|
+
type: NodeType.BinaryExpression,
|
|
1442
|
+
operator: '**',
|
|
1443
|
+
left,
|
|
1444
|
+
right,
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return left;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
parseUnary() {
|
|
1452
|
+
if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.BITNOT)) {
|
|
1453
|
+
const operator = this.tokens[this.pos - 1].value;
|
|
1454
|
+
const argument = this.parseUnary();
|
|
1455
|
+
return {
|
|
1456
|
+
type: NodeType.UnaryExpression,
|
|
1457
|
+
operator: operator === 'not' ? '!' : operator,
|
|
1458
|
+
argument,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (this.match(TokenType.AWAIT)) {
|
|
1463
|
+
const argument = this.parseUnary();
|
|
1464
|
+
return {
|
|
1465
|
+
type: NodeType.AwaitExpression,
|
|
1466
|
+
argument,
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (this.match(TokenType.SPREAD)) {
|
|
1471
|
+
const argument = this.parseUnary();
|
|
1472
|
+
return {
|
|
1473
|
+
type: NodeType.SpreadElement,
|
|
1474
|
+
argument,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
return this.parseCall();
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
parseCall() {
|
|
1482
|
+
let expr = this.parsePrimary();
|
|
1483
|
+
|
|
1484
|
+
while (true) {
|
|
1485
|
+
if (this.match(TokenType.LPAREN)) {
|
|
1486
|
+
const args = this.parseArgumentList();
|
|
1487
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
1488
|
+
expr = {
|
|
1489
|
+
type: NodeType.CallExpression,
|
|
1490
|
+
callee: expr,
|
|
1491
|
+
arguments: args,
|
|
1492
|
+
};
|
|
1493
|
+
} else if (this.match(TokenType.DOT)) {
|
|
1494
|
+
const property = this.expect(TokenType.IDENTIFIER, 'Expected property name').value;
|
|
1495
|
+
expr = {
|
|
1496
|
+
type: NodeType.MemberExpression,
|
|
1497
|
+
object: expr,
|
|
1498
|
+
property,
|
|
1499
|
+
computed: false,
|
|
1500
|
+
};
|
|
1501
|
+
} else if (this.match(TokenType.LBRACKET)) {
|
|
1502
|
+
const property = this.parseExpression();
|
|
1503
|
+
this.expect(TokenType.RBRACKET, 'Expected ]');
|
|
1504
|
+
expr = {
|
|
1505
|
+
type: NodeType.MemberExpression,
|
|
1506
|
+
object: expr,
|
|
1507
|
+
property,
|
|
1508
|
+
computed: true,
|
|
1509
|
+
};
|
|
1510
|
+
} else {
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
return expr;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
parseArgumentList() {
|
|
1519
|
+
const args = [];
|
|
1520
|
+
|
|
1521
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
1522
|
+
do {
|
|
1523
|
+
args.push(this.parseExpression());
|
|
1524
|
+
} while (this.match(TokenType.COMMA));
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
return args;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
parsePrimary() {
|
|
1531
|
+
// Literals
|
|
1532
|
+
if (this.check(TokenType.NUMBER)) {
|
|
1533
|
+
const token = this.advance();
|
|
1534
|
+
const raw = token.value;
|
|
1535
|
+
let value;
|
|
1536
|
+
if (raw.startsWith('0x') || raw.startsWith('0X')) {
|
|
1537
|
+
value = parseInt(raw, 16);
|
|
1538
|
+
} else if (raw.startsWith('0b') || raw.startsWith('0B')) {
|
|
1539
|
+
value = parseInt(raw.slice(2), 2);
|
|
1540
|
+
} else if (raw.startsWith('0o') || raw.startsWith('0O')) {
|
|
1541
|
+
value = parseInt(raw.slice(2), 8);
|
|
1542
|
+
} else {
|
|
1543
|
+
value = parseFloat(raw);
|
|
1544
|
+
}
|
|
1545
|
+
return {
|
|
1546
|
+
type: NodeType.Literal,
|
|
1547
|
+
value,
|
|
1548
|
+
raw,
|
|
1549
|
+
isNumber: true,
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if (this.check(TokenType.STRING)) {
|
|
1554
|
+
const token = this.advance();
|
|
1555
|
+
return {
|
|
1556
|
+
type: NodeType.Literal,
|
|
1557
|
+
value: token.value,
|
|
1558
|
+
raw: token.value,
|
|
1559
|
+
isString: true,
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if (this.check(TokenType.TEMPLATE_STRING)) {
|
|
1564
|
+
const token = this.advance();
|
|
1565
|
+
// Parse the template string with interpolation markers
|
|
1566
|
+
// Format: text\x00INTERP_START\x00expr\x00INTERP_END\x00text...
|
|
1567
|
+
const parts = [];
|
|
1568
|
+
const expressions = [];
|
|
1569
|
+
|
|
1570
|
+
// Split by markers and parse
|
|
1571
|
+
const segments = token.value.split(/\x00INTERP_START\x00|\x00INTERP_END\x00/);
|
|
1572
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1573
|
+
if (i % 2 === 0) {
|
|
1574
|
+
// Even indices are string parts
|
|
1575
|
+
parts.push(segments[i]);
|
|
1576
|
+
} else {
|
|
1577
|
+
// Odd indices are expression strings that need to be parsed
|
|
1578
|
+
const exprSource = segments[i];
|
|
1579
|
+
// Create a sub-lexer and parser for the expression
|
|
1580
|
+
// Lexer is imported at the top of the file
|
|
1581
|
+
const subLexer = new Lexer(exprSource);
|
|
1582
|
+
const subTokens = subLexer.tokenize();
|
|
1583
|
+
const subParser = new Parser(subTokens);
|
|
1584
|
+
const expr = subParser.parseExpression();
|
|
1585
|
+
expressions.push(expr);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
type: NodeType.TemplateLiteral,
|
|
1591
|
+
parts,
|
|
1592
|
+
expressions,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (this.check(TokenType.BOOLEAN)) {
|
|
1597
|
+
return {
|
|
1598
|
+
type: NodeType.Literal,
|
|
1599
|
+
value: this.advance().value === 'true',
|
|
1600
|
+
raw: this.tokens[this.pos - 1].value,
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (this.check(TokenType.NULL)) {
|
|
1605
|
+
this.advance();
|
|
1606
|
+
return {
|
|
1607
|
+
type: NodeType.Literal,
|
|
1608
|
+
value: null,
|
|
1609
|
+
raw: 'null',
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Regex literal
|
|
1614
|
+
if (this.check(TokenType.REGEX)) {
|
|
1615
|
+
const token = this.advance();
|
|
1616
|
+
return {
|
|
1617
|
+
type: NodeType.RegexLiteral,
|
|
1618
|
+
pattern: token.value.pattern,
|
|
1619
|
+
flags: token.value.flags,
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// JS block as expression: dec result = js(a, b) { return a + b; }
|
|
1624
|
+
if (this.check(TokenType.JS)) {
|
|
1625
|
+
return this.parseJSBlockExpression();
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Shell block as expression: dec result = shell { ls -la }
|
|
1629
|
+
if (this.check(TokenType.SHELL)) {
|
|
1630
|
+
return this.parseShellBlockExpression();
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// Arrow function with single param (no parens) - check before identifier
|
|
1634
|
+
if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.FAT_ARROW) {
|
|
1635
|
+
const param = this.advance().value;
|
|
1636
|
+
this.expect(TokenType.FAT_ARROW, 'Expected =>');
|
|
1637
|
+
let body;
|
|
1638
|
+
if (this.check(TokenType.LBRACE)) {
|
|
1639
|
+
body = this.parseBlock();
|
|
1640
|
+
} else {
|
|
1641
|
+
body = this.parseExpression();
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
type: NodeType.ArrowFunctionExpression,
|
|
1645
|
+
params: [{ name: param, defaultValue: null }],
|
|
1646
|
+
body,
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Identifier
|
|
1651
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
1652
|
+
const token = this.advance();
|
|
1653
|
+
return {
|
|
1654
|
+
type: NodeType.Identifier,
|
|
1655
|
+
name: token.value,
|
|
1656
|
+
line: token.line,
|
|
1657
|
+
column: token.column,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Grouped expression or arrow function
|
|
1662
|
+
if (this.match(TokenType.LPAREN)) {
|
|
1663
|
+
// Check for arrow function
|
|
1664
|
+
const startPos = this.pos;
|
|
1665
|
+
let isArrow = false;
|
|
1666
|
+
let params = [];
|
|
1667
|
+
|
|
1668
|
+
if (this.check(TokenType.RPAREN)) {
|
|
1669
|
+
this.advance();
|
|
1670
|
+
if (this.check(TokenType.FAT_ARROW)) {
|
|
1671
|
+
isArrow = true;
|
|
1672
|
+
} else {
|
|
1673
|
+
this.pos = startPos;
|
|
1674
|
+
}
|
|
1675
|
+
} else if (this.check(TokenType.IDENTIFIER)) {
|
|
1676
|
+
// Try to parse as parameter list
|
|
1677
|
+
const savedPos = this.pos;
|
|
1678
|
+
try {
|
|
1679
|
+
params = [];
|
|
1680
|
+
do {
|
|
1681
|
+
params.push(this.expect(TokenType.IDENTIFIER, '').value);
|
|
1682
|
+
} while (this.match(TokenType.COMMA));
|
|
1683
|
+
|
|
1684
|
+
if (this.match(TokenType.RPAREN) && this.check(TokenType.FAT_ARROW)) {
|
|
1685
|
+
isArrow = true;
|
|
1686
|
+
} else {
|
|
1687
|
+
this.pos = savedPos;
|
|
1688
|
+
}
|
|
1689
|
+
} catch {
|
|
1690
|
+
this.pos = savedPos;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (isArrow) {
|
|
1695
|
+
this.expect(TokenType.FAT_ARROW, 'Expected =>');
|
|
1696
|
+
let body;
|
|
1697
|
+
if (this.check(TokenType.LBRACE)) {
|
|
1698
|
+
body = this.parseBlock();
|
|
1699
|
+
} else {
|
|
1700
|
+
body = this.parseExpression();
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
type: NodeType.ArrowFunctionExpression,
|
|
1704
|
+
params: params.map(p => ({ name: p, defaultValue: null })),
|
|
1705
|
+
body,
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// Regular grouped expression
|
|
1710
|
+
const expr = this.parseExpression();
|
|
1711
|
+
this.expect(TokenType.RPAREN, 'Expected )');
|
|
1712
|
+
return expr;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// Array literal
|
|
1716
|
+
if (this.match(TokenType.LBRACKET)) {
|
|
1717
|
+
const elements = [];
|
|
1718
|
+
|
|
1719
|
+
if (!this.check(TokenType.RBRACKET)) {
|
|
1720
|
+
do {
|
|
1721
|
+
if (this.check(TokenType.RBRACKET)) break;
|
|
1722
|
+
elements.push(this.parseExpression());
|
|
1723
|
+
} while (this.match(TokenType.COMMA));
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
this.expect(TokenType.RBRACKET, 'Expected ]');
|
|
1727
|
+
return {
|
|
1728
|
+
type: NodeType.ArrayExpression,
|
|
1729
|
+
elements,
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Object literal
|
|
1734
|
+
if (this.match(TokenType.LBRACE)) {
|
|
1735
|
+
const properties = [];
|
|
1736
|
+
|
|
1737
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
1738
|
+
do {
|
|
1739
|
+
this.skipNewlines();
|
|
1740
|
+
if (this.check(TokenType.RBRACE)) break;
|
|
1741
|
+
|
|
1742
|
+
// Handle spread element: { ...obj }
|
|
1743
|
+
if (this.match(TokenType.SPREAD)) {
|
|
1744
|
+
const argument = this.parseUnary();
|
|
1745
|
+
properties.push({
|
|
1746
|
+
type: NodeType.SpreadElement,
|
|
1747
|
+
argument,
|
|
1748
|
+
});
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
let key;
|
|
1753
|
+
if (this.check(TokenType.STRING)) {
|
|
1754
|
+
key = this.advance().value;
|
|
1755
|
+
} else {
|
|
1756
|
+
key = this.expect(TokenType.IDENTIFIER, 'Expected property name').value;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
let value;
|
|
1760
|
+
if (this.match(TokenType.COLON)) {
|
|
1761
|
+
value = this.parseExpression();
|
|
1762
|
+
} else {
|
|
1763
|
+
// Shorthand property
|
|
1764
|
+
value = { type: NodeType.Identifier, name: key };
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
properties.push({
|
|
1768
|
+
type: NodeType.Property,
|
|
1769
|
+
key,
|
|
1770
|
+
value,
|
|
1771
|
+
shorthand: !this.tokens[this.pos - 1] || this.tokens[this.pos - 1].type !== TokenType.COLON,
|
|
1772
|
+
});
|
|
1773
|
+
} while (this.match(TokenType.COMMA));
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
this.skipNewlines();
|
|
1777
|
+
this.expect(TokenType.RBRACE, 'Expected }');
|
|
1778
|
+
return {
|
|
1779
|
+
type: NodeType.ObjectExpression,
|
|
1780
|
+
properties,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
this.error(`Unexpected token: ${this.peek().type}`);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// Testing framework parsing
|
|
1788
|
+
parseTestBlock() {
|
|
1789
|
+
this.expect(TokenType.TEST, 'Expected test');
|
|
1790
|
+
|
|
1791
|
+
// Test name (string)
|
|
1792
|
+
const name = this.expect(TokenType.STRING, 'Expected test name').value;
|
|
1793
|
+
|
|
1794
|
+
this.skipNewlines();
|
|
1795
|
+
|
|
1796
|
+
// Test body
|
|
1797
|
+
const body = this.parseBlock();
|
|
1798
|
+
|
|
1799
|
+
return {
|
|
1800
|
+
type: NodeType.TestBlock,
|
|
1801
|
+
name,
|
|
1802
|
+
body,
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
parseDescribeBlock() {
|
|
1807
|
+
this.expect(TokenType.DESCRIBE, 'Expected describe');
|
|
1808
|
+
|
|
1809
|
+
// Describe name (string)
|
|
1810
|
+
const name = this.expect(TokenType.STRING, 'Expected describe name').value;
|
|
1811
|
+
|
|
1812
|
+
this.skipNewlines();
|
|
1813
|
+
|
|
1814
|
+
// Describe body (contains tests and other statements)
|
|
1815
|
+
const body = this.parseBlock();
|
|
1816
|
+
|
|
1817
|
+
return {
|
|
1818
|
+
type: NodeType.DescribeBlock,
|
|
1819
|
+
name,
|
|
1820
|
+
body,
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
parseExpectStatement() {
|
|
1825
|
+
this.expect(TokenType.EXPECT, 'Expected expect');
|
|
1826
|
+
this.expect(TokenType.LPAREN, 'Expected ( after expect');
|
|
1827
|
+
|
|
1828
|
+
const actual = this.parseExpression();
|
|
1829
|
+
|
|
1830
|
+
this.expect(TokenType.RPAREN, 'Expected ) after expect value');
|
|
1831
|
+
this.expect(TokenType.DOT, 'Expected . after expect()');
|
|
1832
|
+
|
|
1833
|
+
// Parse matcher: toBe, toEqual, toContain, etc.
|
|
1834
|
+
const matcher = this.expect(TokenType.IDENTIFIER, 'Expected matcher name').value;
|
|
1835
|
+
|
|
1836
|
+
this.expect(TokenType.LPAREN, 'Expected ( after matcher');
|
|
1837
|
+
|
|
1838
|
+
// Parse expected value (optional for some matchers like toBeNull)
|
|
1839
|
+
let expected = null;
|
|
1840
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
1841
|
+
expected = this.parseExpression();
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
this.expect(TokenType.RPAREN, 'Expected ) after matcher value');
|
|
1845
|
+
|
|
1846
|
+
return {
|
|
1847
|
+
type: NodeType.ExpectStatement,
|
|
1848
|
+
actual,
|
|
1849
|
+
matcher,
|
|
1850
|
+
expected,
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
parseAssertStatement() {
|
|
1855
|
+
this.expect(TokenType.ASSERT, 'Expected assert');
|
|
1856
|
+
|
|
1857
|
+
const condition = this.parseExpression();
|
|
1858
|
+
|
|
1859
|
+
// Optional message
|
|
1860
|
+
let message = null;
|
|
1861
|
+
if (this.match(TokenType.COMMA)) {
|
|
1862
|
+
message = this.parseExpression();
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
return {
|
|
1866
|
+
type: NodeType.AssertStatement,
|
|
1867
|
+
condition,
|
|
1868
|
+
message,
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
export function parse(tokens) {
|
|
1874
|
+
const parser = new Parser(tokens);
|
|
1875
|
+
return parser.parse();
|
|
1876
|
+
}
|