IncludeCPP 3.3.20__py3-none-any.whl → 3.4.2__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,1493 @@
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.isdigit() or (char == '-' and self._peek(1).isdigit()):
220
+ self._read_number()
221
+ elif char == 'r' and self._peek(1) == '@':
222
+ # r@<name> global variable declaration (same as 'global')
223
+ self._read_global_ref()
224
+ elif char == 's' and self._peek(1) == '@':
225
+ # s@<name> self-reference to global struct
226
+ self._read_self_ref()
227
+ elif char.isalpha() or char == '_' or char == '-':
228
+ self._read_identifier()
229
+ elif char == '@':
230
+ self._add_token(TokenType.AT, '@')
231
+ self._advance()
232
+ elif char == '&':
233
+ # & for references
234
+ if self._peek(1) == '&':
235
+ self._add_token(TokenType.AND, '&&')
236
+ self._advance()
237
+ self._advance()
238
+ else:
239
+ self._add_token(TokenType.AMPERSAND, '&')
240
+ self._advance()
241
+ elif char == '{':
242
+ self._add_token(TokenType.BLOCK_START, '{')
243
+ self._advance()
244
+ elif char == '}':
245
+ self._add_token(TokenType.BLOCK_END, '}')
246
+ self._advance()
247
+ elif char == '(':
248
+ self._add_token(TokenType.PAREN_START, '(')
249
+ self._advance()
250
+ elif char == ')':
251
+ self._add_token(TokenType.PAREN_END, ')')
252
+ self._advance()
253
+ elif char == '[':
254
+ self._add_token(TokenType.BRACKET_START, '[')
255
+ self._advance()
256
+ elif char == ']':
257
+ self._add_token(TokenType.BRACKET_END, ']')
258
+ self._advance()
259
+ elif char == ';':
260
+ self._add_token(TokenType.SEMICOLON, ';')
261
+ self._advance()
262
+ elif char == ':':
263
+ # Check for :: (double colon for injection helpers)
264
+ if self._peek(1) == ':':
265
+ self._add_token(TokenType.DOUBLE_COLON, '::')
266
+ self._advance()
267
+ self._advance()
268
+ else:
269
+ self._add_token(TokenType.COLON, ':')
270
+ self._advance()
271
+ elif char == ',':
272
+ self._add_token(TokenType.COMMA, ',')
273
+ self._advance()
274
+ elif char == '.':
275
+ self._add_token(TokenType.DOT, '.')
276
+ self._advance()
277
+ elif char == '+':
278
+ # Check for BruteForce Injection: +<== or +<<==
279
+ if self._peek(1) == '<' and self._peek(2) == '<' and self._peek(3) == '=' and self._peek(4) == '=':
280
+ self._add_token(TokenType.INFUSE_PLUS_LEFT, '+<<==')
281
+ for _ in range(5): self._advance()
282
+ elif self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
283
+ self._add_token(TokenType.INJECT_PLUS_LEFT, '+<==')
284
+ for _ in range(4): self._advance()
285
+ else:
286
+ self._add_token(TokenType.PLUS, '+')
287
+ self._advance()
288
+ elif char == '*':
289
+ self._add_token(TokenType.MULTIPLY, '*')
290
+ self._advance()
291
+ elif char == '/':
292
+ # Check if this is a // comment (handled above) or division
293
+ if self._peek(1) != '/':
294
+ self._add_token(TokenType.DIVIDE, '/')
295
+ self._advance()
296
+ else:
297
+ # Already handled by // comment check above, but just in case
298
+ self._skip_comment()
299
+ elif char == '%':
300
+ self._add_token(TokenType.MODULO, '%')
301
+ self._advance()
302
+ elif char == '<':
303
+ self._read_less_than()
304
+ elif char == '>':
305
+ self._read_greater_than()
306
+ elif char == '=':
307
+ self._read_equals()
308
+ elif char == '!':
309
+ self._read_not()
310
+ elif char == '-':
311
+ self._read_minus()
312
+ elif char == '|':
313
+ if self._peek(1) == '|':
314
+ self._add_token(TokenType.OR, '||')
315
+ self._advance()
316
+ self._advance()
317
+ else:
318
+ self._advance()
319
+ else:
320
+ self._advance()
321
+
322
+ self._add_token(TokenType.EOF, '')
323
+ return self.tokens
324
+
325
+ def _advance(self):
326
+ self.pos += 1
327
+ self.column += 1
328
+
329
+ def _peek(self, offset=0) -> str:
330
+ pos = self.pos + offset
331
+ if pos < len(self.source):
332
+ return self.source[pos]
333
+ return ''
334
+
335
+ def _add_token(self, token_type: TokenType, value: Any):
336
+ self.tokens.append(Token(token_type, value, self.line, self.column))
337
+
338
+ def _skip_whitespace(self):
339
+ while self.pos < len(self.source) and self.source[self.pos] in ' \t\r':
340
+ self._advance()
341
+
342
+ def _skip_comment(self):
343
+ while self.pos < len(self.source) and self.source[self.pos] != '\n':
344
+ self._advance()
345
+
346
+ def _read_string(self, quote_char: str):
347
+ self._advance()
348
+ start = self.pos
349
+ while self.pos < len(self.source) and self.source[self.pos] != quote_char:
350
+ if self.source[self.pos] == '\\':
351
+ self._advance()
352
+ self._advance()
353
+ value = self.source[start:self.pos]
354
+ self._add_token(TokenType.STRING, value)
355
+ self._advance()
356
+
357
+ def _read_number(self):
358
+ start = self.pos
359
+ if self.source[self.pos] == '-':
360
+ self._advance()
361
+ while self.pos < len(self.source) and (self.source[self.pos].isdigit() or self.source[self.pos] == '.'):
362
+ self._advance()
363
+ value = self.source[start:self.pos]
364
+ if '.' in value:
365
+ self._add_token(TokenType.NUMBER, float(value))
366
+ else:
367
+ self._add_token(TokenType.NUMBER, int(value))
368
+
369
+ def _read_identifier(self):
370
+ start = self.pos
371
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] in '_-'):
372
+ self._advance()
373
+ value = self.source[start:self.pos]
374
+
375
+ if value in ('True', 'true'):
376
+ self._add_token(TokenType.BOOLEAN, True)
377
+ elif value in ('False', 'false'):
378
+ self._add_token(TokenType.BOOLEAN, False)
379
+ elif value in ('null', 'None', 'none'):
380
+ self._add_token(TokenType.NULL, None)
381
+ elif value in TYPE_LITERALS:
382
+ # NEW: list and dict as type literals (e.g., cache = list;)
383
+ self._add_token(TokenType.TYPE_LITERAL, value)
384
+ elif value == 'as':
385
+ # NEW: 'as' keyword for foreach ... as ... syntax
386
+ self._add_token(TokenType.AS, value)
387
+ elif value in KEYWORDS:
388
+ self._add_token(TokenType.KEYWORD, value)
389
+ else:
390
+ self._add_token(TokenType.IDENTIFIER, value)
391
+
392
+ def _read_self_ref(self):
393
+ """Read s@<name> or s@<name>.<member>... self-reference"""
394
+ start = self.pos
395
+ self._advance() # skip 's'
396
+ self._advance() # skip '@'
397
+
398
+ # Read the identifier path (Name.Member.SubMember)
399
+ path_parts = []
400
+ while self.pos < len(self.source):
401
+ # Read identifier part
402
+ part_start = self.pos
403
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
404
+ self._advance()
405
+ if self.pos > part_start:
406
+ path_parts.append(self.source[part_start:self.pos])
407
+
408
+ # Check for dot to continue path
409
+ if self.pos < len(self.source) and self.source[self.pos] == '.':
410
+ self._advance() # skip '.'
411
+ else:
412
+ break
413
+
414
+ value = '.'.join(path_parts)
415
+ self._add_token(TokenType.SELF_REF, value)
416
+
417
+ def _read_global_ref(self):
418
+ """Read r@<name> global variable declaration (equivalent to 'global')"""
419
+ start = self.pos
420
+ self._advance() # skip 'r'
421
+ self._advance() # skip '@'
422
+
423
+ # Read the identifier
424
+ name_start = self.pos
425
+ while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
426
+ self._advance()
427
+
428
+ value = self.source[name_start:self.pos]
429
+ self._add_token(TokenType.GLOBAL_REF, value)
430
+
431
+ def _read_less_than(self):
432
+ # Check for <<== (code infusion left)
433
+ if self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
434
+ self._add_token(TokenType.INFUSE_LEFT, '<<==')
435
+ for _ in range(4): self._advance()
436
+ # Check for <== (basic injection left)
437
+ elif self._peek(1) == '=' and self._peek(2) == '=':
438
+ self._add_token(TokenType.INJECT_LEFT, '<==')
439
+ for _ in range(3): self._advance()
440
+ elif self._peek(1) == '=':
441
+ self._add_token(TokenType.COMPARE_LE, '<=')
442
+ self._advance()
443
+ self._advance()
444
+ elif self._peek(1) == '-':
445
+ self._add_token(TokenType.FLOW_LEFT, '<-')
446
+ self._advance()
447
+ self._advance()
448
+ else:
449
+ self._add_token(TokenType.COMPARE_LT, '<')
450
+ self._advance()
451
+
452
+ def _read_greater_than(self):
453
+ if self._peek(1) == '=':
454
+ self._add_token(TokenType.COMPARE_GE, '>=')
455
+ self._advance()
456
+ self._advance()
457
+ else:
458
+ self._add_token(TokenType.COMPARE_GT, '>')
459
+ self._advance()
460
+
461
+ def _read_equals(self):
462
+ # Check for ==>>+ (code infusion right plus)
463
+ if self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>' and self._peek(4) == '+':
464
+ self._add_token(TokenType.INFUSE_PLUS_RIGHT, '==>>+')
465
+ for _ in range(5): self._advance()
466
+ # Check for ==>>- (code infusion right minus)
467
+ elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>' and self._peek(4) == '-':
468
+ self._add_token(TokenType.INFUSE_MINUS_RIGHT, '==>>-')
469
+ for _ in range(5): self._advance()
470
+ # Check for ==>> (code infusion right)
471
+ elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>':
472
+ self._add_token(TokenType.INFUSE_RIGHT, '==>>')
473
+ for _ in range(4): self._advance()
474
+ # Check for ==>+ (injection right plus)
475
+ elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '+':
476
+ self._add_token(TokenType.INJECT_PLUS_RIGHT, '==>+')
477
+ for _ in range(4): self._advance()
478
+ # Check for ===>- (injection right minus - moves & removes)
479
+ elif self._peek(1) == '=' and self._peek(2) == '=' and self._peek(3) == '>' and self._peek(4) == '-':
480
+ self._add_token(TokenType.INJECT_MINUS_RIGHT, '===>')
481
+ for _ in range(5): self._advance()
482
+ # Check for ==> (basic injection right)
483
+ elif self._peek(1) == '=' and self._peek(2) == '>':
484
+ self._add_token(TokenType.INJECT_RIGHT, '==>')
485
+ for _ in range(3): self._advance()
486
+ elif self._peek(1) == '=':
487
+ self._add_token(TokenType.COMPARE_EQ, '==')
488
+ self._advance()
489
+ self._advance()
490
+ else:
491
+ self._add_token(TokenType.EQUALS, '=')
492
+ self._advance()
493
+
494
+ def _read_not(self):
495
+ if self._peek(1) == '=':
496
+ self._add_token(TokenType.COMPARE_NE, '!=')
497
+ self._advance()
498
+ self._advance()
499
+ else:
500
+ self._add_token(TokenType.NOT, '!')
501
+ self._advance()
502
+
503
+ def _read_minus(self):
504
+ # Check for -<<== (code infusion minus left)
505
+ if self._peek(1) == '<' and self._peek(2) == '<' and self._peek(3) == '=' and self._peek(4) == '=':
506
+ self._add_token(TokenType.INFUSE_MINUS_LEFT, '-<<==')
507
+ for _ in range(5): self._advance()
508
+ # Check for -<== (injection minus left - move & remove)
509
+ elif self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
510
+ self._add_token(TokenType.INJECT_MINUS_LEFT, '-<==')
511
+ for _ in range(4): self._advance()
512
+ # Check for -==> (injection right minus)
513
+ elif self._peek(1) == '=' and self._peek(2) == '=' and self._peek(3) == '>':
514
+ self._add_token(TokenType.INJECT_MINUS_RIGHT, '-==>')
515
+ for _ in range(4): self._advance()
516
+ elif self._peek(1) == '>':
517
+ self._add_token(TokenType.FLOW_RIGHT, '->')
518
+ self._advance()
519
+ self._advance()
520
+ else:
521
+ self._add_token(TokenType.MINUS, '-')
522
+ self._advance()
523
+
524
+
525
+ @dataclass
526
+ class ASTNode:
527
+ type: str
528
+ value: Any = None
529
+ children: List['ASTNode'] = field(default_factory=list)
530
+ line: int = 0
531
+ column: int = 0
532
+
533
+
534
+ class CSSLParser:
535
+ """Parses CSSL tokens into an Abstract Syntax Tree."""
536
+
537
+ def __init__(self, tokens: List[Token], source_lines: List[str] = None):
538
+ self.tokens = [t for t in tokens if t.type != TokenType.NEWLINE]
539
+ self.pos = 0
540
+ self.source_lines = source_lines or []
541
+
542
+ def get_source_line(self, line_num: int) -> str:
543
+ """Get a specific source line for error reporting"""
544
+ if 0 < line_num <= len(self.source_lines):
545
+ return self.source_lines[line_num - 1]
546
+ return ""
547
+
548
+ def error(self, message: str, token: Token = None):
549
+ """Raise a syntax error with location info"""
550
+ if token is None:
551
+ token = self._current()
552
+ raise CSSLSyntaxError(
553
+ message,
554
+ line=token.line,
555
+ column=token.column,
556
+ source_line=self.get_source_line(token.line)
557
+ )
558
+
559
+ def parse(self) -> ASTNode:
560
+ """Parse a service file (wrapped in braces)"""
561
+ root = ASTNode('service', children=[])
562
+
563
+ if not self._match(TokenType.BLOCK_START):
564
+ self.error(f"Expected '{{' at start of service, got {self._current().type.name}")
565
+
566
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
567
+ if self._match_keyword('service-init'):
568
+ root.children.append(self._parse_service_init())
569
+ elif self._match_keyword('service-include'):
570
+ root.children.append(self._parse_service_include())
571
+ elif self._match_keyword('service-run'):
572
+ root.children.append(self._parse_service_run())
573
+ # NEW: package block support
574
+ elif self._match_keyword('package'):
575
+ root.children.append(self._parse_package())
576
+ # NEW: package-includes block support
577
+ elif self._match_keyword('package-includes'):
578
+ root.children.append(self._parse_package_includes())
579
+ # NEW: struct at top level
580
+ elif self._match_keyword('struct'):
581
+ root.children.append(self._parse_struct())
582
+ # NEW: define at top level
583
+ elif self._match_keyword('define'):
584
+ root.children.append(self._parse_define())
585
+ else:
586
+ self._advance()
587
+
588
+ self._match(TokenType.BLOCK_END)
589
+ return root
590
+
591
+ def parse_program(self) -> ASTNode:
592
+ """Parse a standalone program (no service wrapper)"""
593
+ root = ASTNode('program', children=[])
594
+
595
+ while not self._is_at_end():
596
+ if self._match_keyword('struct'):
597
+ root.children.append(self._parse_struct())
598
+ elif self._match_keyword('define'):
599
+ root.children.append(self._parse_define())
600
+ elif self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or self._check(TokenType.SELF_REF):
601
+ stmt = self._parse_expression_statement()
602
+ if stmt:
603
+ root.children.append(stmt)
604
+ elif self._match_keyword('if'):
605
+ root.children.append(self._parse_if())
606
+ elif self._match_keyword('while'):
607
+ root.children.append(self._parse_while())
608
+ elif self._match_keyword('for'):
609
+ root.children.append(self._parse_for())
610
+ elif self._match_keyword('foreach'):
611
+ root.children.append(self._parse_foreach())
612
+ else:
613
+ self._advance()
614
+
615
+ return root
616
+
617
+ def _current(self) -> Token:
618
+ if self.pos < len(self.tokens):
619
+ return self.tokens[self.pos]
620
+ return Token(TokenType.EOF, '', 0, 0)
621
+
622
+ def _peek(self, offset=0) -> Token:
623
+ pos = self.pos + offset
624
+ if pos < len(self.tokens):
625
+ return self.tokens[pos]
626
+ return Token(TokenType.EOF, '', 0, 0)
627
+
628
+ def _advance(self) -> Token:
629
+ token = self._current()
630
+ self.pos += 1
631
+ return token
632
+
633
+ def _is_at_end(self) -> bool:
634
+ return self._current().type == TokenType.EOF
635
+
636
+ def _check(self, token_type: TokenType) -> bool:
637
+ return self._current().type == token_type
638
+
639
+ def _match(self, token_type: TokenType) -> bool:
640
+ if self._check(token_type):
641
+ self._advance()
642
+ return True
643
+ return False
644
+
645
+ def _match_keyword(self, keyword: str) -> bool:
646
+ if self._current().type == TokenType.KEYWORD and self._current().value == keyword:
647
+ self._advance()
648
+ return True
649
+ return False
650
+
651
+ def _expect(self, token_type: TokenType, message: str = None):
652
+ if not self._match(token_type):
653
+ msg = message or f"Expected {token_type.name}, got {self._current().type.name}"
654
+ self.error(msg)
655
+ return self.tokens[self.pos - 1]
656
+
657
+ def _parse_service_init(self) -> ASTNode:
658
+ node = ASTNode('service-init', children=[])
659
+ self._expect(TokenType.BLOCK_START)
660
+
661
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
662
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
663
+ key = self._advance().value
664
+ self._expect(TokenType.COLON)
665
+ value = self._parse_value()
666
+ node.children.append(ASTNode('property', value={'key': key, 'value': value}))
667
+ self._match(TokenType.SEMICOLON)
668
+ else:
669
+ self._advance()
670
+
671
+ self._expect(TokenType.BLOCK_END)
672
+ return node
673
+
674
+ def _parse_service_include(self) -> ASTNode:
675
+ """Parse service-include block for importing modules and files
676
+
677
+ Syntax:
678
+ service-include {
679
+ @KernelClient <== get(include(cso_root('/root32/etc/tasks/kernel.cssl')));
680
+ @Time <== get('time');
681
+ @Secrets <== get('secrets');
682
+ }
683
+ """
684
+ node = ASTNode('service-include', children=[])
685
+ self._expect(TokenType.BLOCK_START)
686
+
687
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
688
+ # Parse module injection statements like @ModuleName <== get(...);
689
+ if self._check(TokenType.AT):
690
+ stmt = self._parse_expression_statement()
691
+ if stmt:
692
+ node.children.append(stmt)
693
+ elif self._check(TokenType.IDENTIFIER):
694
+ # Also support identifier-based assignments: moduleName <== get(...);
695
+ stmt = self._parse_expression_statement()
696
+ if stmt:
697
+ node.children.append(stmt)
698
+ else:
699
+ self._advance()
700
+
701
+ self._expect(TokenType.BLOCK_END)
702
+ return node
703
+
704
+ def _parse_service_run(self) -> ASTNode:
705
+ node = ASTNode('service-run', children=[])
706
+ self._expect(TokenType.BLOCK_START)
707
+
708
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
709
+ if self._match_keyword('struct'):
710
+ node.children.append(self._parse_struct())
711
+ elif self._match_keyword('define'):
712
+ node.children.append(self._parse_define())
713
+ else:
714
+ self._advance()
715
+
716
+ self._expect(TokenType.BLOCK_END)
717
+ return node
718
+
719
+ def _parse_package(self) -> ASTNode:
720
+ """Parse package {} block for service metadata - NEW
721
+
722
+ Syntax:
723
+ package {
724
+ service = "ServiceName";
725
+ exec = @Start();
726
+ version = "1.0.0";
727
+ description = "Beschreibung";
728
+ }
729
+ """
730
+ node = ASTNode('package', children=[])
731
+ self._expect(TokenType.BLOCK_START)
732
+
733
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
734
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
735
+ key = self._advance().value
736
+ self._expect(TokenType.EQUALS)
737
+ value = self._parse_expression()
738
+ node.children.append(ASTNode('package_property', value={'key': key, 'value': value}))
739
+ self._match(TokenType.SEMICOLON)
740
+ else:
741
+ self._advance()
742
+
743
+ self._expect(TokenType.BLOCK_END)
744
+ return node
745
+
746
+ def _parse_package_includes(self) -> ASTNode:
747
+ """Parse package-includes {} block for imports - NEW
748
+
749
+ Syntax:
750
+ package-includes {
751
+ @Lists = get('list');
752
+ @OS = get('os');
753
+ @Time = get('time');
754
+ @VSRam = get('vsramsdk');
755
+ }
756
+ """
757
+ node = ASTNode('package-includes', children=[])
758
+ self._expect(TokenType.BLOCK_START)
759
+
760
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
761
+ # Parse module injection statements like @ModuleName = get(...);
762
+ if self._check(TokenType.AT):
763
+ stmt = self._parse_expression_statement()
764
+ if stmt:
765
+ node.children.append(stmt)
766
+ elif self._check(TokenType.IDENTIFIER):
767
+ # Also support identifier-based assignments
768
+ stmt = self._parse_expression_statement()
769
+ if stmt:
770
+ node.children.append(stmt)
771
+ else:
772
+ self._advance()
773
+
774
+ self._expect(TokenType.BLOCK_END)
775
+ return node
776
+
777
+ def _parse_struct(self) -> ASTNode:
778
+ name = self._advance().value
779
+ is_global = False
780
+
781
+ # Check for (@) decorator: struct Name(@) { ... }
782
+ if self._match(TokenType.PAREN_START):
783
+ if self._check(TokenType.AT):
784
+ self._advance() # skip @
785
+ is_global = True
786
+ self._expect(TokenType.PAREN_END)
787
+
788
+ node = ASTNode('struct', value={'name': name, 'global': is_global}, children=[])
789
+ self._expect(TokenType.BLOCK_START)
790
+
791
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
792
+ if self._match_keyword('define'):
793
+ node.children.append(self._parse_define())
794
+ elif self._check(TokenType.IDENTIFIER):
795
+ # Look ahead to determine what kind of statement this is
796
+ saved_pos = self.pos
797
+ var_name = self._advance().value
798
+
799
+ if self._match(TokenType.INJECT_LEFT):
800
+ # Injection: var <== expr
801
+ value = self._parse_expression()
802
+ node.children.append(ASTNode('injection', value={'name': var_name, 'source': value}))
803
+ self._match(TokenType.SEMICOLON)
804
+ elif self._match(TokenType.EQUALS):
805
+ # Assignment: var = expr
806
+ value = self._parse_expression()
807
+ node.children.append(ASTNode('assignment', value={'name': var_name, 'value': value}))
808
+ self._match(TokenType.SEMICOLON)
809
+ elif self._check(TokenType.PAREN_START):
810
+ # Function call: func(args)
811
+ self.pos = saved_pos # Go back to parse full expression
812
+ stmt = self._parse_expression_statement()
813
+ if stmt:
814
+ node.children.append(stmt)
815
+ elif self._match(TokenType.DOT):
816
+ # Method call: obj.method(args)
817
+ self.pos = saved_pos # Go back to parse full expression
818
+ stmt = self._parse_expression_statement()
819
+ if stmt:
820
+ node.children.append(stmt)
821
+ else:
822
+ self._match(TokenType.SEMICOLON)
823
+ elif self._check(TokenType.AT):
824
+ # Module reference statement
825
+ stmt = self._parse_expression_statement()
826
+ if stmt:
827
+ node.children.append(stmt)
828
+ else:
829
+ self._advance()
830
+
831
+ self._expect(TokenType.BLOCK_END)
832
+ return node
833
+
834
+ def _parse_define(self) -> ASTNode:
835
+ name = self._advance().value
836
+ params = []
837
+
838
+ if self._match(TokenType.PAREN_START):
839
+ while not self._check(TokenType.PAREN_END):
840
+ if self._check(TokenType.IDENTIFIER):
841
+ params.append(self._advance().value)
842
+ self._match(TokenType.COMMA)
843
+ else:
844
+ break
845
+ self._expect(TokenType.PAREN_END)
846
+
847
+ node = ASTNode('function', value={'name': name, 'params': params}, children=[])
848
+ self._expect(TokenType.BLOCK_START)
849
+
850
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
851
+ stmt = self._parse_statement()
852
+ if stmt:
853
+ node.children.append(stmt)
854
+
855
+ self._expect(TokenType.BLOCK_END)
856
+ return node
857
+
858
+ def _parse_statement(self) -> Optional[ASTNode]:
859
+ if self._match_keyword('if'):
860
+ return self._parse_if()
861
+ elif self._match_keyword('while'):
862
+ return self._parse_while()
863
+ elif self._match_keyword('for'):
864
+ return self._parse_for()
865
+ elif self._match_keyword('foreach'):
866
+ return self._parse_foreach()
867
+ elif self._match_keyword('switch'):
868
+ return self._parse_switch()
869
+ elif self._match_keyword('return'):
870
+ return self._parse_return()
871
+ elif self._match_keyword('break'):
872
+ self._match(TokenType.SEMICOLON)
873
+ return ASTNode('break')
874
+ elif self._match_keyword('continue'):
875
+ self._match(TokenType.SEMICOLON)
876
+ return ASTNode('continue')
877
+ elif self._match_keyword('try'):
878
+ return self._parse_try()
879
+ elif self._match_keyword('await'):
880
+ return self._parse_await()
881
+ elif self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT):
882
+ return self._parse_expression_statement()
883
+ else:
884
+ self._advance()
885
+ return None
886
+
887
+ def _parse_if(self) -> ASTNode:
888
+ self._expect(TokenType.PAREN_START)
889
+ condition = self._parse_expression()
890
+ self._expect(TokenType.PAREN_END)
891
+
892
+ node = ASTNode('if', value={'condition': condition}, children=[])
893
+
894
+ self._expect(TokenType.BLOCK_START)
895
+ then_block = ASTNode('then', children=[])
896
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
897
+ stmt = self._parse_statement()
898
+ if stmt:
899
+ then_block.children.append(stmt)
900
+ self._expect(TokenType.BLOCK_END)
901
+ node.children.append(then_block)
902
+
903
+ if self._match_keyword('else'):
904
+ else_block = ASTNode('else', children=[])
905
+ if self._match_keyword('if'):
906
+ else_block.children.append(self._parse_if())
907
+ else:
908
+ self._expect(TokenType.BLOCK_START)
909
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
910
+ stmt = self._parse_statement()
911
+ if stmt:
912
+ else_block.children.append(stmt)
913
+ self._expect(TokenType.BLOCK_END)
914
+ node.children.append(else_block)
915
+
916
+ return node
917
+
918
+ def _parse_while(self) -> ASTNode:
919
+ self._expect(TokenType.PAREN_START)
920
+ condition = self._parse_expression()
921
+ self._expect(TokenType.PAREN_END)
922
+
923
+ node = ASTNode('while', value={'condition': condition}, children=[])
924
+ self._expect(TokenType.BLOCK_START)
925
+
926
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
927
+ stmt = self._parse_statement()
928
+ if stmt:
929
+ node.children.append(stmt)
930
+
931
+ self._expect(TokenType.BLOCK_END)
932
+ return node
933
+
934
+ def _parse_for(self) -> ASTNode:
935
+ self._expect(TokenType.PAREN_START)
936
+ var_name = self._advance().value
937
+ self._expect(TokenType.KEYWORD)
938
+ self._expect(TokenType.KEYWORD)
939
+ self._expect(TokenType.PAREN_START)
940
+ start = self._parse_expression()
941
+ self._expect(TokenType.COMMA)
942
+ end = self._parse_expression()
943
+ self._expect(TokenType.PAREN_END)
944
+ self._expect(TokenType.PAREN_END)
945
+
946
+ node = ASTNode('for', value={'var': var_name, 'start': start, 'end': end}, children=[])
947
+ self._expect(TokenType.BLOCK_START)
948
+
949
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
950
+ stmt = self._parse_statement()
951
+ if stmt:
952
+ node.children.append(stmt)
953
+
954
+ self._expect(TokenType.BLOCK_END)
955
+ return node
956
+
957
+ def _parse_foreach(self) -> ASTNode:
958
+ """Parse foreach loop - supports both syntaxes:
959
+
960
+ Traditional: foreach (var in iterable) { }
961
+ New 'as' syntax: foreach iterable as var { }
962
+ """
963
+ # Check if this is the new 'as' syntax or traditional syntax
964
+ if self._check(TokenType.PAREN_START):
965
+ # Traditional syntax: foreach (var in iterable) { }
966
+ self._expect(TokenType.PAREN_START)
967
+ var_name = self._advance().value
968
+ self._match_keyword('in')
969
+ iterable = self._parse_expression()
970
+ self._expect(TokenType.PAREN_END)
971
+ else:
972
+ # NEW: 'as' syntax: foreach iterable as var { }
973
+ iterable = self._parse_expression()
974
+ if self._check(TokenType.AS):
975
+ self._advance() # consume 'as'
976
+ else:
977
+ self._match_keyword('as') # try keyword match as fallback
978
+ var_name = self._advance().value
979
+
980
+ node = ASTNode('foreach', value={'var': var_name, 'iterable': iterable}, children=[])
981
+ self._expect(TokenType.BLOCK_START)
982
+
983
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
984
+ stmt = self._parse_statement()
985
+ if stmt:
986
+ node.children.append(stmt)
987
+
988
+ self._expect(TokenType.BLOCK_END)
989
+ return node
990
+
991
+ def _parse_switch(self) -> ASTNode:
992
+ self._expect(TokenType.PAREN_START)
993
+ value = self._parse_expression()
994
+ self._expect(TokenType.PAREN_END)
995
+
996
+ node = ASTNode('switch', value={'value': value}, children=[])
997
+ self._expect(TokenType.BLOCK_START)
998
+
999
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1000
+ if self._match_keyword('case'):
1001
+ case_value = self._parse_expression()
1002
+ self._expect(TokenType.COLON)
1003
+ case_node = ASTNode('case', value={'value': case_value}, children=[])
1004
+
1005
+ while not self._check_keyword('case') and not self._check_keyword('default') and not self._check(TokenType.BLOCK_END):
1006
+ stmt = self._parse_statement()
1007
+ if stmt:
1008
+ case_node.children.append(stmt)
1009
+ if self._check_keyword('break'):
1010
+ break
1011
+
1012
+ node.children.append(case_node)
1013
+ elif self._match_keyword('default'):
1014
+ self._expect(TokenType.COLON)
1015
+ default_node = ASTNode('default', children=[])
1016
+
1017
+ while not self._check(TokenType.BLOCK_END):
1018
+ stmt = self._parse_statement()
1019
+ if stmt:
1020
+ default_node.children.append(stmt)
1021
+
1022
+ node.children.append(default_node)
1023
+ else:
1024
+ self._advance()
1025
+
1026
+ self._expect(TokenType.BLOCK_END)
1027
+ return node
1028
+
1029
+ def _parse_return(self) -> ASTNode:
1030
+ value = None
1031
+ if not self._check(TokenType.SEMICOLON) and not self._check(TokenType.BLOCK_END):
1032
+ value = self._parse_expression()
1033
+ self._match(TokenType.SEMICOLON)
1034
+ return ASTNode('return', value=value)
1035
+
1036
+ def _parse_try(self) -> ASTNode:
1037
+ node = ASTNode('try', children=[])
1038
+
1039
+ try_block = ASTNode('try-block', children=[])
1040
+ self._expect(TokenType.BLOCK_START)
1041
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1042
+ stmt = self._parse_statement()
1043
+ if stmt:
1044
+ try_block.children.append(stmt)
1045
+ self._expect(TokenType.BLOCK_END)
1046
+ node.children.append(try_block)
1047
+
1048
+ if self._match_keyword('catch'):
1049
+ error_var = None
1050
+ if self._match(TokenType.PAREN_START):
1051
+ error_var = self._advance().value
1052
+ self._expect(TokenType.PAREN_END)
1053
+
1054
+ catch_block = ASTNode('catch-block', value={'error_var': error_var}, children=[])
1055
+ self._expect(TokenType.BLOCK_START)
1056
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1057
+ stmt = self._parse_statement()
1058
+ if stmt:
1059
+ catch_block.children.append(stmt)
1060
+ self._expect(TokenType.BLOCK_END)
1061
+ node.children.append(catch_block)
1062
+
1063
+ return node
1064
+
1065
+ def _parse_await(self) -> ASTNode:
1066
+ """Parse await statement: await expression;"""
1067
+ expr = self._parse_expression()
1068
+ self._match(TokenType.SEMICOLON)
1069
+ return ASTNode('await', value=expr)
1070
+
1071
+ def _parse_action_block(self) -> ASTNode:
1072
+ """Parse an action block { ... } containing statements for createcmd"""
1073
+ node = ASTNode('action_block', children=[])
1074
+ self._expect(TokenType.BLOCK_START)
1075
+
1076
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1077
+ # Check for define statements inside action block
1078
+ if self._match_keyword('define'):
1079
+ node.children.append(self._parse_define())
1080
+ else:
1081
+ stmt = self._parse_statement()
1082
+ if stmt:
1083
+ node.children.append(stmt)
1084
+
1085
+ self._expect(TokenType.BLOCK_END)
1086
+ return node
1087
+
1088
+ def _parse_injection_filter(self) -> Optional[dict]:
1089
+ """Parse injection filter: [type::helper=value]"""
1090
+ if not self._match(TokenType.BRACKET_START):
1091
+ return None
1092
+
1093
+ filter_info = {}
1094
+ # Parse type::helper=value patterns
1095
+ while not self._check(TokenType.BRACKET_END) and not self._is_at_end():
1096
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
1097
+ filter_type = self._advance().value
1098
+ if self._match(TokenType.DOUBLE_COLON):
1099
+ helper = self._advance().value
1100
+ if self._match(TokenType.EQUALS):
1101
+ value = self._parse_expression()
1102
+ filter_info[f'{filter_type}::{helper}'] = value
1103
+ else:
1104
+ filter_info[f'{filter_type}::{helper}'] = True
1105
+ else:
1106
+ filter_info['type'] = filter_type
1107
+ elif self._check(TokenType.COMMA):
1108
+ self._advance()
1109
+ else:
1110
+ break
1111
+
1112
+ self._expect(TokenType.BRACKET_END)
1113
+ return filter_info if filter_info else None
1114
+
1115
+ def _parse_expression_statement(self) -> Optional[ASTNode]:
1116
+ expr = self._parse_expression()
1117
+
1118
+ # === BASIC INJECTION: <== (replace target with source) ===
1119
+ if self._match(TokenType.INJECT_LEFT):
1120
+ # Check if this is a createcmd injection with a code block
1121
+ is_createcmd = (
1122
+ expr.type == 'call' and
1123
+ expr.value.get('callee') and
1124
+ expr.value.get('callee').type == 'identifier' and
1125
+ expr.value.get('callee').value == 'createcmd'
1126
+ )
1127
+
1128
+ if is_createcmd and self._check(TokenType.BLOCK_START):
1129
+ action_block = self._parse_action_block()
1130
+ self._match(TokenType.SEMICOLON)
1131
+ return ASTNode('createcmd_inject', value={'command_call': expr, 'action': action_block})
1132
+ else:
1133
+ # Check for injection filter [type::helper=value]
1134
+ filter_info = self._parse_injection_filter()
1135
+ source = self._parse_expression()
1136
+ self._match(TokenType.SEMICOLON)
1137
+ return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'replace', 'filter': filter_info})
1138
+
1139
+ # === PLUS INJECTION: +<== (copy & add to target) ===
1140
+ if self._match(TokenType.INJECT_PLUS_LEFT):
1141
+ filter_info = self._parse_injection_filter()
1142
+ source = self._parse_expression()
1143
+ self._match(TokenType.SEMICOLON)
1144
+ return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'add', 'filter': filter_info})
1145
+
1146
+ # === MINUS INJECTION: -<== (move & remove from source) ===
1147
+ if self._match(TokenType.INJECT_MINUS_LEFT):
1148
+ filter_info = self._parse_injection_filter()
1149
+ source = self._parse_expression()
1150
+ self._match(TokenType.SEMICOLON)
1151
+ return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'move', 'filter': filter_info})
1152
+
1153
+ # === CODE INFUSION: <<== (inject code into function) ===
1154
+ if self._match(TokenType.INFUSE_LEFT):
1155
+ if self._check(TokenType.BLOCK_START):
1156
+ code_block = self._parse_action_block()
1157
+ self._match(TokenType.SEMICOLON)
1158
+ return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'replace'})
1159
+ else:
1160
+ source = self._parse_expression()
1161
+ self._match(TokenType.SEMICOLON)
1162
+ return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'replace'})
1163
+
1164
+ # === CODE INFUSION PLUS: +<<== (add code to function) ===
1165
+ if self._match(TokenType.INFUSE_PLUS_LEFT):
1166
+ if self._check(TokenType.BLOCK_START):
1167
+ code_block = self._parse_action_block()
1168
+ self._match(TokenType.SEMICOLON)
1169
+ return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'add'})
1170
+ else:
1171
+ source = self._parse_expression()
1172
+ self._match(TokenType.SEMICOLON)
1173
+ return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'add'})
1174
+
1175
+ # === CODE INFUSION MINUS: -<<== (remove code from function) ===
1176
+ if self._match(TokenType.INFUSE_MINUS_LEFT):
1177
+ if self._check(TokenType.BLOCK_START):
1178
+ code_block = self._parse_action_block()
1179
+ self._match(TokenType.SEMICOLON)
1180
+ return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'remove'})
1181
+ else:
1182
+ source = self._parse_expression()
1183
+ self._match(TokenType.SEMICOLON)
1184
+ return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'remove'})
1185
+
1186
+ # === RIGHT-SIDE OPERATORS ===
1187
+
1188
+ # === BASIC RECEIVE: ==> (move source to target) ===
1189
+ if self._match(TokenType.INJECT_RIGHT):
1190
+ filter_info = self._parse_injection_filter()
1191
+ target = self._parse_expression()
1192
+ self._match(TokenType.SEMICOLON)
1193
+ return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'replace', 'filter': filter_info})
1194
+
1195
+ # === PLUS RECEIVE: ==>+ (copy source to target) ===
1196
+ if self._match(TokenType.INJECT_PLUS_RIGHT):
1197
+ filter_info = self._parse_injection_filter()
1198
+ target = self._parse_expression()
1199
+ self._match(TokenType.SEMICOLON)
1200
+ return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'add', 'filter': filter_info})
1201
+
1202
+ # === MINUS RECEIVE: -==> (move & remove from source) ===
1203
+ if self._match(TokenType.INJECT_MINUS_RIGHT):
1204
+ filter_info = self._parse_injection_filter()
1205
+ target = self._parse_expression()
1206
+ self._match(TokenType.SEMICOLON)
1207
+ return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'move', 'filter': filter_info})
1208
+
1209
+ # === CODE INFUSION RIGHT: ==>> ===
1210
+ if self._match(TokenType.INFUSE_RIGHT):
1211
+ target = self._parse_expression()
1212
+ self._match(TokenType.SEMICOLON)
1213
+ return ASTNode('infuse_right', value={'source': expr, 'target': target, 'mode': 'replace'})
1214
+
1215
+ # === FLOW OPERATORS ===
1216
+ if self._match(TokenType.FLOW_RIGHT):
1217
+ target = self._parse_expression()
1218
+ self._match(TokenType.SEMICOLON)
1219
+ return ASTNode('flow', value={'source': expr, 'target': target})
1220
+
1221
+ if self._match(TokenType.FLOW_LEFT):
1222
+ source = self._parse_expression()
1223
+ self._match(TokenType.SEMICOLON)
1224
+ return ASTNode('flow', value={'source': source, 'target': expr})
1225
+
1226
+ # === BASIC ASSIGNMENT ===
1227
+ if self._match(TokenType.EQUALS):
1228
+ value = self._parse_expression()
1229
+ self._match(TokenType.SEMICOLON)
1230
+ return ASTNode('assignment', value={'target': expr, 'value': value})
1231
+
1232
+ self._match(TokenType.SEMICOLON)
1233
+ return ASTNode('expression', value=expr)
1234
+
1235
+ def _parse_expression(self) -> ASTNode:
1236
+ return self._parse_or()
1237
+
1238
+ def _parse_or(self) -> ASTNode:
1239
+ left = self._parse_and()
1240
+
1241
+ while self._match(TokenType.OR) or self._match_keyword('or'):
1242
+ right = self._parse_and()
1243
+ left = ASTNode('binary', value={'op': 'or', 'left': left, 'right': right})
1244
+
1245
+ return left
1246
+
1247
+ def _parse_and(self) -> ASTNode:
1248
+ left = self._parse_comparison()
1249
+
1250
+ while self._match(TokenType.AND) or self._match_keyword('and'):
1251
+ right = self._parse_comparison()
1252
+ left = ASTNode('binary', value={'op': 'and', 'left': left, 'right': right})
1253
+
1254
+ return left
1255
+
1256
+ def _parse_comparison(self) -> ASTNode:
1257
+ left = self._parse_term()
1258
+
1259
+ while True:
1260
+ if self._match(TokenType.COMPARE_EQ):
1261
+ right = self._parse_term()
1262
+ left = ASTNode('binary', value={'op': '==', 'left': left, 'right': right})
1263
+ elif self._match(TokenType.COMPARE_NE):
1264
+ right = self._parse_term()
1265
+ left = ASTNode('binary', value={'op': '!=', 'left': left, 'right': right})
1266
+ elif self._match(TokenType.COMPARE_LT):
1267
+ right = self._parse_term()
1268
+ left = ASTNode('binary', value={'op': '<', 'left': left, 'right': right})
1269
+ elif self._match(TokenType.COMPARE_GT):
1270
+ right = self._parse_term()
1271
+ left = ASTNode('binary', value={'op': '>', 'left': left, 'right': right})
1272
+ elif self._match(TokenType.COMPARE_LE):
1273
+ right = self._parse_term()
1274
+ left = ASTNode('binary', value={'op': '<=', 'left': left, 'right': right})
1275
+ elif self._match(TokenType.COMPARE_GE):
1276
+ right = self._parse_term()
1277
+ left = ASTNode('binary', value={'op': '>=', 'left': left, 'right': right})
1278
+ else:
1279
+ break
1280
+
1281
+ return left
1282
+
1283
+ def _parse_term(self) -> ASTNode:
1284
+ left = self._parse_factor()
1285
+
1286
+ while True:
1287
+ if self._match(TokenType.PLUS):
1288
+ right = self._parse_factor()
1289
+ left = ASTNode('binary', value={'op': '+', 'left': left, 'right': right})
1290
+ elif self._match(TokenType.MINUS):
1291
+ right = self._parse_factor()
1292
+ left = ASTNode('binary', value={'op': '-', 'left': left, 'right': right})
1293
+ else:
1294
+ break
1295
+
1296
+ return left
1297
+
1298
+ def _parse_factor(self) -> ASTNode:
1299
+ left = self._parse_unary()
1300
+
1301
+ while True:
1302
+ if self._match(TokenType.MULTIPLY):
1303
+ right = self._parse_unary()
1304
+ left = ASTNode('binary', value={'op': '*', 'left': left, 'right': right})
1305
+ elif self._match(TokenType.DIVIDE):
1306
+ right = self._parse_unary()
1307
+ left = ASTNode('binary', value={'op': '/', 'left': left, 'right': right})
1308
+ elif self._match(TokenType.MODULO):
1309
+ right = self._parse_unary()
1310
+ left = ASTNode('binary', value={'op': '%', 'left': left, 'right': right})
1311
+ else:
1312
+ break
1313
+
1314
+ return left
1315
+
1316
+ def _parse_unary(self) -> ASTNode:
1317
+ if self._match(TokenType.NOT) or self._match_keyword('not'):
1318
+ operand = self._parse_unary()
1319
+ return ASTNode('unary', value={'op': 'not', 'operand': operand})
1320
+ if self._match(TokenType.MINUS):
1321
+ operand = self._parse_unary()
1322
+ return ASTNode('unary', value={'op': '-', 'operand': operand})
1323
+
1324
+ return self._parse_primary()
1325
+
1326
+ def _parse_primary(self) -> ASTNode:
1327
+ if self._match(TokenType.AT):
1328
+ return self._parse_module_reference()
1329
+
1330
+ if self._check(TokenType.SELF_REF):
1331
+ # s@<name> self-reference to global struct
1332
+ token = self._advance()
1333
+ node = ASTNode('self_ref', value=token.value, line=token.line, column=token.column)
1334
+ # Check for function call: s@Backend.Loop.Start()
1335
+ if self._match(TokenType.PAREN_START):
1336
+ args = []
1337
+ while not self._check(TokenType.PAREN_END):
1338
+ args.append(self._parse_expression())
1339
+ if not self._check(TokenType.PAREN_END):
1340
+ self._expect(TokenType.COMMA)
1341
+ self._expect(TokenType.PAREN_END)
1342
+ node = ASTNode('call', value={'callee': node, 'args': args})
1343
+ return node
1344
+
1345
+ if self._check(TokenType.NUMBER):
1346
+ return ASTNode('literal', value=self._advance().value)
1347
+
1348
+ if self._check(TokenType.STRING):
1349
+ return ASTNode('literal', value=self._advance().value)
1350
+
1351
+ if self._check(TokenType.BOOLEAN):
1352
+ return ASTNode('literal', value=self._advance().value)
1353
+
1354
+ if self._check(TokenType.NULL):
1355
+ self._advance()
1356
+ return ASTNode('literal', value=None)
1357
+
1358
+ # NEW: Type literals (list, dict) - create empty instances
1359
+ if self._check(TokenType.TYPE_LITERAL):
1360
+ type_name = self._advance().value
1361
+ return ASTNode('type_literal', value=type_name)
1362
+
1363
+ if self._match(TokenType.PAREN_START):
1364
+ expr = self._parse_expression()
1365
+ self._expect(TokenType.PAREN_END)
1366
+ return expr
1367
+
1368
+ if self._match(TokenType.BLOCK_START):
1369
+ return self._parse_object()
1370
+
1371
+ if self._match(TokenType.BRACKET_START):
1372
+ return self._parse_array()
1373
+
1374
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
1375
+ return self._parse_identifier_or_call()
1376
+
1377
+ return ASTNode('literal', value=None)
1378
+
1379
+ def _parse_module_reference(self) -> ASTNode:
1380
+ parts = []
1381
+ parts.append(self._advance().value)
1382
+
1383
+ while self._match(TokenType.DOT):
1384
+ parts.append(self._advance().value)
1385
+
1386
+ return ASTNode('module_ref', value='.'.join(parts))
1387
+
1388
+ def _parse_identifier_or_call(self) -> ASTNode:
1389
+ name = self._advance().value
1390
+ node = ASTNode('identifier', value=name)
1391
+
1392
+ while True:
1393
+ if self._match(TokenType.DOT):
1394
+ member = self._advance().value
1395
+ node = ASTNode('member_access', value={'object': node, 'member': member})
1396
+ elif self._match(TokenType.PAREN_START):
1397
+ args = []
1398
+ while not self._check(TokenType.PAREN_END):
1399
+ args.append(self._parse_expression())
1400
+ if not self._check(TokenType.PAREN_END):
1401
+ self._expect(TokenType.COMMA)
1402
+ self._expect(TokenType.PAREN_END)
1403
+ node = ASTNode('call', value={'callee': node, 'args': args})
1404
+ elif self._match(TokenType.BRACKET_START):
1405
+ index = self._parse_expression()
1406
+ self._expect(TokenType.BRACKET_END)
1407
+ node = ASTNode('index_access', value={'object': node, 'index': index})
1408
+ else:
1409
+ break
1410
+
1411
+ return node
1412
+
1413
+ def _parse_object(self) -> ASTNode:
1414
+ properties = {}
1415
+
1416
+ while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
1417
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.STRING):
1418
+ key = self._advance().value
1419
+ self._expect(TokenType.EQUALS)
1420
+ value = self._parse_expression()
1421
+ properties[key] = value
1422
+ self._match(TokenType.SEMICOLON)
1423
+ self._match(TokenType.COMMA)
1424
+ else:
1425
+ self._advance()
1426
+
1427
+ self._expect(TokenType.BLOCK_END)
1428
+ return ASTNode('object', value=properties)
1429
+
1430
+ def _parse_array(self) -> ASTNode:
1431
+ elements = []
1432
+
1433
+ while not self._check(TokenType.BRACKET_END) and not self._is_at_end():
1434
+ elements.append(self._parse_expression())
1435
+ if not self._check(TokenType.BRACKET_END):
1436
+ self._expect(TokenType.COMMA)
1437
+
1438
+ self._expect(TokenType.BRACKET_END)
1439
+ return ASTNode('array', value=elements)
1440
+
1441
+ def _parse_value(self) -> Any:
1442
+ if self._check(TokenType.STRING):
1443
+ return self._advance().value
1444
+ if self._check(TokenType.NUMBER):
1445
+ return self._advance().value
1446
+ if self._check(TokenType.BOOLEAN):
1447
+ return self._advance().value
1448
+ if self._check(TokenType.NULL):
1449
+ self._advance()
1450
+ return None
1451
+ if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
1452
+ return self._advance().value
1453
+ return None
1454
+
1455
+ def _check_keyword(self, keyword: str) -> bool:
1456
+ return self._current().type == TokenType.KEYWORD and self._current().value == keyword
1457
+
1458
+
1459
+ def parse_cssl(source: str) -> ASTNode:
1460
+ """Parse CSSL source code into an AST - auto-detects service vs program format"""
1461
+ lexer = CSSLLexer(source)
1462
+ tokens = lexer.tokenize()
1463
+ parser = CSSLParser(tokens, lexer.source_lines)
1464
+
1465
+ # Auto-detect: if first token is '{', it's a service file
1466
+ # Otherwise treat as standalone program (whitespace is already filtered by lexer)
1467
+ if tokens and tokens[0].type == TokenType.BLOCK_START:
1468
+ return parser.parse() # Service file format
1469
+ else:
1470
+ return parser.parse_program() # Standalone program format
1471
+
1472
+
1473
+ def parse_cssl_program(source: str) -> ASTNode:
1474
+ """Parse standalone CSSL program (no service wrapper) into an AST"""
1475
+ lexer = CSSLLexer(source)
1476
+ tokens = lexer.tokenize()
1477
+ parser = CSSLParser(tokens, lexer.source_lines)
1478
+ return parser.parse_program()
1479
+
1480
+
1481
+ def tokenize_cssl(source: str) -> List[Token]:
1482
+ """Tokenize CSSL source code (useful for syntax highlighting)"""
1483
+ lexer = CSSLLexer(source)
1484
+ return lexer.tokenize()
1485
+
1486
+
1487
+ # Export public API
1488
+ __all__ = [
1489
+ 'TokenType', 'Token', 'ASTNode',
1490
+ 'CSSLLexer', 'CSSLParser', 'CSSLSyntaxError',
1491
+ 'parse_cssl', 'parse_cssl_program', 'tokenize_cssl',
1492
+ 'KEYWORDS', 'TYPE_LITERALS'
1493
+ ]