IncludeCPP 3.3.20__py3-none-any.whl → 3.4.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1791 @@
1
+ """
2
+ CSSL Parser - Lexer and Parser for CSO Service Script Language
3
+
4
+ Features:
5
+ - Complete tokenization of CSSL syntax
6
+ - AST (Abstract Syntax Tree) generation
7
+ - Enhanced error reporting with line/column info
8
+ - Support for service files and standalone programs
9
+ - Special operators: <== (inject), ==> (receive), -> <- (flow)
10
+ - Module references (@Module) and self-references (s@Struct)
11
+ """
12
+
13
+ import re
14
+ from enum import Enum, auto
15
+ from dataclasses import dataclass, field
16
+ from typing import List, Dict, Any, Optional, Union
17
+
18
+
19
+ class CSSLSyntaxError(Exception):
20
+ """Syntax error with detailed location information"""
21
+
22
+ def __init__(self, message: str, line: int = 0, column: int = 0, source_line: str = ""):
23
+ self.line = line
24
+ self.column = column
25
+ self.source_line = source_line
26
+
27
+ # Build detailed error message
28
+ location = f" at line {line}" if line else ""
29
+ if column:
30
+ location += f", column {column}"
31
+
32
+ full_message = f"CSSL Syntax Error{location}: {message}"
33
+
34
+ if source_line:
35
+ full_message += f"\n {source_line}"
36
+ if column > 0:
37
+ full_message += f"\n {' ' * (column - 1)}^"
38
+
39
+ super().__init__(full_message)
40
+
41
+
42
+ class TokenType(Enum):
43
+ KEYWORD = auto()
44
+ IDENTIFIER = auto()
45
+ STRING = auto()
46
+ STRING_INTERP = auto() # <variable> in strings
47
+ NUMBER = auto()
48
+ BOOLEAN = auto()
49
+ NULL = auto()
50
+ TYPE_LITERAL = auto() # list, dict as type literals
51
+ TYPE_GENERIC = auto() # datastruct<T>, shuffled<T>, iterator<T>, combo<T>
52
+ OPERATOR = auto()
53
+ # Basic injection operators
54
+ INJECT_LEFT = auto() # <==
55
+ INJECT_RIGHT = auto() # ==>
56
+ # BruteForce Injection operators - Copy & Add
57
+ INJECT_PLUS_LEFT = auto() # +<==
58
+ INJECT_PLUS_RIGHT = auto() # ==>+
59
+ # BruteForce Injection operators - Move & Remove
60
+ INJECT_MINUS_LEFT = auto() # -<==
61
+ INJECT_MINUS_RIGHT = auto() # ===>-
62
+ # BruteForce Injection operators - Code Infusion
63
+ INFUSE_LEFT = auto() # <<==
64
+ INFUSE_RIGHT = auto() # ==>>
65
+ INFUSE_PLUS_LEFT = auto() # +<<==
66
+ INFUSE_PLUS_RIGHT = auto() # ==>>+
67
+ INFUSE_MINUS_LEFT = auto() # -<<==
68
+ INFUSE_MINUS_RIGHT = auto() # ==>>-
69
+ # Flow operators
70
+ FLOW_RIGHT = auto()
71
+ FLOW_LEFT = auto()
72
+ EQUALS = auto()
73
+ COMPARE_EQ = auto()
74
+ COMPARE_NE = auto()
75
+ COMPARE_LT = auto()
76
+ COMPARE_GT = auto()
77
+ COMPARE_LE = auto()
78
+ COMPARE_GE = auto()
79
+ PLUS = auto()
80
+ MINUS = auto()
81
+ MULTIPLY = auto()
82
+ DIVIDE = auto()
83
+ MODULO = auto()
84
+ AND = auto()
85
+ OR = auto()
86
+ NOT = auto()
87
+ AMPERSAND = auto() # & for references
88
+ BLOCK_START = auto()
89
+ BLOCK_END = auto()
90
+ PAREN_START = auto()
91
+ PAREN_END = auto()
92
+ BRACKET_START = auto()
93
+ BRACKET_END = auto()
94
+ SEMICOLON = auto()
95
+ COLON = auto()
96
+ DOUBLE_COLON = auto() # :: for injection helpers (string::where, json::key, etc)
97
+ COMMA = auto()
98
+ DOT = auto()
99
+ AT = auto()
100
+ GLOBAL_REF = auto() # r@<name> global variable declaration
101
+ SELF_REF = auto() # s@<name> self-reference to global struct
102
+ PACKAGE = auto()
103
+ PACKAGE_INCLUDES = auto()
104
+ AS = auto()
105
+ COMMENT = auto()
106
+ NEWLINE = auto()
107
+ EOF = auto()
108
+
109
+
110
+ KEYWORDS = {
111
+ # Service structure
112
+ 'service-init', 'service-run', 'service-include', 'struct', 'define', 'main',
113
+ # Control flow
114
+ 'if', 'else', 'elif', 'while', 'for', 'foreach', 'in', 'range',
115
+ 'switch', 'case', 'default', 'break', 'continue', 'return',
116
+ 'try', 'catch', 'finally', 'throw',
117
+ # Literals
118
+ 'True', 'False', 'null', 'None', 'true', 'false',
119
+ # Logical operators
120
+ 'and', 'or', 'not',
121
+ # Async/Events
122
+ 'start', 'stop', 'wait_for', 'on_event', 'emit_event', 'await',
123
+ # Package system
124
+ 'package', 'package-includes', 'exec', 'as', 'global',
125
+ # CSSL Type Keywords
126
+ 'int', 'string', 'float', 'bool', 'void', 'json', 'array', 'vector', 'stack',
127
+ 'dynamic', # No type declaration (slow but flexible)
128
+ 'undefined', # Function errors ignored
129
+ 'open', # Accept any parameter type
130
+ 'datastruct', # Universal container (lazy declarator)
131
+ 'dataspace', # SQL/data storage container
132
+ 'shuffled', # Unorganized fast storage (multiple returns)
133
+ 'iterator', # Advanced iterator with tasks
134
+ 'combo', # Filter/search spaces
135
+ 'structure', # Advanced C++/Py Class
136
+ 'openquote', # SQL openquote container
137
+ # CSSL Function Modifiers
138
+ 'meta', # Source function (must return)
139
+ 'super', # Force execution (no exceptions)
140
+ 'closed', # Protect from external injection
141
+ 'private', # Disable all injections
142
+ 'virtual', # Import cycle safe
143
+ 'sqlbased', # SQL-based function
144
+ # CSSL Include Keywords
145
+ 'include', 'get',
146
+ }
147
+
148
+ # Type literals that create empty instances
149
+ TYPE_LITERALS = {'list', 'dict'}
150
+
151
+ # Generic type keywords that use <T> syntax
152
+ TYPE_GENERICS = {
153
+ 'datastruct', 'dataspace', 'shuffled', 'iterator', 'combo',
154
+ 'vector', 'stack', 'array', 'openquote'
155
+ }
156
+
157
+ # Injection helper prefixes (type::helper=value)
158
+ INJECTION_HELPERS = {
159
+ 'string', 'integer', 'json', 'array', 'vector', 'combo', 'dynamic', 'sql'
160
+ }
161
+
162
+
163
+ @dataclass
164
+ class Token:
165
+ type: TokenType
166
+ value: Any
167
+ line: int
168
+ column: int
169
+
170
+
171
+ class CSSLLexer:
172
+ """Tokenizes CSSL source code into a stream of tokens."""
173
+
174
+ def __init__(self, source: str):
175
+ self.source = source
176
+ self.pos = 0
177
+ self.line = 1
178
+ self.column = 1
179
+ self.tokens: List[Token] = []
180
+ # Store source lines for error messages
181
+ self.source_lines = source.split('\n')
182
+
183
+ def get_source_line(self, line_num: int) -> str:
184
+ """Get a specific source line for error reporting"""
185
+ if 0 < line_num <= len(self.source_lines):
186
+ return self.source_lines[line_num - 1]
187
+ return ""
188
+
189
+ def error(self, message: str):
190
+ """Raise a syntax error with location info"""
191
+ raise CSSLSyntaxError(
192
+ message,
193
+ line=self.line,
194
+ column=self.column,
195
+ source_line=self.get_source_line(self.line)
196
+ )
197
+
198
+ def tokenize(self) -> List[Token]:
199
+ while self.pos < len(self.source):
200
+ self._skip_whitespace()
201
+ if self.pos >= len(self.source):
202
+ break
203
+
204
+ char = self.source[self.pos]
205
+
206
+ # Comments: both # and // style
207
+ if char == '#':
208
+ self._skip_comment()
209
+ elif char == '/' and self._peek(1) == '/':
210
+ # C-style // comment - NEW
211
+ self._skip_comment()
212
+ elif char == '\n':
213
+ self._add_token(TokenType.NEWLINE, '\n')
214
+ self._advance()
215
+ self.line += 1
216
+ self.column = 1
217
+ elif char in '"\'':
218
+ self._read_string(char)
219
+ elif char == '`':
220
+ # Raw string (no escape processing) - useful for JSON
221
+ self._read_raw_string()
222
+ elif char.isdigit() or (char == '-' and self._peek(1).isdigit()):
223
+ self._read_number()
224
+ elif char == 'r' and self._peek(1) == '@':
225
+ # r@<name> global variable declaration (same as 'global')
226
+ self._read_global_ref()
227
+ elif char == 's' and self._peek(1) == '@':
228
+ # s@<name> self-reference to global struct
229
+ self._read_self_ref()
230
+ elif char.isalpha() or char == '_':
231
+ self._read_identifier()
232
+ elif char == '@':
233
+ self._add_token(TokenType.AT, '@')
234
+ self._advance()
235
+ elif char == '&':
236
+ # & for references
237
+ if self._peek(1) == '&':
238
+ self._add_token(TokenType.AND, '&&')
239
+ self._advance()
240
+ self._advance()
241
+ else:
242
+ self._add_token(TokenType.AMPERSAND, '&')
243
+ self._advance()
244
+ elif char == '{':
245
+ self._add_token(TokenType.BLOCK_START, '{')
246
+ self._advance()
247
+ elif char == '}':
248
+ self._add_token(TokenType.BLOCK_END, '}')
249
+ self._advance()
250
+ elif char == '(':
251
+ self._add_token(TokenType.PAREN_START, '(')
252
+ self._advance()
253
+ elif char == ')':
254
+ self._add_token(TokenType.PAREN_END, ')')
255
+ self._advance()
256
+ elif char == '[':
257
+ self._add_token(TokenType.BRACKET_START, '[')
258
+ self._advance()
259
+ elif char == ']':
260
+ self._add_token(TokenType.BRACKET_END, ']')
261
+ self._advance()
262
+ elif char == ';':
263
+ self._add_token(TokenType.SEMICOLON, ';')
264
+ self._advance()
265
+ elif char == ':':
266
+ # Check for :: (double colon for injection helpers)
267
+ if self._peek(1) == ':':
268
+ self._add_token(TokenType.DOUBLE_COLON, '::')
269
+ self._advance()
270
+ self._advance()
271
+ else:
272
+ self._add_token(TokenType.COLON, ':')
273
+ self._advance()
274
+ elif char == ',':
275
+ self._add_token(TokenType.COMMA, ',')
276
+ self._advance()
277
+ elif char == '.':
278
+ self._add_token(TokenType.DOT, '.')
279
+ self._advance()
280
+ elif char == '+':
281
+ # Check for BruteForce Injection: +<== or +<<==
282
+ if self._peek(1) == '<' and self._peek(2) == '<' and self._peek(3) == '=' and self._peek(4) == '=':
283
+ self._add_token(TokenType.INFUSE_PLUS_LEFT, '+<<==')
284
+ for _ in range(5): self._advance()
285
+ elif self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
286
+ self._add_token(TokenType.INJECT_PLUS_LEFT, '+<==')
287
+ for _ in range(4): self._advance()
288
+ else:
289
+ self._add_token(TokenType.PLUS, '+')
290
+ self._advance()
291
+ elif char == '*':
292
+ self._add_token(TokenType.MULTIPLY, '*')
293
+ self._advance()
294
+ elif char == '/':
295
+ # Check if this is a // comment (handled above) or division
296
+ if self._peek(1) != '/':
297
+ self._add_token(TokenType.DIVIDE, '/')
298
+ self._advance()
299
+ else:
300
+ # Already handled by // comment check above, but just in case
301
+ self._skip_comment()
302
+ elif char == '%':
303
+ self._add_token(TokenType.MODULO, '%')
304
+ self._advance()
305
+ elif char == '<':
306
+ self._read_less_than()
307
+ elif char == '>':
308
+ self._read_greater_than()
309
+ elif char == '=':
310
+ self._read_equals()
311
+ elif char == '!':
312
+ self._read_not()
313
+ elif char == '-':
314
+ self._read_minus()
315
+ elif char == '|':
316
+ if self._peek(1) == '|':
317
+ self._add_token(TokenType.OR, '||')
318
+ self._advance()
319
+ self._advance()
320
+ else:
321
+ self._advance()
322
+ else:
323
+ self._advance()
324
+
325
+ self._add_token(TokenType.EOF, '')
326
+ return self.tokens
327
+
328
+ def _advance(self):
329
+ self.pos += 1
330
+ self.column += 1
331
+
332
+ def _peek(self, offset=0) -> str:
333
+ pos = self.pos + offset
334
+ if pos < len(self.source):
335
+ return self.source[pos]
336
+ return ''
337
+
338
+ def _add_token(self, token_type: TokenType, value: Any):
339
+ self.tokens.append(Token(token_type, value, self.line, self.column))
340
+
341
+ def _skip_whitespace(self):
342
+ while self.pos < len(self.source) and self.source[self.pos] in ' \t\r':
343
+ self._advance()
344
+
345
+ def _skip_comment(self):
346
+ while self.pos < len(self.source) and self.source[self.pos] != '\n':
347
+ self._advance()
348
+
349
+ def _read_string(self, quote_char: str):
350
+ self._advance()
351
+ start = self.pos
352
+ result = []
353
+ while self.pos < len(self.source) and self.source[self.pos] != quote_char:
354
+ if self.source[self.pos] == '\\' and self.pos + 1 < len(self.source):
355
+ # Handle escape sequences
356
+ next_char = self.source[self.pos + 1]
357
+ if next_char == 'n':
358
+ result.append('\n')
359
+ elif next_char == 't':
360
+ result.append('\t')
361
+ elif next_char == 'r':
362
+ result.append('\r')
363
+ elif next_char == '\\':
364
+ result.append('\\')
365
+ elif next_char == quote_char:
366
+ result.append(quote_char)
367
+ elif next_char == '"':
368
+ result.append('"')
369
+ elif next_char == "'":
370
+ result.append("'")
371
+ else:
372
+ result.append(self.source[self.pos])
373
+ result.append(next_char)
374
+ self._advance()
375
+ self._advance()
376
+ else:
377
+ result.append(self.source[self.pos])
378
+ self._advance()
379
+ value = ''.join(result)
380
+ self._add_token(TokenType.STRING, value)
381
+ self._advance()
382
+
383
+ def _read_raw_string(self):
384
+ """Read raw string with backticks - no escape processing.
385
+
386
+ Useful for JSON: `{"id": "2819e1", "name": "test"}`
387
+ """
388
+ self._advance() # Skip opening backtick
389
+ start = self.pos
390
+ while self.pos < len(self.source) and self.source[self.pos] != '`':
391
+ if self.source[self.pos] == '\n':
392
+ self.line += 1
393
+ self.column = 0
394
+ self._advance()
395
+ value = self.source[start:self.pos]
396
+ self._add_token(TokenType.STRING, value)
397
+ self._advance() # Skip closing backtick
398
+
399
+ def _read_number(self):
400
+ start = self.pos
401
+ if self.source[self.pos] == '-':
402
+ self._advance()
403
+ while self.pos < len(self.source) and (self.source[self.pos].isdigit() or self.source[self.pos] == '.'):
404
+ self._advance()
405
+ value = self.source[start:self.pos]
406
+ if '.' in value:
407
+ self._add_token(TokenType.NUMBER, float(value))
408
+ else:
409
+ self._add_token(TokenType.NUMBER, int(value))
410
+
411
+ def _read_identifier(self):
412
+ start = self.pos
413
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
414
+ self._advance()
415
+ value = self.source[start:self.pos]
416
+
417
+ if value in ('True', 'true'):
418
+ self._add_token(TokenType.BOOLEAN, True)
419
+ elif value in ('False', 'false'):
420
+ self._add_token(TokenType.BOOLEAN, False)
421
+ elif value in ('null', 'None', 'none'):
422
+ self._add_token(TokenType.NULL, None)
423
+ elif value in TYPE_LITERALS:
424
+ # NEW: list and dict as type literals (e.g., cache = list;)
425
+ self._add_token(TokenType.TYPE_LITERAL, value)
426
+ elif value == 'as':
427
+ # NEW: 'as' keyword for foreach ... as ... syntax
428
+ self._add_token(TokenType.AS, value)
429
+ elif value in KEYWORDS:
430
+ self._add_token(TokenType.KEYWORD, value)
431
+ else:
432
+ self._add_token(TokenType.IDENTIFIER, value)
433
+
434
+ def _read_self_ref(self):
435
+ """Read s@<name> or s@<name>.<member>... self-reference"""
436
+ start = self.pos
437
+ self._advance() # skip 's'
438
+ self._advance() # skip '@'
439
+
440
+ # Read the identifier path (Name.Member.SubMember)
441
+ path_parts = []
442
+ while self.pos < len(self.source):
443
+ # Read identifier part
444
+ part_start = self.pos
445
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
446
+ self._advance()
447
+ if self.pos > part_start:
448
+ path_parts.append(self.source[part_start:self.pos])
449
+
450
+ # Check for dot to continue path
451
+ if self.pos < len(self.source) and self.source[self.pos] == '.':
452
+ self._advance() # skip '.'
453
+ else:
454
+ break
455
+
456
+ value = '.'.join(path_parts)
457
+ self._add_token(TokenType.SELF_REF, value)
458
+
459
+ def _read_global_ref(self):
460
+ """Read r@<name> global variable declaration (equivalent to 'global')"""
461
+ start = self.pos
462
+ self._advance() # skip 'r'
463
+ self._advance() # skip '@'
464
+
465
+ # Read the identifier
466
+ name_start = self.pos
467
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
468
+ self._advance()
469
+
470
+ value = self.source[name_start:self.pos]
471
+ self._add_token(TokenType.GLOBAL_REF, value)
472
+
473
+ def _read_less_than(self):
474
+ # Check for <<== (code infusion left)
475
+ if self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
476
+ self._add_token(TokenType.INFUSE_LEFT, '<<==')
477
+ for _ in range(4): self._advance()
478
+ # Check for <== (basic injection left)
479
+ elif self._peek(1) == '=' and self._peek(2) == '=':
480
+ self._add_token(TokenType.INJECT_LEFT, '<==')
481
+ for _ in range(3): self._advance()
482
+ elif self._peek(1) == '=':
483
+ self._add_token(TokenType.COMPARE_LE, '<=')
484
+ self._advance()
485
+ self._advance()
486
+ elif self._peek(1) == '-':
487
+ self._add_token(TokenType.FLOW_LEFT, '<-')
488
+ self._advance()
489
+ self._advance()
490
+ else:
491
+ self._add_token(TokenType.COMPARE_LT, '<')
492
+ self._advance()
493
+
494
+ def _read_greater_than(self):
495
+ if self._peek(1) == '=':
496
+ self._add_token(TokenType.COMPARE_GE, '>=')
497
+ self._advance()
498
+ self._advance()
499
+ else:
500
+ self._add_token(TokenType.COMPARE_GT, '>')
501
+ self._advance()
502
+
503
+ def _read_equals(self):
504
+ # Check for ==>>+ (code infusion right plus)
505
+ if self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>' and self._peek(4) == '+':
506
+ self._add_token(TokenType.INFUSE_PLUS_RIGHT, '==>>+')
507
+ for _ in range(5): self._advance()
508
+ # Check for ==>>- (code infusion right minus)
509
+ elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>' and self._peek(4) == '-':
510
+ self._add_token(TokenType.INFUSE_MINUS_RIGHT, '==>>-')
511
+ for _ in range(5): self._advance()
512
+ # Check for ==>> (code infusion right)
513
+ elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>':
514
+ self._add_token(TokenType.INFUSE_RIGHT, '==>>')
515
+ for _ in range(4): self._advance()
516
+ # Check for ==>+ (injection right plus)
517
+ elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '+':
518
+ self._add_token(TokenType.INJECT_PLUS_RIGHT, '==>+')
519
+ for _ in range(4): self._advance()
520
+ # Check for ===>- (injection right minus - moves & removes)
521
+ elif self._peek(1) == '=' and self._peek(2) == '=' and self._peek(3) == '>' and self._peek(4) == '-':
522
+ self._add_token(TokenType.INJECT_MINUS_RIGHT, '===>')
523
+ for _ in range(5): self._advance()
524
+ # Check for ==> (basic injection right)
525
+ elif self._peek(1) == '=' and self._peek(2) == '>':
526
+ self._add_token(TokenType.INJECT_RIGHT, '==>')
527
+ for _ in range(3): self._advance()
528
+ elif self._peek(1) == '=':
529
+ self._add_token(TokenType.COMPARE_EQ, '==')
530
+ self._advance()
531
+ self._advance()
532
+ else:
533
+ self._add_token(TokenType.EQUALS, '=')
534
+ self._advance()
535
+
536
+ def _read_not(self):
537
+ if self._peek(1) == '=':
538
+ self._add_token(TokenType.COMPARE_NE, '!=')
539
+ self._advance()
540
+ self._advance()
541
+ else:
542
+ self._add_token(TokenType.NOT, '!')
543
+ self._advance()
544
+
545
+ def _read_minus(self):
546
+ # Check for -<<== (code infusion minus left)
547
+ if self._peek(1) == '<' and self._peek(2) == '<' and self._peek(3) == '=' and self._peek(4) == '=':
548
+ self._add_token(TokenType.INFUSE_MINUS_LEFT, '-<<==')
549
+ for _ in range(5): self._advance()
550
+ # Check for -<== (injection minus left - move & remove)
551
+ elif self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
552
+ self._add_token(TokenType.INJECT_MINUS_LEFT, '-<==')
553
+ for _ in range(4): self._advance()
554
+ # Check for -==> (injection right minus)
555
+ elif self._peek(1) == '=' and self._peek(2) == '=' and self._peek(3) == '>':
556
+ self._add_token(TokenType.INJECT_MINUS_RIGHT, '-==>')
557
+ for _ in range(4): self._advance()
558
+ elif self._peek(1) == '>':
559
+ self._add_token(TokenType.FLOW_RIGHT, '->')
560
+ self._advance()
561
+ self._advance()
562
+ else:
563
+ self._add_token(TokenType.MINUS, '-')
564
+ self._advance()
565
+
566
+
567
+ @dataclass
568
+ class ASTNode:
569
+ type: str
570
+ value: Any = None
571
+ children: List['ASTNode'] = field(default_factory=list)
572
+ line: int = 0
573
+ column: int = 0
574
+
575
+
576
+ class CSSLParser:
577
+ """Parses CSSL tokens into an Abstract Syntax Tree."""
578
+
579
+ def __init__(self, tokens: List[Token], source_lines: List[str] = None):
580
+ self.tokens = [t for t in tokens if t.type != TokenType.NEWLINE]
581
+ self.pos = 0
582
+ self.source_lines = source_lines or []
583
+
584
+ def get_source_line(self, line_num: int) -> str:
585
+ """Get a specific source line for error reporting"""
586
+ if 0 < line_num <= len(self.source_lines):
587
+ return self.source_lines[line_num - 1]
588
+ return ""
589
+
590
+ def error(self, message: str, token: Token = None):
591
+ """Raise a syntax error with location info"""
592
+ if token is None:
593
+ token = self._current()
594
+ raise CSSLSyntaxError(
595
+ message,
596
+ line=token.line,
597
+ column=token.column,
598
+ source_line=self.get_source_line(token.line)
599
+ )
600
+
601
+ def parse(self) -> ASTNode:
602
+ """Parse a service file (wrapped in braces)"""
603
+ root = ASTNode('service', children=[])
604
+
605
+ if not self._match(TokenType.BLOCK_START):
606
+ self.error(f"Expected '{{' at start of service, got {self._current().type.name}")
607
+
608
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
609
+ if self._match_keyword('service-init'):
610
+ root.children.append(self._parse_service_init())
611
+ elif self._match_keyword('service-include'):
612
+ root.children.append(self._parse_service_include())
613
+ elif self._match_keyword('service-run'):
614
+ root.children.append(self._parse_service_run())
615
+ # NEW: package block support
616
+ elif self._match_keyword('package'):
617
+ root.children.append(self._parse_package())
618
+ # NEW: package-includes block support
619
+ elif self._match_keyword('package-includes'):
620
+ root.children.append(self._parse_package_includes())
621
+ # NEW: struct at top level
622
+ elif self._match_keyword('struct'):
623
+ root.children.append(self._parse_struct())
624
+ # NEW: define at top level
625
+ elif self._match_keyword('define'):
626
+ root.children.append(self._parse_define())
627
+ else:
628
+ self._advance()
629
+
630
+ self._match(TokenType.BLOCK_END)
631
+ return root
632
+
633
+ def _is_function_modifier(self, value: str) -> bool:
634
+ """Check if a keyword is a function modifier"""
635
+ return value in ('undefined', 'open', 'meta', 'super', 'closed', 'private', 'virtual', 'sqlbased')
636
+
637
+ def _is_type_keyword(self, value: str) -> bool:
638
+ """Check if a keyword is a type declaration"""
639
+ return value in ('int', 'string', 'float', 'bool', 'void', 'json', 'array', 'vector', 'stack',
640
+ 'dynamic', 'datastruct', 'dataspace', 'shuffled', 'iterator', 'combo', 'structure')
641
+
642
+ def _looks_like_function_declaration(self) -> bool:
643
+ """Check if current position looks like a C-style function declaration.
644
+
645
+ Patterns:
646
+ - int funcName(...)
647
+ - undefined int funcName(...)
648
+ - vector<string> funcName(...)
649
+ - undefined void funcName(...)
650
+ """
651
+ saved_pos = self.pos
652
+
653
+ # Skip modifiers (undefined, open, meta, super, closed, private, virtual)
654
+ while self._check(TokenType.KEYWORD) and self._is_function_modifier(self._current().value):
655
+ self._advance()
656
+
657
+ # Check for type keyword
658
+ if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
659
+ self._advance()
660
+
661
+ # Skip generic type parameters <T>
662
+ if self._check(TokenType.COMPARE_LT):
663
+ depth = 1
664
+ self._advance()
665
+ while depth > 0 and not self._is_at_end():
666
+ if self._check(TokenType.COMPARE_LT):
667
+ depth += 1
668
+ elif self._check(TokenType.COMPARE_GT):
669
+ depth -= 1
670
+ self._advance()
671
+
672
+ # Check for identifier followed by (
673
+ if self._check(TokenType.IDENTIFIER):
674
+ self._advance()
675
+ is_func = self._check(TokenType.PAREN_START)
676
+ self.pos = saved_pos
677
+ return is_func
678
+
679
+ self.pos = saved_pos
680
+ return False
681
+
682
+ def _parse_typed_function(self) -> ASTNode:
683
+ """Parse C-style typed function declaration.
684
+
685
+ Patterns:
686
+ - int Add(int a, int b) { }
687
+ - undefined int Func() { }
688
+ - open void Handler(open Params) { }
689
+ - vector<string> GetNames() { }
690
+ """
691
+ modifiers = []
692
+ return_type = None
693
+ generic_type = None
694
+
695
+ # Collect modifiers (undefined, open, meta, super, closed, private, virtual)
696
+ while self._check(TokenType.KEYWORD) and self._is_function_modifier(self._current().value):
697
+ modifiers.append(self._advance().value)
698
+
699
+ # Get return type
700
+ if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
701
+ return_type = self._advance().value
702
+
703
+ # Check for generic type <T>
704
+ if self._check(TokenType.COMPARE_LT):
705
+ self._advance() # skip <
706
+ generic_parts = []
707
+ depth = 1
708
+ while depth > 0 and not self._is_at_end():
709
+ if self._check(TokenType.COMPARE_LT):
710
+ depth += 1
711
+ generic_parts.append('<')
712
+ elif self._check(TokenType.COMPARE_GT):
713
+ depth -= 1
714
+ if depth > 0:
715
+ generic_parts.append('>')
716
+ elif self._check(TokenType.COMMA):
717
+ generic_parts.append(',')
718
+ else:
719
+ generic_parts.append(self._current().value)
720
+ self._advance()
721
+ generic_type = ''.join(generic_parts)
722
+
723
+ # Get function name
724
+ name = self._advance().value
725
+
726
+ # Parse parameters
727
+ params = []
728
+ self._expect(TokenType.PAREN_START)
729
+
730
+ while not self._check(TokenType.PAREN_END) and not self._is_at_end():
731
+ param_info = {}
732
+
733
+ # Handle 'open' keyword for open parameters
734
+ if self._match_keyword('open'):
735
+ param_info['open'] = True
736
+
737
+ # Handle type annotations
738
+ if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
739
+ param_info['type'] = self._advance().value
740
+
741
+ # Check for generic type parameter <T>
742
+ if self._check(TokenType.COMPARE_LT):
743
+ self._advance()
744
+ generic_parts = []
745
+ depth = 1
746
+ while depth > 0 and not self._is_at_end():
747
+ if self._check(TokenType.COMPARE_LT):
748
+ depth += 1
749
+ generic_parts.append('<')
750
+ elif self._check(TokenType.COMPARE_GT):
751
+ depth -= 1
752
+ if depth > 0:
753
+ generic_parts.append('>')
754
+ elif self._check(TokenType.COMMA):
755
+ generic_parts.append(',')
756
+ else:
757
+ generic_parts.append(self._current().value)
758
+ self._advance()
759
+ param_info['generic'] = ''.join(generic_parts)
760
+
761
+ # Handle reference operator &
762
+ if self._match(TokenType.AMPERSAND):
763
+ param_info['ref'] = True
764
+
765
+ # Get parameter name
766
+ if self._check(TokenType.IDENTIFIER):
767
+ param_name = self._advance().value
768
+ if param_info:
769
+ params.append({'name': param_name, **param_info})
770
+ else:
771
+ params.append(param_name)
772
+ elif self._check(TokenType.KEYWORD):
773
+ # Parameter name could be a keyword
774
+ param_name = self._advance().value
775
+ if param_info:
776
+ params.append({'name': param_name, **param_info})
777
+ else:
778
+ params.append(param_name)
779
+
780
+ self._match(TokenType.COMMA)
781
+
782
+ self._expect(TokenType.PAREN_END)
783
+
784
+ # Parse function body
785
+ node = ASTNode('function', value={
786
+ 'name': name,
787
+ 'params': params,
788
+ 'return_type': return_type,
789
+ 'generic_type': generic_type,
790
+ 'modifiers': modifiers
791
+ }, children=[])
792
+
793
+ self._expect(TokenType.BLOCK_START)
794
+
795
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
796
+ stmt = self._parse_statement()
797
+ if stmt:
798
+ node.children.append(stmt)
799
+
800
+ self._expect(TokenType.BLOCK_END)
801
+ return node
802
+
803
+ def parse_program(self) -> ASTNode:
804
+ """Parse a standalone program (no service wrapper)"""
805
+ root = ASTNode('program', children=[])
806
+
807
+ while not self._is_at_end():
808
+ if self._match_keyword('struct'):
809
+ root.children.append(self._parse_struct())
810
+ elif self._match_keyword('define'):
811
+ root.children.append(self._parse_define())
812
+ # Check for C-style typed function declarations
813
+ elif self._looks_like_function_declaration():
814
+ root.children.append(self._parse_typed_function())
815
+ # Handle service blocks
816
+ elif self._match_keyword('service-init'):
817
+ root.children.append(self._parse_service_init())
818
+ elif self._match_keyword('service-include'):
819
+ root.children.append(self._parse_service_include())
820
+ elif self._match_keyword('service-run'):
821
+ root.children.append(self._parse_service_run())
822
+ elif self._match_keyword('package'):
823
+ root.children.append(self._parse_package())
824
+ elif self._match_keyword('package-includes'):
825
+ root.children.append(self._parse_package_includes())
826
+ # Handle global declarations
827
+ elif self._match_keyword('global'):
828
+ stmt = self._parse_expression_statement()
829
+ if stmt:
830
+ root.children.append(stmt)
831
+ elif self._check(TokenType.GLOBAL_REF):
832
+ stmt = self._parse_expression_statement()
833
+ if stmt:
834
+ root.children.append(stmt)
835
+ # Handle statements
836
+ elif self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or self._check(TokenType.SELF_REF):
837
+ stmt = self._parse_expression_statement()
838
+ if stmt:
839
+ root.children.append(stmt)
840
+ elif self._match_keyword('if'):
841
+ root.children.append(self._parse_if())
842
+ elif self._match_keyword('while'):
843
+ root.children.append(self._parse_while())
844
+ elif self._match_keyword('for'):
845
+ root.children.append(self._parse_for())
846
+ elif self._match_keyword('foreach'):
847
+ root.children.append(self._parse_foreach())
848
+ # Skip comments and newlines
849
+ elif self._check(TokenType.COMMENT) or self._check(TokenType.NEWLINE):
850
+ self._advance()
851
+ else:
852
+ self._advance()
853
+
854
+ return root
855
+
856
+ def _current(self) -> Token:
857
+ if self.pos < len(self.tokens):
858
+ return self.tokens[self.pos]
859
+ return Token(TokenType.EOF, '', 0, 0)
860
+
861
+ def _peek(self, offset=0) -> Token:
862
+ pos = self.pos + offset
863
+ if pos < len(self.tokens):
864
+ return self.tokens[pos]
865
+ return Token(TokenType.EOF, '', 0, 0)
866
+
867
+ def _advance(self) -> Token:
868
+ token = self._current()
869
+ self.pos += 1
870
+ return token
871
+
872
+ def _is_at_end(self) -> bool:
873
+ return self._current().type == TokenType.EOF
874
+
875
+ def _check(self, token_type: TokenType) -> bool:
876
+ return self._current().type == token_type
877
+
878
+ def _match(self, token_type: TokenType) -> bool:
879
+ if self._check(token_type):
880
+ self._advance()
881
+ return True
882
+ return False
883
+
884
+ def _match_keyword(self, keyword: str) -> bool:
885
+ if self._current().type == TokenType.KEYWORD and self._current().value == keyword:
886
+ self._advance()
887
+ return True
888
+ return False
889
+
890
+ def _expect(self, token_type: TokenType, message: str = None):
891
+ if not self._match(token_type):
892
+ msg = message or f"Expected {token_type.name}, got {self._current().type.name}"
893
+ self.error(msg)
894
+ return self.tokens[self.pos - 1]
895
+
896
+ def _parse_service_init(self) -> ASTNode:
897
+ node = ASTNode('service-init', children=[])
898
+ self._expect(TokenType.BLOCK_START)
899
+
900
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
901
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
902
+ key = self._advance().value
903
+ self._expect(TokenType.COLON)
904
+ value = self._parse_value()
905
+ node.children.append(ASTNode('property', value={'key': key, 'value': value}))
906
+ self._match(TokenType.SEMICOLON)
907
+ else:
908
+ self._advance()
909
+
910
+ self._expect(TokenType.BLOCK_END)
911
+ return node
912
+
913
+ def _parse_service_include(self) -> ASTNode:
914
+ """Parse service-include block for importing modules and files
915
+
916
+ Syntax:
917
+ service-include {
918
+ @KernelClient <== get(include(cso_root('/root32/etc/tasks/kernel.cssl')));
919
+ @Time <== get('time');
920
+ @Secrets <== get('secrets');
921
+ }
922
+ """
923
+ node = ASTNode('service-include', children=[])
924
+ self._expect(TokenType.BLOCK_START)
925
+
926
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
927
+ # Parse module injection statements like @ModuleName <== get(...);
928
+ if self._check(TokenType.AT):
929
+ stmt = self._parse_expression_statement()
930
+ if stmt:
931
+ node.children.append(stmt)
932
+ elif self._check(TokenType.IDENTIFIER):
933
+ # Also support identifier-based assignments: moduleName <== get(...);
934
+ stmt = self._parse_expression_statement()
935
+ if stmt:
936
+ node.children.append(stmt)
937
+ else:
938
+ self._advance()
939
+
940
+ self._expect(TokenType.BLOCK_END)
941
+ return node
942
+
943
+ def _parse_service_run(self) -> ASTNode:
944
+ node = ASTNode('service-run', children=[])
945
+ self._expect(TokenType.BLOCK_START)
946
+
947
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
948
+ if self._match_keyword('struct'):
949
+ node.children.append(self._parse_struct())
950
+ elif self._match_keyword('define'):
951
+ node.children.append(self._parse_define())
952
+ else:
953
+ self._advance()
954
+
955
+ self._expect(TokenType.BLOCK_END)
956
+ return node
957
+
958
+ def _parse_package(self) -> ASTNode:
959
+ """Parse package {} block for service metadata - NEW
960
+
961
+ Syntax:
962
+ package {
963
+ service = "ServiceName";
964
+ exec = @Start();
965
+ version = "1.0.0";
966
+ description = "Beschreibung";
967
+ }
968
+ """
969
+ node = ASTNode('package', children=[])
970
+ self._expect(TokenType.BLOCK_START)
971
+
972
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
973
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
974
+ key = self._advance().value
975
+ self._expect(TokenType.EQUALS)
976
+ value = self._parse_expression()
977
+ node.children.append(ASTNode('package_property', value={'key': key, 'value': value}))
978
+ self._match(TokenType.SEMICOLON)
979
+ else:
980
+ self._advance()
981
+
982
+ self._expect(TokenType.BLOCK_END)
983
+ return node
984
+
985
+ def _parse_package_includes(self) -> ASTNode:
986
+ """Parse package-includes {} block for imports - NEW
987
+
988
+ Syntax:
989
+ package-includes {
990
+ @Lists = get('list');
991
+ @OS = get('os');
992
+ @Time = get('time');
993
+ @VSRam = get('vsramsdk');
994
+ }
995
+ """
996
+ node = ASTNode('package-includes', children=[])
997
+ self._expect(TokenType.BLOCK_START)
998
+
999
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1000
+ # Parse module injection statements like @ModuleName = get(...);
1001
+ if self._check(TokenType.AT):
1002
+ stmt = self._parse_expression_statement()
1003
+ if stmt:
1004
+ node.children.append(stmt)
1005
+ elif self._check(TokenType.IDENTIFIER):
1006
+ # Also support identifier-based assignments
1007
+ stmt = self._parse_expression_statement()
1008
+ if stmt:
1009
+ node.children.append(stmt)
1010
+ else:
1011
+ self._advance()
1012
+
1013
+ self._expect(TokenType.BLOCK_END)
1014
+ return node
1015
+
1016
+ def _parse_struct(self) -> ASTNode:
1017
+ name = self._advance().value
1018
+ is_global = False
1019
+
1020
+ # Check for (@) decorator: struct Name(@) { ... }
1021
+ if self._match(TokenType.PAREN_START):
1022
+ if self._check(TokenType.AT):
1023
+ self._advance() # skip @
1024
+ is_global = True
1025
+ self._expect(TokenType.PAREN_END)
1026
+
1027
+ node = ASTNode('struct', value={'name': name, 'global': is_global}, children=[])
1028
+ self._expect(TokenType.BLOCK_START)
1029
+
1030
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1031
+ if self._match_keyword('define'):
1032
+ node.children.append(self._parse_define())
1033
+ elif self._check(TokenType.IDENTIFIER):
1034
+ # Look ahead to determine what kind of statement this is
1035
+ saved_pos = self.pos
1036
+ var_name = self._advance().value
1037
+
1038
+ if self._match(TokenType.INJECT_LEFT):
1039
+ # Injection: var <== expr
1040
+ value = self._parse_expression()
1041
+ node.children.append(ASTNode('injection', value={'name': var_name, 'source': value}))
1042
+ self._match(TokenType.SEMICOLON)
1043
+ elif self._match(TokenType.EQUALS):
1044
+ # Assignment: var = expr
1045
+ value = self._parse_expression()
1046
+ node.children.append(ASTNode('assignment', value={'name': var_name, 'value': value}))
1047
+ self._match(TokenType.SEMICOLON)
1048
+ elif self._check(TokenType.PAREN_START):
1049
+ # Function call: func(args)
1050
+ self.pos = saved_pos # Go back to parse full expression
1051
+ stmt = self._parse_expression_statement()
1052
+ if stmt:
1053
+ node.children.append(stmt)
1054
+ elif self._match(TokenType.DOT):
1055
+ # Method call: obj.method(args)
1056
+ self.pos = saved_pos # Go back to parse full expression
1057
+ stmt = self._parse_expression_statement()
1058
+ if stmt:
1059
+ node.children.append(stmt)
1060
+ else:
1061
+ self._match(TokenType.SEMICOLON)
1062
+ elif self._check(TokenType.AT):
1063
+ # Module reference statement
1064
+ stmt = self._parse_expression_statement()
1065
+ if stmt:
1066
+ node.children.append(stmt)
1067
+ else:
1068
+ self._advance()
1069
+
1070
+ self._expect(TokenType.BLOCK_END)
1071
+ return node
1072
+
1073
+ def _parse_define(self) -> ASTNode:
1074
+ name = self._advance().value
1075
+ params = []
1076
+
1077
+ if self._match(TokenType.PAREN_START):
1078
+ while not self._check(TokenType.PAREN_END):
1079
+ param_info = {}
1080
+ # Handle 'open' keyword for open parameters
1081
+ if self._match_keyword('open'):
1082
+ param_info['open'] = True
1083
+ # Handle type annotations (e.g., string, int, dynamic, etc.)
1084
+ if self._check(TokenType.KEYWORD):
1085
+ param_info['type'] = self._advance().value
1086
+ # Handle reference operator &
1087
+ if self._match(TokenType.AMPERSAND):
1088
+ param_info['ref'] = True
1089
+ # Get parameter name
1090
+ if self._check(TokenType.IDENTIFIER):
1091
+ param_name = self._advance().value
1092
+ if param_info:
1093
+ params.append({'name': param_name, **param_info})
1094
+ else:
1095
+ params.append(param_name)
1096
+ self._match(TokenType.COMMA)
1097
+ elif self._check(TokenType.KEYWORD):
1098
+ # Parameter name could be a keyword like 'Params'
1099
+ param_name = self._advance().value
1100
+ if param_info:
1101
+ params.append({'name': param_name, **param_info})
1102
+ else:
1103
+ params.append(param_name)
1104
+ self._match(TokenType.COMMA)
1105
+ else:
1106
+ break
1107
+ self._expect(TokenType.PAREN_END)
1108
+
1109
+ node = ASTNode('function', value={'name': name, 'params': params}, children=[])
1110
+ self._expect(TokenType.BLOCK_START)
1111
+
1112
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1113
+ stmt = self._parse_statement()
1114
+ if stmt:
1115
+ node.children.append(stmt)
1116
+
1117
+ self._expect(TokenType.BLOCK_END)
1118
+ return node
1119
+
1120
+ def _parse_statement(self) -> Optional[ASTNode]:
1121
+ if self._match_keyword('if'):
1122
+ return self._parse_if()
1123
+ elif self._match_keyword('while'):
1124
+ return self._parse_while()
1125
+ elif self._match_keyword('for'):
1126
+ return self._parse_for()
1127
+ elif self._match_keyword('foreach'):
1128
+ return self._parse_foreach()
1129
+ elif self._match_keyword('switch'):
1130
+ return self._parse_switch()
1131
+ elif self._match_keyword('return'):
1132
+ return self._parse_return()
1133
+ elif self._match_keyword('break'):
1134
+ self._match(TokenType.SEMICOLON)
1135
+ return ASTNode('break')
1136
+ elif self._match_keyword('continue'):
1137
+ self._match(TokenType.SEMICOLON)
1138
+ return ASTNode('continue')
1139
+ elif self._match_keyword('try'):
1140
+ return self._parse_try()
1141
+ elif self._match_keyword('await'):
1142
+ return self._parse_await()
1143
+ elif self._match_keyword('define'):
1144
+ # Nested define function
1145
+ return self._parse_define()
1146
+ elif self._looks_like_function_declaration():
1147
+ # Nested typed function (e.g., void Level2() { ... })
1148
+ return self._parse_typed_function()
1149
+ elif self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT):
1150
+ return self._parse_expression_statement()
1151
+ else:
1152
+ self._advance()
1153
+ return None
1154
+
1155
+ def _parse_if(self) -> ASTNode:
1156
+ self._expect(TokenType.PAREN_START)
1157
+ condition = self._parse_expression()
1158
+ self._expect(TokenType.PAREN_END)
1159
+
1160
+ node = ASTNode('if', value={'condition': condition}, children=[])
1161
+
1162
+ self._expect(TokenType.BLOCK_START)
1163
+ then_block = ASTNode('then', children=[])
1164
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1165
+ stmt = self._parse_statement()
1166
+ if stmt:
1167
+ then_block.children.append(stmt)
1168
+ self._expect(TokenType.BLOCK_END)
1169
+ node.children.append(then_block)
1170
+
1171
+ if self._match_keyword('else'):
1172
+ else_block = ASTNode('else', children=[])
1173
+ if self._match_keyword('if'):
1174
+ else_block.children.append(self._parse_if())
1175
+ else:
1176
+ self._expect(TokenType.BLOCK_START)
1177
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1178
+ stmt = self._parse_statement()
1179
+ if stmt:
1180
+ else_block.children.append(stmt)
1181
+ self._expect(TokenType.BLOCK_END)
1182
+ node.children.append(else_block)
1183
+
1184
+ return node
1185
+
1186
+ def _parse_while(self) -> ASTNode:
1187
+ self._expect(TokenType.PAREN_START)
1188
+ condition = self._parse_expression()
1189
+ self._expect(TokenType.PAREN_END)
1190
+
1191
+ node = ASTNode('while', value={'condition': condition}, children=[])
1192
+ self._expect(TokenType.BLOCK_START)
1193
+
1194
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1195
+ stmt = self._parse_statement()
1196
+ if stmt:
1197
+ node.children.append(stmt)
1198
+
1199
+ self._expect(TokenType.BLOCK_END)
1200
+ return node
1201
+
1202
+ def _parse_for(self) -> ASTNode:
1203
+ self._expect(TokenType.PAREN_START)
1204
+ var_name = self._advance().value
1205
+ self._expect(TokenType.KEYWORD)
1206
+ self._expect(TokenType.KEYWORD)
1207
+ self._expect(TokenType.PAREN_START)
1208
+ start = self._parse_expression()
1209
+ self._expect(TokenType.COMMA)
1210
+ end = self._parse_expression()
1211
+ self._expect(TokenType.PAREN_END)
1212
+ self._expect(TokenType.PAREN_END)
1213
+
1214
+ node = ASTNode('for', value={'var': var_name, 'start': start, 'end': end}, children=[])
1215
+ self._expect(TokenType.BLOCK_START)
1216
+
1217
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1218
+ stmt = self._parse_statement()
1219
+ if stmt:
1220
+ node.children.append(stmt)
1221
+
1222
+ self._expect(TokenType.BLOCK_END)
1223
+ return node
1224
+
1225
+ def _parse_foreach(self) -> ASTNode:
1226
+ """Parse foreach loop - supports both syntaxes:
1227
+
1228
+ Traditional: foreach (var in iterable) { }
1229
+ New 'as' syntax: foreach iterable as var { }
1230
+ """
1231
+ # Check if this is the new 'as' syntax or traditional syntax
1232
+ if self._check(TokenType.PAREN_START):
1233
+ # Traditional syntax: foreach (var in iterable) { }
1234
+ self._expect(TokenType.PAREN_START)
1235
+ var_name = self._advance().value
1236
+ self._match_keyword('in')
1237
+ iterable = self._parse_expression()
1238
+ self._expect(TokenType.PAREN_END)
1239
+ else:
1240
+ # NEW: 'as' syntax: foreach iterable as var { }
1241
+ iterable = self._parse_expression()
1242
+ if self._check(TokenType.AS):
1243
+ self._advance() # consume 'as'
1244
+ else:
1245
+ self._match_keyword('as') # try keyword match as fallback
1246
+ var_name = self._advance().value
1247
+
1248
+ node = ASTNode('foreach', value={'var': var_name, 'iterable': iterable}, children=[])
1249
+ self._expect(TokenType.BLOCK_START)
1250
+
1251
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1252
+ stmt = self._parse_statement()
1253
+ if stmt:
1254
+ node.children.append(stmt)
1255
+
1256
+ self._expect(TokenType.BLOCK_END)
1257
+ return node
1258
+
1259
+ def _parse_switch(self) -> ASTNode:
1260
+ self._expect(TokenType.PAREN_START)
1261
+ value = self._parse_expression()
1262
+ self._expect(TokenType.PAREN_END)
1263
+
1264
+ node = ASTNode('switch', value={'value': value}, children=[])
1265
+ self._expect(TokenType.BLOCK_START)
1266
+
1267
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1268
+ if self._match_keyword('case'):
1269
+ case_value = self._parse_expression()
1270
+ self._expect(TokenType.COLON)
1271
+ case_node = ASTNode('case', value={'value': case_value}, children=[])
1272
+
1273
+ while not self._check_keyword('case') and not self._check_keyword('default') and not self._check(TokenType.BLOCK_END):
1274
+ stmt = self._parse_statement()
1275
+ if stmt:
1276
+ case_node.children.append(stmt)
1277
+ if self._check_keyword('break'):
1278
+ break
1279
+
1280
+ node.children.append(case_node)
1281
+ elif self._match_keyword('default'):
1282
+ self._expect(TokenType.COLON)
1283
+ default_node = ASTNode('default', children=[])
1284
+
1285
+ while not self._check(TokenType.BLOCK_END):
1286
+ stmt = self._parse_statement()
1287
+ if stmt:
1288
+ default_node.children.append(stmt)
1289
+
1290
+ node.children.append(default_node)
1291
+ else:
1292
+ self._advance()
1293
+
1294
+ self._expect(TokenType.BLOCK_END)
1295
+ return node
1296
+
1297
+ def _parse_return(self) -> ASTNode:
1298
+ value = None
1299
+ if not self._check(TokenType.SEMICOLON) and not self._check(TokenType.BLOCK_END):
1300
+ value = self._parse_expression()
1301
+ self._match(TokenType.SEMICOLON)
1302
+ return ASTNode('return', value=value)
1303
+
1304
+ def _parse_try(self) -> ASTNode:
1305
+ node = ASTNode('try', children=[])
1306
+
1307
+ try_block = ASTNode('try-block', children=[])
1308
+ self._expect(TokenType.BLOCK_START)
1309
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1310
+ stmt = self._parse_statement()
1311
+ if stmt:
1312
+ try_block.children.append(stmt)
1313
+ self._expect(TokenType.BLOCK_END)
1314
+ node.children.append(try_block)
1315
+
1316
+ if self._match_keyword('catch'):
1317
+ error_var = None
1318
+ if self._match(TokenType.PAREN_START):
1319
+ error_var = self._advance().value
1320
+ self._expect(TokenType.PAREN_END)
1321
+
1322
+ catch_block = ASTNode('catch-block', value={'error_var': error_var}, children=[])
1323
+ self._expect(TokenType.BLOCK_START)
1324
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1325
+ stmt = self._parse_statement()
1326
+ if stmt:
1327
+ catch_block.children.append(stmt)
1328
+ self._expect(TokenType.BLOCK_END)
1329
+ node.children.append(catch_block)
1330
+
1331
+ return node
1332
+
1333
+ def _parse_await(self) -> ASTNode:
1334
+ """Parse await statement: await expression;"""
1335
+ expr = self._parse_expression()
1336
+ self._match(TokenType.SEMICOLON)
1337
+ return ASTNode('await', value=expr)
1338
+
1339
+ def _parse_action_block(self) -> ASTNode:
1340
+ """Parse an action block { ... } containing statements for createcmd"""
1341
+ node = ASTNode('action_block', children=[])
1342
+ self._expect(TokenType.BLOCK_START)
1343
+
1344
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1345
+ # Check for define statements inside action block
1346
+ if self._match_keyword('define'):
1347
+ node.children.append(self._parse_define())
1348
+ # Check for typed function definitions (nested functions)
1349
+ elif self._looks_like_function_declaration():
1350
+ node.children.append(self._parse_typed_function())
1351
+ else:
1352
+ stmt = self._parse_statement()
1353
+ if stmt:
1354
+ node.children.append(stmt)
1355
+
1356
+ self._expect(TokenType.BLOCK_END)
1357
+ return node
1358
+
1359
+ def _parse_injection_filter(self) -> Optional[dict]:
1360
+ """Parse injection filter: [type::helper=value]"""
1361
+ if not self._match(TokenType.BRACKET_START):
1362
+ return None
1363
+
1364
+ filter_info = {}
1365
+ # Parse type::helper=value patterns
1366
+ while not self._check(TokenType.BRACKET_END) and not self._is_at_end():
1367
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
1368
+ filter_type = self._advance().value
1369
+ if self._match(TokenType.DOUBLE_COLON):
1370
+ helper = self._advance().value
1371
+ if self._match(TokenType.EQUALS):
1372
+ value = self._parse_expression()
1373
+ filter_info[f'{filter_type}::{helper}'] = value
1374
+ else:
1375
+ filter_info[f'{filter_type}::{helper}'] = True
1376
+ else:
1377
+ filter_info['type'] = filter_type
1378
+ elif self._check(TokenType.COMMA):
1379
+ self._advance()
1380
+ else:
1381
+ break
1382
+
1383
+ self._expect(TokenType.BRACKET_END)
1384
+ return filter_info if filter_info else None
1385
+
1386
+ def _parse_expression_statement(self) -> Optional[ASTNode]:
1387
+ expr = self._parse_expression()
1388
+
1389
+ # === BASIC INJECTION: <== (replace target with source) ===
1390
+ if self._match(TokenType.INJECT_LEFT):
1391
+ # Check if this is a createcmd injection with a code block
1392
+ is_createcmd = (
1393
+ expr.type == 'call' and
1394
+ expr.value.get('callee') and
1395
+ expr.value.get('callee').type == 'identifier' and
1396
+ expr.value.get('callee').value == 'createcmd'
1397
+ )
1398
+
1399
+ if is_createcmd and self._check(TokenType.BLOCK_START):
1400
+ action_block = self._parse_action_block()
1401
+ self._match(TokenType.SEMICOLON)
1402
+ return ASTNode('createcmd_inject', value={'command_call': expr, 'action': action_block})
1403
+ else:
1404
+ # Check for injection filter [type::helper=value]
1405
+ filter_info = self._parse_injection_filter()
1406
+ source = self._parse_expression()
1407
+ self._match(TokenType.SEMICOLON)
1408
+ return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'replace', 'filter': filter_info})
1409
+
1410
+ # === PLUS INJECTION: +<== (copy & add to target) ===
1411
+ if self._match(TokenType.INJECT_PLUS_LEFT):
1412
+ filter_info = self._parse_injection_filter()
1413
+ source = self._parse_expression()
1414
+ self._match(TokenType.SEMICOLON)
1415
+ return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'add', 'filter': filter_info})
1416
+
1417
+ # === MINUS INJECTION: -<== (move & remove from source) ===
1418
+ if self._match(TokenType.INJECT_MINUS_LEFT):
1419
+ filter_info = self._parse_injection_filter()
1420
+ source = self._parse_expression()
1421
+ self._match(TokenType.SEMICOLON)
1422
+ return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'move', 'filter': filter_info})
1423
+
1424
+ # === CODE INFUSION: <<== (inject code into function) ===
1425
+ if self._match(TokenType.INFUSE_LEFT):
1426
+ if self._check(TokenType.BLOCK_START):
1427
+ code_block = self._parse_action_block()
1428
+ self._match(TokenType.SEMICOLON)
1429
+ return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'replace'})
1430
+ else:
1431
+ source = self._parse_expression()
1432
+ self._match(TokenType.SEMICOLON)
1433
+ return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'replace'})
1434
+
1435
+ # === CODE INFUSION PLUS: +<<== (add code to function) ===
1436
+ if self._match(TokenType.INFUSE_PLUS_LEFT):
1437
+ if self._check(TokenType.BLOCK_START):
1438
+ code_block = self._parse_action_block()
1439
+ self._match(TokenType.SEMICOLON)
1440
+ return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'add'})
1441
+ else:
1442
+ source = self._parse_expression()
1443
+ self._match(TokenType.SEMICOLON)
1444
+ return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'add'})
1445
+
1446
+ # === CODE INFUSION MINUS: -<<== (remove code from function) ===
1447
+ if self._match(TokenType.INFUSE_MINUS_LEFT):
1448
+ if self._check(TokenType.BLOCK_START):
1449
+ code_block = self._parse_action_block()
1450
+ self._match(TokenType.SEMICOLON)
1451
+ return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'remove'})
1452
+ else:
1453
+ source = self._parse_expression()
1454
+ self._match(TokenType.SEMICOLON)
1455
+ return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'remove'})
1456
+
1457
+ # === RIGHT-SIDE OPERATORS ===
1458
+
1459
+ # === BASIC RECEIVE: ==> (move source to target) ===
1460
+ if self._match(TokenType.INJECT_RIGHT):
1461
+ filter_info = self._parse_injection_filter()
1462
+ target = self._parse_expression()
1463
+ self._match(TokenType.SEMICOLON)
1464
+ return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'replace', 'filter': filter_info})
1465
+
1466
+ # === PLUS RECEIVE: ==>+ (copy source to target) ===
1467
+ if self._match(TokenType.INJECT_PLUS_RIGHT):
1468
+ filter_info = self._parse_injection_filter()
1469
+ target = self._parse_expression()
1470
+ self._match(TokenType.SEMICOLON)
1471
+ return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'add', 'filter': filter_info})
1472
+
1473
+ # === MINUS RECEIVE: -==> (move & remove from source) ===
1474
+ if self._match(TokenType.INJECT_MINUS_RIGHT):
1475
+ filter_info = self._parse_injection_filter()
1476
+ target = self._parse_expression()
1477
+ self._match(TokenType.SEMICOLON)
1478
+ return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'move', 'filter': filter_info})
1479
+
1480
+ # === CODE INFUSION RIGHT: ==>> ===
1481
+ if self._match(TokenType.INFUSE_RIGHT):
1482
+ target = self._parse_expression()
1483
+ self._match(TokenType.SEMICOLON)
1484
+ return ASTNode('infuse_right', value={'source': expr, 'target': target, 'mode': 'replace'})
1485
+
1486
+ # === FLOW OPERATORS ===
1487
+ if self._match(TokenType.FLOW_RIGHT):
1488
+ target = self._parse_expression()
1489
+ self._match(TokenType.SEMICOLON)
1490
+ return ASTNode('flow', value={'source': expr, 'target': target})
1491
+
1492
+ if self._match(TokenType.FLOW_LEFT):
1493
+ source = self._parse_expression()
1494
+ self._match(TokenType.SEMICOLON)
1495
+ return ASTNode('flow', value={'source': source, 'target': expr})
1496
+
1497
+ # === BASIC ASSIGNMENT ===
1498
+ if self._match(TokenType.EQUALS):
1499
+ value = self._parse_expression()
1500
+ self._match(TokenType.SEMICOLON)
1501
+ return ASTNode('assignment', value={'target': expr, 'value': value})
1502
+
1503
+ self._match(TokenType.SEMICOLON)
1504
+ return ASTNode('expression', value=expr)
1505
+
1506
+ def _parse_expression(self) -> ASTNode:
1507
+ return self._parse_or()
1508
+
1509
+ def _parse_or(self) -> ASTNode:
1510
+ left = self._parse_and()
1511
+
1512
+ while self._match(TokenType.OR) or self._match_keyword('or'):
1513
+ right = self._parse_and()
1514
+ left = ASTNode('binary', value={'op': 'or', 'left': left, 'right': right})
1515
+
1516
+ return left
1517
+
1518
+ def _parse_and(self) -> ASTNode:
1519
+ left = self._parse_comparison()
1520
+
1521
+ while self._match(TokenType.AND) or self._match_keyword('and'):
1522
+ right = self._parse_comparison()
1523
+ left = ASTNode('binary', value={'op': 'and', 'left': left, 'right': right})
1524
+
1525
+ return left
1526
+
1527
+ def _parse_comparison(self) -> ASTNode:
1528
+ left = self._parse_term()
1529
+
1530
+ while True:
1531
+ if self._match(TokenType.COMPARE_EQ):
1532
+ right = self._parse_term()
1533
+ left = ASTNode('binary', value={'op': '==', 'left': left, 'right': right})
1534
+ elif self._match(TokenType.COMPARE_NE):
1535
+ right = self._parse_term()
1536
+ left = ASTNode('binary', value={'op': '!=', 'left': left, 'right': right})
1537
+ elif self._match(TokenType.COMPARE_LT):
1538
+ right = self._parse_term()
1539
+ left = ASTNode('binary', value={'op': '<', 'left': left, 'right': right})
1540
+ elif self._match(TokenType.COMPARE_GT):
1541
+ right = self._parse_term()
1542
+ left = ASTNode('binary', value={'op': '>', 'left': left, 'right': right})
1543
+ elif self._match(TokenType.COMPARE_LE):
1544
+ right = self._parse_term()
1545
+ left = ASTNode('binary', value={'op': '<=', 'left': left, 'right': right})
1546
+ elif self._match(TokenType.COMPARE_GE):
1547
+ right = self._parse_term()
1548
+ left = ASTNode('binary', value={'op': '>=', 'left': left, 'right': right})
1549
+ else:
1550
+ break
1551
+
1552
+ return left
1553
+
1554
+ def _parse_term(self) -> ASTNode:
1555
+ left = self._parse_factor()
1556
+
1557
+ while True:
1558
+ if self._match(TokenType.PLUS):
1559
+ right = self._parse_factor()
1560
+ left = ASTNode('binary', value={'op': '+', 'left': left, 'right': right})
1561
+ elif self._match(TokenType.MINUS):
1562
+ right = self._parse_factor()
1563
+ left = ASTNode('binary', value={'op': '-', 'left': left, 'right': right})
1564
+ else:
1565
+ break
1566
+
1567
+ return left
1568
+
1569
+ def _parse_factor(self) -> ASTNode:
1570
+ left = self._parse_unary()
1571
+
1572
+ while True:
1573
+ if self._match(TokenType.MULTIPLY):
1574
+ right = self._parse_unary()
1575
+ left = ASTNode('binary', value={'op': '*', 'left': left, 'right': right})
1576
+ elif self._match(TokenType.DIVIDE):
1577
+ right = self._parse_unary()
1578
+ left = ASTNode('binary', value={'op': '/', 'left': left, 'right': right})
1579
+ elif self._match(TokenType.MODULO):
1580
+ right = self._parse_unary()
1581
+ left = ASTNode('binary', value={'op': '%', 'left': left, 'right': right})
1582
+ else:
1583
+ break
1584
+
1585
+ return left
1586
+
1587
+ def _parse_unary(self) -> ASTNode:
1588
+ if self._match(TokenType.NOT) or self._match_keyword('not'):
1589
+ operand = self._parse_unary()
1590
+ return ASTNode('unary', value={'op': 'not', 'operand': operand})
1591
+ if self._match(TokenType.MINUS):
1592
+ operand = self._parse_unary()
1593
+ return ASTNode('unary', value={'op': '-', 'operand': operand})
1594
+ if self._match(TokenType.AMPERSAND):
1595
+ # Reference operator: &variable or &@module
1596
+ operand = self._parse_unary()
1597
+ return ASTNode('reference', value=operand)
1598
+
1599
+ return self._parse_primary()
1600
+
1601
+ def _parse_primary(self) -> ASTNode:
1602
+ if self._match(TokenType.AT):
1603
+ node = self._parse_module_reference()
1604
+ # Continue to check for calls, indexing, member access on module refs
1605
+ while True:
1606
+ if self._match(TokenType.PAREN_START):
1607
+ # Function call on module ref: @Module.method()
1608
+ args = []
1609
+ while not self._check(TokenType.PAREN_END) and not self._is_at_end():
1610
+ args.append(self._parse_expression())
1611
+ if not self._check(TokenType.PAREN_END):
1612
+ self._expect(TokenType.COMMA)
1613
+ self._expect(TokenType.PAREN_END)
1614
+ node = ASTNode('call', value={'callee': node, 'args': args})
1615
+ elif self._match(TokenType.DOT):
1616
+ # Member access: @Module.property
1617
+ member = self._advance().value
1618
+ node = ASTNode('member_access', value={'object': node, 'member': member})
1619
+ elif self._match(TokenType.BRACKET_START):
1620
+ # Index access: @Module[index]
1621
+ index = self._parse_expression()
1622
+ self._expect(TokenType.BRACKET_END)
1623
+ node = ASTNode('index_access', value={'object': node, 'index': index})
1624
+ else:
1625
+ break
1626
+ return node
1627
+
1628
+ if self._check(TokenType.SELF_REF):
1629
+ # s@<name> self-reference to global struct
1630
+ token = self._advance()
1631
+ node = ASTNode('self_ref', value=token.value, line=token.line, column=token.column)
1632
+ # Check for function call: s@Backend.Loop.Start()
1633
+ if self._match(TokenType.PAREN_START):
1634
+ args = []
1635
+ while not self._check(TokenType.PAREN_END):
1636
+ args.append(self._parse_expression())
1637
+ if not self._check(TokenType.PAREN_END):
1638
+ self._expect(TokenType.COMMA)
1639
+ self._expect(TokenType.PAREN_END)
1640
+ node = ASTNode('call', value={'callee': node, 'args': args})
1641
+ return node
1642
+
1643
+ if self._check(TokenType.NUMBER):
1644
+ return ASTNode('literal', value=self._advance().value)
1645
+
1646
+ if self._check(TokenType.STRING):
1647
+ return ASTNode('literal', value=self._advance().value)
1648
+
1649
+ if self._check(TokenType.BOOLEAN):
1650
+ return ASTNode('literal', value=self._advance().value)
1651
+
1652
+ if self._check(TokenType.NULL):
1653
+ self._advance()
1654
+ return ASTNode('literal', value=None)
1655
+
1656
+ # NEW: Type literals (list, dict) - create empty instances
1657
+ if self._check(TokenType.TYPE_LITERAL):
1658
+ type_name = self._advance().value
1659
+ return ASTNode('type_literal', value=type_name)
1660
+
1661
+ if self._match(TokenType.PAREN_START):
1662
+ expr = self._parse_expression()
1663
+ self._expect(TokenType.PAREN_END)
1664
+ return expr
1665
+
1666
+ if self._match(TokenType.BLOCK_START):
1667
+ return self._parse_object()
1668
+
1669
+ if self._match(TokenType.BRACKET_START):
1670
+ return self._parse_array()
1671
+
1672
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
1673
+ return self._parse_identifier_or_call()
1674
+
1675
+ return ASTNode('literal', value=None)
1676
+
1677
+ def _parse_module_reference(self) -> ASTNode:
1678
+ parts = []
1679
+ parts.append(self._advance().value)
1680
+
1681
+ while self._match(TokenType.DOT):
1682
+ parts.append(self._advance().value)
1683
+
1684
+ return ASTNode('module_ref', value='.'.join(parts))
1685
+
1686
+ def _parse_identifier_or_call(self) -> ASTNode:
1687
+ name = self._advance().value
1688
+ node = ASTNode('identifier', value=name)
1689
+
1690
+ while True:
1691
+ if self._match(TokenType.DOT):
1692
+ member = self._advance().value
1693
+ node = ASTNode('member_access', value={'object': node, 'member': member})
1694
+ elif self._match(TokenType.PAREN_START):
1695
+ args = []
1696
+ while not self._check(TokenType.PAREN_END):
1697
+ args.append(self._parse_expression())
1698
+ if not self._check(TokenType.PAREN_END):
1699
+ self._expect(TokenType.COMMA)
1700
+ self._expect(TokenType.PAREN_END)
1701
+ node = ASTNode('call', value={'callee': node, 'args': args})
1702
+ elif self._match(TokenType.BRACKET_START):
1703
+ index = self._parse_expression()
1704
+ self._expect(TokenType.BRACKET_END)
1705
+ node = ASTNode('index_access', value={'object': node, 'index': index})
1706
+ else:
1707
+ break
1708
+
1709
+ return node
1710
+
1711
+ def _parse_object(self) -> ASTNode:
1712
+ properties = {}
1713
+
1714
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1715
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.STRING):
1716
+ key = self._advance().value
1717
+ self._expect(TokenType.EQUALS)
1718
+ value = self._parse_expression()
1719
+ properties[key] = value
1720
+ self._match(TokenType.SEMICOLON)
1721
+ self._match(TokenType.COMMA)
1722
+ else:
1723
+ self._advance()
1724
+
1725
+ self._expect(TokenType.BLOCK_END)
1726
+ return ASTNode('object', value=properties)
1727
+
1728
+ def _parse_array(self) -> ASTNode:
1729
+ elements = []
1730
+
1731
+ while not self._check(TokenType.BRACKET_END) and not self._is_at_end():
1732
+ elements.append(self._parse_expression())
1733
+ if not self._check(TokenType.BRACKET_END):
1734
+ self._expect(TokenType.COMMA)
1735
+
1736
+ self._expect(TokenType.BRACKET_END)
1737
+ return ASTNode('array', value=elements)
1738
+
1739
+ def _parse_value(self) -> Any:
1740
+ if self._check(TokenType.STRING):
1741
+ return self._advance().value
1742
+ if self._check(TokenType.NUMBER):
1743
+ return self._advance().value
1744
+ if self._check(TokenType.BOOLEAN):
1745
+ return self._advance().value
1746
+ if self._check(TokenType.NULL):
1747
+ self._advance()
1748
+ return None
1749
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
1750
+ return self._advance().value
1751
+ return None
1752
+
1753
+ def _check_keyword(self, keyword: str) -> bool:
1754
+ return self._current().type == TokenType.KEYWORD and self._current().value == keyword
1755
+
1756
+
1757
+ def parse_cssl(source: str) -> ASTNode:
1758
+ """Parse CSSL source code into an AST - auto-detects service vs program format"""
1759
+ lexer = CSSLLexer(source)
1760
+ tokens = lexer.tokenize()
1761
+ parser = CSSLParser(tokens, lexer.source_lines)
1762
+
1763
+ # Auto-detect: if first token is '{', it's a service file
1764
+ # Otherwise treat as standalone program (whitespace is already filtered by lexer)
1765
+ if tokens and tokens[0].type == TokenType.BLOCK_START:
1766
+ return parser.parse() # Service file format
1767
+ else:
1768
+ return parser.parse_program() # Standalone program format
1769
+
1770
+
1771
+ def parse_cssl_program(source: str) -> ASTNode:
1772
+ """Parse standalone CSSL program (no service wrapper) into an AST"""
1773
+ lexer = CSSLLexer(source)
1774
+ tokens = lexer.tokenize()
1775
+ parser = CSSLParser(tokens, lexer.source_lines)
1776
+ return parser.parse_program()
1777
+
1778
+
1779
+ def tokenize_cssl(source: str) -> List[Token]:
1780
+ """Tokenize CSSL source code (useful for syntax highlighting)"""
1781
+ lexer = CSSLLexer(source)
1782
+ return lexer.tokenize()
1783
+
1784
+
1785
+ # Export public API
1786
+ __all__ = [
1787
+ 'TokenType', 'Token', 'ASTNode',
1788
+ 'CSSLLexer', 'CSSLParser', 'CSSLSyntaxError',
1789
+ 'parse_cssl', 'parse_cssl_program', 'tokenize_cssl',
1790
+ 'KEYWORDS', 'TYPE_LITERALS'
1791
+ ]