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