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.
Files changed (90) hide show
  1. package/.github/workflows/ci.yml +66 -0
  2. package/README.md +1547 -0
  3. package/create-kimchi-app/README.md +44 -0
  4. package/create-kimchi-app/index.js +214 -0
  5. package/create-kimchi-app/package.json +22 -0
  6. package/editors/README.md +121 -0
  7. package/editors/sublime/KimchiLang.sublime-syntax +138 -0
  8. package/editors/vscode/README.md +90 -0
  9. package/editors/vscode/kimchilang-1.1.0.vsix +0 -0
  10. package/editors/vscode/language-configuration.json +37 -0
  11. package/editors/vscode/package.json +55 -0
  12. package/editors/vscode/src/extension.js +354 -0
  13. package/editors/vscode/syntaxes/kimchi.tmLanguage.json +215 -0
  14. package/examples/api/client.km +36 -0
  15. package/examples/async_pipe.km +58 -0
  16. package/examples/basic.kimchi +109 -0
  17. package/examples/cli_framework/README.md +92 -0
  18. package/examples/cli_framework/calculator.km +61 -0
  19. package/examples/cli_framework/deploy.km +126 -0
  20. package/examples/cli_framework/greeter.km +26 -0
  21. package/examples/config.static +27 -0
  22. package/examples/config.static.js +10 -0
  23. package/examples/env_test.km +37 -0
  24. package/examples/fibonacci.kimchi +17 -0
  25. package/examples/greeter.km +15 -0
  26. package/examples/hello.js +1 -0
  27. package/examples/hello.kimchi +3 -0
  28. package/examples/js_interop.km +42 -0
  29. package/examples/logger_example.km +34 -0
  30. package/examples/memo_fibonacci.km +17 -0
  31. package/examples/myapp/lib/http.js +14 -0
  32. package/examples/myapp/lib/http.km +16 -0
  33. package/examples/myapp/main.km +16 -0
  34. package/examples/myapp/main_with_mock.km +42 -0
  35. package/examples/myapp/services/api.js +18 -0
  36. package/examples/myapp/services/api.km +18 -0
  37. package/examples/new_features.kimchi +52 -0
  38. package/examples/project_example.static +20 -0
  39. package/examples/readme_examples.km +240 -0
  40. package/examples/reduce_pattern_match.km +85 -0
  41. package/examples/regex_match.km +46 -0
  42. package/examples/sample.js +45 -0
  43. package/examples/sample.km +39 -0
  44. package/examples/secrets.static +35 -0
  45. package/examples/secrets.static.js +30 -0
  46. package/examples/shell-example.mjs +144 -0
  47. package/examples/shell_example.km +19 -0
  48. package/examples/stdlib_test.km +22 -0
  49. package/examples/test_example.km +69 -0
  50. package/examples/testing/README.md +88 -0
  51. package/examples/testing/http_client.km +18 -0
  52. package/examples/testing/math.km +48 -0
  53. package/examples/testing/math.test.km +93 -0
  54. package/examples/testing/user_service.km +29 -0
  55. package/examples/testing/user_service.test.km +72 -0
  56. package/examples/use-config.mjs +141 -0
  57. package/examples/use_config.km +13 -0
  58. package/install.sh +59 -0
  59. package/package.json +29 -0
  60. package/pantry/acorn/index.km +1 -0
  61. package/pantry/is_number/index.km +1 -0
  62. package/pantry/is_odd/index.km +2 -0
  63. package/project.static +6 -0
  64. package/src/cli.js +1245 -0
  65. package/src/generator.js +1241 -0
  66. package/src/index.js +141 -0
  67. package/src/js2km.js +568 -0
  68. package/src/lexer.js +822 -0
  69. package/src/linter.js +810 -0
  70. package/src/package-manager.js +307 -0
  71. package/src/parser.js +1876 -0
  72. package/src/static-parser.js +500 -0
  73. package/src/typechecker.js +950 -0
  74. package/stdlib/array.km +0 -0
  75. package/stdlib/bitwise.km +38 -0
  76. package/stdlib/console.km +49 -0
  77. package/stdlib/date.km +97 -0
  78. package/stdlib/function.km +44 -0
  79. package/stdlib/http.km +197 -0
  80. package/stdlib/http.md +333 -0
  81. package/stdlib/index.km +26 -0
  82. package/stdlib/json.km +17 -0
  83. package/stdlib/logger.js +114 -0
  84. package/stdlib/logger.km +104 -0
  85. package/stdlib/math.km +120 -0
  86. package/stdlib/object.km +41 -0
  87. package/stdlib/promise.km +33 -0
  88. package/stdlib/string.km +93 -0
  89. package/stdlib/testing.md +265 -0
  90. 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
+ }