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.
- includecpp/__init__.py +4 -3
- includecpp/cli/commands.py +400 -21
- includecpp/core/cppy_converter.py +143 -18
- includecpp/core/cssl/__init__.py +40 -0
- includecpp/core/cssl/cssl_builtins.py +1693 -0
- includecpp/core/cssl/cssl_events.py +621 -0
- includecpp/core/cssl/cssl_modules.py +2803 -0
- includecpp/core/cssl/cssl_parser.py +1493 -0
- includecpp/core/cssl/cssl_runtime.py +1549 -0
- includecpp/core/cssl/cssl_syntax.py +488 -0
- includecpp/core/cssl/cssl_types.py +390 -0
- includecpp/core/cssl_bridge.py +132 -0
- includecpp/core/project_ui.py +684 -34
- includecpp/generator/parser.cpp +81 -0
- {includecpp-3.3.20.dist-info → includecpp-3.4.2.dist-info}/METADATA +48 -4
- includecpp-3.4.2.dist-info/RECORD +40 -0
- includecpp-3.3.20.dist-info/RECORD +0 -31
- {includecpp-3.3.20.dist-info → includecpp-3.4.2.dist-info}/WHEEL +0 -0
- {includecpp-3.3.20.dist-info → includecpp-3.4.2.dist-info}/entry_points.txt +0 -0
- {includecpp-3.3.20.dist-info → includecpp-3.4.2.dist-info}/licenses/LICENSE +0 -0
- {includecpp-3.3.20.dist-info → includecpp-3.4.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
]
|