IncludeCPP 3.7.3__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.
Potentially problematic release.
This version of IncludeCPP might be problematic. Click here for more details.
- includecpp/__init__.py +59 -0
- includecpp/__init__.pyi +255 -0
- includecpp/__main__.py +4 -0
- includecpp/cli/__init__.py +4 -0
- includecpp/cli/commands.py +8270 -0
- includecpp/cli/config_parser.py +127 -0
- includecpp/core/__init__.py +19 -0
- includecpp/core/ai_integration.py +2132 -0
- includecpp/core/build_manager.py +2416 -0
- includecpp/core/cpp_api.py +376 -0
- includecpp/core/cpp_api.pyi +95 -0
- includecpp/core/cppy_converter.py +3448 -0
- includecpp/core/cssl/CSSL_DOCUMENTATION.md +2075 -0
- includecpp/core/cssl/__init__.py +42 -0
- includecpp/core/cssl/cssl_builtins.py +2271 -0
- includecpp/core/cssl/cssl_builtins.pyi +1393 -0
- includecpp/core/cssl/cssl_events.py +621 -0
- includecpp/core/cssl/cssl_modules.py +2803 -0
- includecpp/core/cssl/cssl_parser.py +2575 -0
- includecpp/core/cssl/cssl_runtime.py +3051 -0
- includecpp/core/cssl/cssl_syntax.py +488 -0
- includecpp/core/cssl/cssl_types.py +1512 -0
- includecpp/core/cssl_bridge.py +882 -0
- includecpp/core/cssl_bridge.pyi +488 -0
- includecpp/core/error_catalog.py +802 -0
- includecpp/core/error_formatter.py +1016 -0
- includecpp/core/exceptions.py +97 -0
- includecpp/core/path_discovery.py +77 -0
- includecpp/core/project_ui.py +3370 -0
- includecpp/core/settings_ui.py +326 -0
- includecpp/generator/__init__.py +1 -0
- includecpp/generator/parser.cpp +1903 -0
- includecpp/generator/parser.h +281 -0
- includecpp/generator/type_resolver.cpp +363 -0
- includecpp/generator/type_resolver.h +68 -0
- includecpp/py.typed +0 -0
- includecpp/templates/cpp.proj.template +18 -0
- includecpp/vscode/__init__.py +1 -0
- includecpp/vscode/cssl/__init__.py +1 -0
- includecpp/vscode/cssl/language-configuration.json +38 -0
- includecpp/vscode/cssl/package.json +50 -0
- includecpp/vscode/cssl/snippets/cssl.snippets.json +1080 -0
- includecpp/vscode/cssl/syntaxes/cssl.tmLanguage.json +341 -0
- includecpp-3.7.3.dist-info/METADATA +1076 -0
- includecpp-3.7.3.dist-info/RECORD +49 -0
- includecpp-3.7.3.dist-info/WHEEL +5 -0
- includecpp-3.7.3.dist-info/entry_points.txt +2 -0
- includecpp-3.7.3.dist-info/licenses/LICENSE +21 -0
- includecpp-3.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2575 @@
|
|
|
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
|
+
SHARED_REF = auto() # $<name> shared object reference
|
|
103
|
+
CAPTURED_REF = auto() # %<name> captured reference (for infusion)
|
|
104
|
+
THIS_REF = auto() # this-><name> class member reference
|
|
105
|
+
PACKAGE = auto()
|
|
106
|
+
PACKAGE_INCLUDES = auto()
|
|
107
|
+
AS = auto()
|
|
108
|
+
COMMENT = auto()
|
|
109
|
+
NEWLINE = auto()
|
|
110
|
+
EOF = auto()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
KEYWORDS = {
|
|
114
|
+
# Service structure
|
|
115
|
+
'service-init', 'service-run', 'service-include', 'struct', 'define', 'main', 'class', 'new', 'this',
|
|
116
|
+
# Control flow
|
|
117
|
+
'if', 'else', 'elif', 'while', 'for', 'foreach', 'in', 'range',
|
|
118
|
+
'switch', 'case', 'default', 'break', 'continue', 'return',
|
|
119
|
+
'try', 'catch', 'finally', 'throw',
|
|
120
|
+
# Literals
|
|
121
|
+
'True', 'False', 'null', 'None', 'true', 'false',
|
|
122
|
+
# Logical operators
|
|
123
|
+
'and', 'or', 'not',
|
|
124
|
+
# Async/Events
|
|
125
|
+
'start', 'stop', 'wait_for', 'on_event', 'emit_event', 'await',
|
|
126
|
+
# Package system
|
|
127
|
+
'package', 'package-includes', 'exec', 'as', 'global',
|
|
128
|
+
# CSSL Type Keywords
|
|
129
|
+
'int', 'string', 'float', 'bool', 'void', 'json', 'array', 'vector', 'stack',
|
|
130
|
+
'list', 'dictionary', 'dict', 'instance', 'map', # Python-like types
|
|
131
|
+
'dynamic', # No type declaration (slow but flexible)
|
|
132
|
+
'undefined', # Function errors ignored
|
|
133
|
+
'open', # Accept any parameter type
|
|
134
|
+
'datastruct', # Universal container (lazy declarator)
|
|
135
|
+
'dataspace', # SQL/data storage container
|
|
136
|
+
'shuffled', # Unorganized fast storage (multiple returns)
|
|
137
|
+
'iterator', # Advanced iterator with tasks
|
|
138
|
+
'combo', # Filter/search spaces
|
|
139
|
+
'structure', # Advanced C++/Py Class
|
|
140
|
+
'openquote', # SQL openquote container
|
|
141
|
+
# CSSL Function Modifiers
|
|
142
|
+
'meta', # Source function (must return)
|
|
143
|
+
'super', # Force execution (no exceptions)
|
|
144
|
+
'closed', # Protect from external injection
|
|
145
|
+
'private', # Disable all injections
|
|
146
|
+
'virtual', # Import cycle safe
|
|
147
|
+
'sqlbased', # SQL-based function
|
|
148
|
+
# CSSL Include Keywords
|
|
149
|
+
'include', 'get',
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Type literals that create empty instances
|
|
153
|
+
TYPE_LITERALS = {'list', 'dict'}
|
|
154
|
+
|
|
155
|
+
# Generic type keywords that use <T> syntax
|
|
156
|
+
TYPE_GENERICS = {
|
|
157
|
+
'datastruct', 'dataspace', 'shuffled', 'iterator', 'combo',
|
|
158
|
+
'vector', 'stack', 'array', 'openquote', 'list', 'dictionary', 'map'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Functions that accept type parameters: FuncName<type>(args)
|
|
162
|
+
TYPE_PARAM_FUNCTIONS = {
|
|
163
|
+
'OpenFind' # OpenFind<string>(0)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Injection helper prefixes (type::helper=value)
|
|
167
|
+
INJECTION_HELPERS = {
|
|
168
|
+
'string', 'integer', 'json', 'array', 'vector', 'combo', 'dynamic', 'sql'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class Token:
|
|
174
|
+
type: TokenType
|
|
175
|
+
value: Any
|
|
176
|
+
line: int
|
|
177
|
+
column: int
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class CSSLLexer:
|
|
181
|
+
"""Tokenizes CSSL source code into a stream of tokens."""
|
|
182
|
+
|
|
183
|
+
def __init__(self, source: str):
|
|
184
|
+
self.source = source
|
|
185
|
+
self.pos = 0
|
|
186
|
+
self.line = 1
|
|
187
|
+
self.column = 1
|
|
188
|
+
self.tokens: List[Token] = []
|
|
189
|
+
# Store source lines for error messages
|
|
190
|
+
self.source_lines = source.split('\n')
|
|
191
|
+
|
|
192
|
+
def get_source_line(self, line_num: int) -> str:
|
|
193
|
+
"""Get a specific source line for error reporting"""
|
|
194
|
+
if 0 < line_num <= len(self.source_lines):
|
|
195
|
+
return self.source_lines[line_num - 1]
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
def error(self, message: str):
|
|
199
|
+
"""Raise a syntax error with location info"""
|
|
200
|
+
raise CSSLSyntaxError(
|
|
201
|
+
message,
|
|
202
|
+
line=self.line,
|
|
203
|
+
column=self.column,
|
|
204
|
+
source_line=self.get_source_line(self.line)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def tokenize(self) -> List[Token]:
|
|
208
|
+
while self.pos < len(self.source):
|
|
209
|
+
self._skip_whitespace()
|
|
210
|
+
if self.pos >= len(self.source):
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
char = self.source[self.pos]
|
|
214
|
+
|
|
215
|
+
# Comments: both # and // style
|
|
216
|
+
if char == '#':
|
|
217
|
+
self._skip_comment()
|
|
218
|
+
elif char == '/' and self._peek(1) == '/':
|
|
219
|
+
# C-style // comment - NEW
|
|
220
|
+
self._skip_comment()
|
|
221
|
+
elif char == '\n':
|
|
222
|
+
self._add_token(TokenType.NEWLINE, '\n')
|
|
223
|
+
self._advance()
|
|
224
|
+
self.line += 1
|
|
225
|
+
self.column = 1
|
|
226
|
+
elif char in '"\'':
|
|
227
|
+
self._read_string(char)
|
|
228
|
+
elif char == '`':
|
|
229
|
+
# Raw string (no escape processing) - useful for JSON
|
|
230
|
+
self._read_raw_string()
|
|
231
|
+
elif char.isdigit() or (char == '-' and self._peek(1).isdigit()):
|
|
232
|
+
self._read_number()
|
|
233
|
+
elif char == 'r' and self._peek(1) == '@':
|
|
234
|
+
# r@<name> global variable declaration (same as 'global')
|
|
235
|
+
self._read_global_ref()
|
|
236
|
+
elif char == 's' and self._peek(1) == '@':
|
|
237
|
+
# s@<name> self-reference to global struct
|
|
238
|
+
self._read_self_ref()
|
|
239
|
+
elif char.isalpha() or char == '_':
|
|
240
|
+
self._read_identifier()
|
|
241
|
+
elif char == '@':
|
|
242
|
+
self._add_token(TokenType.AT, '@')
|
|
243
|
+
self._advance()
|
|
244
|
+
elif char == '$':
|
|
245
|
+
# $<name> shared object reference
|
|
246
|
+
self._read_shared_ref()
|
|
247
|
+
elif char == '%':
|
|
248
|
+
# %<name> captured reference (for infusion)
|
|
249
|
+
self._read_captured_ref()
|
|
250
|
+
elif char == '&':
|
|
251
|
+
# & for references
|
|
252
|
+
if self._peek(1) == '&':
|
|
253
|
+
self._add_token(TokenType.AND, '&&')
|
|
254
|
+
self._advance()
|
|
255
|
+
self._advance()
|
|
256
|
+
else:
|
|
257
|
+
self._add_token(TokenType.AMPERSAND, '&')
|
|
258
|
+
self._advance()
|
|
259
|
+
elif char == '{':
|
|
260
|
+
self._add_token(TokenType.BLOCK_START, '{')
|
|
261
|
+
self._advance()
|
|
262
|
+
elif char == '}':
|
|
263
|
+
self._add_token(TokenType.BLOCK_END, '}')
|
|
264
|
+
self._advance()
|
|
265
|
+
elif char == '(':
|
|
266
|
+
self._add_token(TokenType.PAREN_START, '(')
|
|
267
|
+
self._advance()
|
|
268
|
+
elif char == ')':
|
|
269
|
+
self._add_token(TokenType.PAREN_END, ')')
|
|
270
|
+
self._advance()
|
|
271
|
+
elif char == '[':
|
|
272
|
+
self._add_token(TokenType.BRACKET_START, '[')
|
|
273
|
+
self._advance()
|
|
274
|
+
elif char == ']':
|
|
275
|
+
self._add_token(TokenType.BRACKET_END, ']')
|
|
276
|
+
self._advance()
|
|
277
|
+
elif char == ';':
|
|
278
|
+
self._add_token(TokenType.SEMICOLON, ';')
|
|
279
|
+
self._advance()
|
|
280
|
+
elif char == ':':
|
|
281
|
+
# Check for :: (double colon for injection helpers)
|
|
282
|
+
if self._peek(1) == ':':
|
|
283
|
+
self._add_token(TokenType.DOUBLE_COLON, '::')
|
|
284
|
+
self._advance()
|
|
285
|
+
self._advance()
|
|
286
|
+
else:
|
|
287
|
+
self._add_token(TokenType.COLON, ':')
|
|
288
|
+
self._advance()
|
|
289
|
+
elif char == ',':
|
|
290
|
+
self._add_token(TokenType.COMMA, ',')
|
|
291
|
+
self._advance()
|
|
292
|
+
elif char == '.':
|
|
293
|
+
self._add_token(TokenType.DOT, '.')
|
|
294
|
+
self._advance()
|
|
295
|
+
elif char == '+':
|
|
296
|
+
# Check for BruteForce Injection: +<== or +<<==
|
|
297
|
+
if self._peek(1) == '<' and self._peek(2) == '<' and self._peek(3) == '=' and self._peek(4) == '=':
|
|
298
|
+
self._add_token(TokenType.INFUSE_PLUS_LEFT, '+<<==')
|
|
299
|
+
for _ in range(5): self._advance()
|
|
300
|
+
elif self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
|
|
301
|
+
self._add_token(TokenType.INJECT_PLUS_LEFT, '+<==')
|
|
302
|
+
for _ in range(4): self._advance()
|
|
303
|
+
else:
|
|
304
|
+
self._add_token(TokenType.PLUS, '+')
|
|
305
|
+
self._advance()
|
|
306
|
+
elif char == '*':
|
|
307
|
+
self._add_token(TokenType.MULTIPLY, '*')
|
|
308
|
+
self._advance()
|
|
309
|
+
elif char == '/':
|
|
310
|
+
# Check if this is a // comment (handled above) or division
|
|
311
|
+
if self._peek(1) != '/':
|
|
312
|
+
self._add_token(TokenType.DIVIDE, '/')
|
|
313
|
+
self._advance()
|
|
314
|
+
else:
|
|
315
|
+
# Already handled by // comment check above, but just in case
|
|
316
|
+
self._skip_comment()
|
|
317
|
+
elif char == '%':
|
|
318
|
+
self._add_token(TokenType.MODULO, '%')
|
|
319
|
+
self._advance()
|
|
320
|
+
elif char == '<':
|
|
321
|
+
self._read_less_than()
|
|
322
|
+
elif char == '>':
|
|
323
|
+
self._read_greater_than()
|
|
324
|
+
elif char == '=':
|
|
325
|
+
self._read_equals()
|
|
326
|
+
elif char == '!':
|
|
327
|
+
self._read_not()
|
|
328
|
+
elif char == '-':
|
|
329
|
+
self._read_minus()
|
|
330
|
+
elif char == '|':
|
|
331
|
+
if self._peek(1) == '|':
|
|
332
|
+
self._add_token(TokenType.OR, '||')
|
|
333
|
+
self._advance()
|
|
334
|
+
self._advance()
|
|
335
|
+
else:
|
|
336
|
+
self._advance()
|
|
337
|
+
else:
|
|
338
|
+
self._advance()
|
|
339
|
+
|
|
340
|
+
self._add_token(TokenType.EOF, '')
|
|
341
|
+
return self.tokens
|
|
342
|
+
|
|
343
|
+
def _advance(self):
|
|
344
|
+
self.pos += 1
|
|
345
|
+
self.column += 1
|
|
346
|
+
|
|
347
|
+
def _peek(self, offset=0) -> str:
|
|
348
|
+
pos = self.pos + offset
|
|
349
|
+
if pos < len(self.source):
|
|
350
|
+
return self.source[pos]
|
|
351
|
+
return ''
|
|
352
|
+
|
|
353
|
+
def _add_token(self, token_type: TokenType, value: Any):
|
|
354
|
+
self.tokens.append(Token(token_type, value, self.line, self.column))
|
|
355
|
+
|
|
356
|
+
def _skip_whitespace(self):
|
|
357
|
+
while self.pos < len(self.source) and self.source[self.pos] in ' \t\r':
|
|
358
|
+
self._advance()
|
|
359
|
+
|
|
360
|
+
def _skip_comment(self):
|
|
361
|
+
while self.pos < len(self.source) and self.source[self.pos] != '\n':
|
|
362
|
+
self._advance()
|
|
363
|
+
|
|
364
|
+
def _read_string(self, quote_char: str):
|
|
365
|
+
self._advance()
|
|
366
|
+
start = self.pos
|
|
367
|
+
result = []
|
|
368
|
+
while self.pos < len(self.source) and self.source[self.pos] != quote_char:
|
|
369
|
+
if self.source[self.pos] == '\\' and self.pos + 1 < len(self.source):
|
|
370
|
+
# Handle escape sequences
|
|
371
|
+
next_char = self.source[self.pos + 1]
|
|
372
|
+
if next_char == 'n':
|
|
373
|
+
result.append('\n')
|
|
374
|
+
elif next_char == 't':
|
|
375
|
+
result.append('\t')
|
|
376
|
+
elif next_char == 'r':
|
|
377
|
+
result.append('\r')
|
|
378
|
+
elif next_char == '\\':
|
|
379
|
+
result.append('\\')
|
|
380
|
+
elif next_char == quote_char:
|
|
381
|
+
result.append(quote_char)
|
|
382
|
+
elif next_char == '"':
|
|
383
|
+
result.append('"')
|
|
384
|
+
elif next_char == "'":
|
|
385
|
+
result.append("'")
|
|
386
|
+
else:
|
|
387
|
+
result.append(self.source[self.pos])
|
|
388
|
+
result.append(next_char)
|
|
389
|
+
self._advance()
|
|
390
|
+
self._advance()
|
|
391
|
+
else:
|
|
392
|
+
result.append(self.source[self.pos])
|
|
393
|
+
self._advance()
|
|
394
|
+
value = ''.join(result)
|
|
395
|
+
self._add_token(TokenType.STRING, value)
|
|
396
|
+
self._advance()
|
|
397
|
+
|
|
398
|
+
def _read_raw_string(self):
|
|
399
|
+
"""Read raw string with backticks - no escape processing.
|
|
400
|
+
|
|
401
|
+
Useful for JSON: `{"id": "2819e1", "name": "test"}`
|
|
402
|
+
"""
|
|
403
|
+
self._advance() # Skip opening backtick
|
|
404
|
+
start = self.pos
|
|
405
|
+
while self.pos < len(self.source) and self.source[self.pos] != '`':
|
|
406
|
+
if self.source[self.pos] == '\n':
|
|
407
|
+
self.line += 1
|
|
408
|
+
self.column = 0
|
|
409
|
+
self._advance()
|
|
410
|
+
value = self.source[start:self.pos]
|
|
411
|
+
self._add_token(TokenType.STRING, value)
|
|
412
|
+
self._advance() # Skip closing backtick
|
|
413
|
+
|
|
414
|
+
def _read_number(self):
|
|
415
|
+
start = self.pos
|
|
416
|
+
if self.source[self.pos] == '-':
|
|
417
|
+
self._advance()
|
|
418
|
+
while self.pos < len(self.source) and (self.source[self.pos].isdigit() or self.source[self.pos] == '.'):
|
|
419
|
+
self._advance()
|
|
420
|
+
value = self.source[start:self.pos]
|
|
421
|
+
if '.' in value:
|
|
422
|
+
self._add_token(TokenType.NUMBER, float(value))
|
|
423
|
+
else:
|
|
424
|
+
self._add_token(TokenType.NUMBER, int(value))
|
|
425
|
+
|
|
426
|
+
def _read_identifier(self):
|
|
427
|
+
start = self.pos
|
|
428
|
+
while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
|
|
429
|
+
self._advance()
|
|
430
|
+
value = self.source[start:self.pos]
|
|
431
|
+
|
|
432
|
+
if value in ('True', 'true'):
|
|
433
|
+
self._add_token(TokenType.BOOLEAN, True)
|
|
434
|
+
elif value in ('False', 'false'):
|
|
435
|
+
self._add_token(TokenType.BOOLEAN, False)
|
|
436
|
+
elif value in ('null', 'None', 'none'):
|
|
437
|
+
self._add_token(TokenType.NULL, None)
|
|
438
|
+
elif value in TYPE_LITERALS:
|
|
439
|
+
# NEW: list and dict as type literals (e.g., cache = list;)
|
|
440
|
+
self._add_token(TokenType.TYPE_LITERAL, value)
|
|
441
|
+
elif value == 'as':
|
|
442
|
+
# NEW: 'as' keyword for foreach ... as ... syntax
|
|
443
|
+
self._add_token(TokenType.AS, value)
|
|
444
|
+
elif value in KEYWORDS:
|
|
445
|
+
self._add_token(TokenType.KEYWORD, value)
|
|
446
|
+
else:
|
|
447
|
+
self._add_token(TokenType.IDENTIFIER, value)
|
|
448
|
+
|
|
449
|
+
def _read_self_ref(self):
|
|
450
|
+
"""Read s@<name> or s@<name>.<member>... self-reference"""
|
|
451
|
+
start = self.pos
|
|
452
|
+
self._advance() # skip 's'
|
|
453
|
+
self._advance() # skip '@'
|
|
454
|
+
|
|
455
|
+
# Read the identifier path (Name.Member.SubMember)
|
|
456
|
+
path_parts = []
|
|
457
|
+
while self.pos < len(self.source):
|
|
458
|
+
# Read identifier part
|
|
459
|
+
part_start = self.pos
|
|
460
|
+
while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
|
|
461
|
+
self._advance()
|
|
462
|
+
if self.pos > part_start:
|
|
463
|
+
path_parts.append(self.source[part_start:self.pos])
|
|
464
|
+
|
|
465
|
+
# Check for dot to continue path
|
|
466
|
+
if self.pos < len(self.source) and self.source[self.pos] == '.':
|
|
467
|
+
self._advance() # skip '.'
|
|
468
|
+
else:
|
|
469
|
+
break
|
|
470
|
+
|
|
471
|
+
value = '.'.join(path_parts)
|
|
472
|
+
self._add_token(TokenType.SELF_REF, value)
|
|
473
|
+
|
|
474
|
+
def _read_global_ref(self):
|
|
475
|
+
"""Read r@<name> global variable declaration (equivalent to 'global')"""
|
|
476
|
+
start = self.pos
|
|
477
|
+
self._advance() # skip 'r'
|
|
478
|
+
self._advance() # skip '@'
|
|
479
|
+
|
|
480
|
+
# Read the identifier
|
|
481
|
+
name_start = self.pos
|
|
482
|
+
while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
|
|
483
|
+
self._advance()
|
|
484
|
+
|
|
485
|
+
value = self.source[name_start:self.pos]
|
|
486
|
+
self._add_token(TokenType.GLOBAL_REF, value)
|
|
487
|
+
|
|
488
|
+
def _read_shared_ref(self):
|
|
489
|
+
"""Read $<name> shared object reference"""
|
|
490
|
+
self._advance() # skip '$'
|
|
491
|
+
|
|
492
|
+
# Read the identifier (shared object name)
|
|
493
|
+
name_start = self.pos
|
|
494
|
+
while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
|
|
495
|
+
self._advance()
|
|
496
|
+
|
|
497
|
+
value = self.source[name_start:self.pos]
|
|
498
|
+
if not value:
|
|
499
|
+
self.error("Expected identifier after '$'")
|
|
500
|
+
self._add_token(TokenType.SHARED_REF, value)
|
|
501
|
+
|
|
502
|
+
def _read_captured_ref(self):
|
|
503
|
+
"""Read %<name> captured reference (captures value at definition time for infusions)"""
|
|
504
|
+
self._advance() # skip '%'
|
|
505
|
+
|
|
506
|
+
# Read the identifier (captured reference name)
|
|
507
|
+
name_start = self.pos
|
|
508
|
+
while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'):
|
|
509
|
+
self._advance()
|
|
510
|
+
|
|
511
|
+
value = self.source[name_start:self.pos]
|
|
512
|
+
if not value:
|
|
513
|
+
self.error("Expected identifier after '%'")
|
|
514
|
+
self._add_token(TokenType.CAPTURED_REF, value)
|
|
515
|
+
|
|
516
|
+
def _read_less_than(self):
|
|
517
|
+
# Check for <<== (code infusion left)
|
|
518
|
+
if self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
|
|
519
|
+
self._add_token(TokenType.INFUSE_LEFT, '<<==')
|
|
520
|
+
for _ in range(4): self._advance()
|
|
521
|
+
# Check for <== (basic injection left)
|
|
522
|
+
elif self._peek(1) == '=' and self._peek(2) == '=':
|
|
523
|
+
self._add_token(TokenType.INJECT_LEFT, '<==')
|
|
524
|
+
for _ in range(3): self._advance()
|
|
525
|
+
elif self._peek(1) == '=':
|
|
526
|
+
self._add_token(TokenType.COMPARE_LE, '<=')
|
|
527
|
+
self._advance()
|
|
528
|
+
self._advance()
|
|
529
|
+
elif self._peek(1) == '-':
|
|
530
|
+
self._add_token(TokenType.FLOW_LEFT, '<-')
|
|
531
|
+
self._advance()
|
|
532
|
+
self._advance()
|
|
533
|
+
else:
|
|
534
|
+
self._add_token(TokenType.COMPARE_LT, '<')
|
|
535
|
+
self._advance()
|
|
536
|
+
|
|
537
|
+
def _read_greater_than(self):
|
|
538
|
+
if self._peek(1) == '=':
|
|
539
|
+
self._add_token(TokenType.COMPARE_GE, '>=')
|
|
540
|
+
self._advance()
|
|
541
|
+
self._advance()
|
|
542
|
+
else:
|
|
543
|
+
self._add_token(TokenType.COMPARE_GT, '>')
|
|
544
|
+
self._advance()
|
|
545
|
+
|
|
546
|
+
def _read_equals(self):
|
|
547
|
+
# Check for ==>>+ (code infusion right plus)
|
|
548
|
+
if self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>' and self._peek(4) == '+':
|
|
549
|
+
self._add_token(TokenType.INFUSE_PLUS_RIGHT, '==>>+')
|
|
550
|
+
for _ in range(5): self._advance()
|
|
551
|
+
# Check for ==>>- (code infusion right minus)
|
|
552
|
+
elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>' and self._peek(4) == '-':
|
|
553
|
+
self._add_token(TokenType.INFUSE_MINUS_RIGHT, '==>>-')
|
|
554
|
+
for _ in range(5): self._advance()
|
|
555
|
+
# Check for ==>> (code infusion right)
|
|
556
|
+
elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '>':
|
|
557
|
+
self._add_token(TokenType.INFUSE_RIGHT, '==>>')
|
|
558
|
+
for _ in range(4): self._advance()
|
|
559
|
+
# Check for ==>+ (injection right plus)
|
|
560
|
+
elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '+':
|
|
561
|
+
self._add_token(TokenType.INJECT_PLUS_RIGHT, '==>+')
|
|
562
|
+
for _ in range(4): self._advance()
|
|
563
|
+
# Check for ==>- (injection right minus - moves & removes)
|
|
564
|
+
elif self._peek(1) == '=' and self._peek(2) == '>' and self._peek(3) == '-':
|
|
565
|
+
self._add_token(TokenType.INJECT_MINUS_RIGHT, '==>-')
|
|
566
|
+
for _ in range(4): self._advance()
|
|
567
|
+
# Check for ==> (basic injection right)
|
|
568
|
+
elif self._peek(1) == '=' and self._peek(2) == '>':
|
|
569
|
+
self._add_token(TokenType.INJECT_RIGHT, '==>')
|
|
570
|
+
for _ in range(3): self._advance()
|
|
571
|
+
elif self._peek(1) == '=':
|
|
572
|
+
self._add_token(TokenType.COMPARE_EQ, '==')
|
|
573
|
+
self._advance()
|
|
574
|
+
self._advance()
|
|
575
|
+
else:
|
|
576
|
+
self._add_token(TokenType.EQUALS, '=')
|
|
577
|
+
self._advance()
|
|
578
|
+
|
|
579
|
+
def _read_not(self):
|
|
580
|
+
if self._peek(1) == '=':
|
|
581
|
+
self._add_token(TokenType.COMPARE_NE, '!=')
|
|
582
|
+
self._advance()
|
|
583
|
+
self._advance()
|
|
584
|
+
else:
|
|
585
|
+
self._add_token(TokenType.NOT, '!')
|
|
586
|
+
self._advance()
|
|
587
|
+
|
|
588
|
+
def _read_minus(self):
|
|
589
|
+
# Check for -<<== (code infusion minus left)
|
|
590
|
+
if self._peek(1) == '<' and self._peek(2) == '<' and self._peek(3) == '=' and self._peek(4) == '=':
|
|
591
|
+
self._add_token(TokenType.INFUSE_MINUS_LEFT, '-<<==')
|
|
592
|
+
for _ in range(5): self._advance()
|
|
593
|
+
# Check for -<== (injection minus left - move & remove)
|
|
594
|
+
elif self._peek(1) == '<' and self._peek(2) == '=' and self._peek(3) == '=':
|
|
595
|
+
self._add_token(TokenType.INJECT_MINUS_LEFT, '-<==')
|
|
596
|
+
for _ in range(4): self._advance()
|
|
597
|
+
# Check for -==> (injection right minus)
|
|
598
|
+
elif self._peek(1) == '=' and self._peek(2) == '=' and self._peek(3) == '>':
|
|
599
|
+
self._add_token(TokenType.INJECT_MINUS_RIGHT, '-==>')
|
|
600
|
+
for _ in range(4): self._advance()
|
|
601
|
+
elif self._peek(1) == '>':
|
|
602
|
+
self._add_token(TokenType.FLOW_RIGHT, '->')
|
|
603
|
+
self._advance()
|
|
604
|
+
self._advance()
|
|
605
|
+
else:
|
|
606
|
+
self._add_token(TokenType.MINUS, '-')
|
|
607
|
+
self._advance()
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@dataclass
|
|
611
|
+
class ASTNode:
|
|
612
|
+
type: str
|
|
613
|
+
value: Any = None
|
|
614
|
+
children: List['ASTNode'] = field(default_factory=list)
|
|
615
|
+
line: int = 0
|
|
616
|
+
column: int = 0
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class CSSLParser:
|
|
620
|
+
"""Parses CSSL tokens into an Abstract Syntax Tree."""
|
|
621
|
+
|
|
622
|
+
def __init__(self, tokens: List[Token], source_lines: List[str] = None):
|
|
623
|
+
self.tokens = [t for t in tokens if t.type != TokenType.NEWLINE]
|
|
624
|
+
self.pos = 0
|
|
625
|
+
self.source_lines = source_lines or []
|
|
626
|
+
|
|
627
|
+
def get_source_line(self, line_num: int) -> str:
|
|
628
|
+
"""Get a specific source line for error reporting"""
|
|
629
|
+
if 0 < line_num <= len(self.source_lines):
|
|
630
|
+
return self.source_lines[line_num - 1]
|
|
631
|
+
return ""
|
|
632
|
+
|
|
633
|
+
def error(self, message: str, token: Token = None):
|
|
634
|
+
"""Raise a syntax error with location info"""
|
|
635
|
+
if token is None:
|
|
636
|
+
token = self._current()
|
|
637
|
+
raise CSSLSyntaxError(
|
|
638
|
+
message,
|
|
639
|
+
line=token.line,
|
|
640
|
+
column=token.column,
|
|
641
|
+
source_line=self.get_source_line(token.line)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def parse(self) -> ASTNode:
|
|
645
|
+
"""Parse a service file (wrapped in braces)"""
|
|
646
|
+
root = ASTNode('service', children=[])
|
|
647
|
+
|
|
648
|
+
if not self._match(TokenType.BLOCK_START):
|
|
649
|
+
self.error(f"Expected '{{' at start of service, got {self._current().type.name}")
|
|
650
|
+
|
|
651
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
652
|
+
if self._match_keyword('service-init'):
|
|
653
|
+
root.children.append(self._parse_service_init())
|
|
654
|
+
elif self._match_keyword('service-include'):
|
|
655
|
+
root.children.append(self._parse_service_include())
|
|
656
|
+
elif self._match_keyword('service-run'):
|
|
657
|
+
root.children.append(self._parse_service_run())
|
|
658
|
+
# NEW: package block support
|
|
659
|
+
elif self._match_keyword('package'):
|
|
660
|
+
root.children.append(self._parse_package())
|
|
661
|
+
# NEW: package-includes block support
|
|
662
|
+
elif self._match_keyword('package-includes'):
|
|
663
|
+
root.children.append(self._parse_package_includes())
|
|
664
|
+
# NEW: struct at top level
|
|
665
|
+
elif self._match_keyword('struct'):
|
|
666
|
+
root.children.append(self._parse_struct())
|
|
667
|
+
# NEW: define at top level
|
|
668
|
+
elif self._match_keyword('define'):
|
|
669
|
+
root.children.append(self._parse_define())
|
|
670
|
+
else:
|
|
671
|
+
self._advance()
|
|
672
|
+
|
|
673
|
+
self._match(TokenType.BLOCK_END)
|
|
674
|
+
return root
|
|
675
|
+
|
|
676
|
+
def _is_function_modifier(self, value: str) -> bool:
|
|
677
|
+
"""Check if a keyword is a function modifier"""
|
|
678
|
+
return value in ('undefined', 'open', 'meta', 'super', 'closed', 'private', 'virtual', 'sqlbased')
|
|
679
|
+
|
|
680
|
+
def _is_type_keyword(self, value: str) -> bool:
|
|
681
|
+
"""Check if a keyword is a type declaration"""
|
|
682
|
+
return value in ('int', 'string', 'float', 'bool', 'void', 'json', 'array', 'vector', 'stack',
|
|
683
|
+
'list', 'dictionary', 'dict', 'instance', 'map',
|
|
684
|
+
'dynamic', 'datastruct', 'dataspace', 'shuffled', 'iterator', 'combo', 'structure')
|
|
685
|
+
|
|
686
|
+
def _looks_like_function_declaration(self) -> bool:
|
|
687
|
+
"""Check if current position looks like a C-style function declaration.
|
|
688
|
+
|
|
689
|
+
Patterns:
|
|
690
|
+
- int funcName(...)
|
|
691
|
+
- undefined int funcName(...)
|
|
692
|
+
- vector<string> funcName(...)
|
|
693
|
+
- undefined void funcName(...)
|
|
694
|
+
"""
|
|
695
|
+
saved_pos = self.pos
|
|
696
|
+
|
|
697
|
+
# Skip modifiers (undefined, open, meta, super, closed, private, virtual)
|
|
698
|
+
while self._check(TokenType.KEYWORD) and self._is_function_modifier(self._current().value):
|
|
699
|
+
self._advance()
|
|
700
|
+
|
|
701
|
+
# Check for type keyword
|
|
702
|
+
if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
|
|
703
|
+
self._advance()
|
|
704
|
+
|
|
705
|
+
# Skip generic type parameters <T>
|
|
706
|
+
if self._check(TokenType.COMPARE_LT):
|
|
707
|
+
depth = 1
|
|
708
|
+
self._advance()
|
|
709
|
+
while depth > 0 and not self._is_at_end():
|
|
710
|
+
if self._check(TokenType.COMPARE_LT):
|
|
711
|
+
depth += 1
|
|
712
|
+
elif self._check(TokenType.COMPARE_GT):
|
|
713
|
+
depth -= 1
|
|
714
|
+
self._advance()
|
|
715
|
+
|
|
716
|
+
# Check for identifier followed by (
|
|
717
|
+
if self._check(TokenType.IDENTIFIER):
|
|
718
|
+
self._advance()
|
|
719
|
+
is_func = self._check(TokenType.PAREN_START)
|
|
720
|
+
self.pos = saved_pos
|
|
721
|
+
return is_func
|
|
722
|
+
|
|
723
|
+
self.pos = saved_pos
|
|
724
|
+
return False
|
|
725
|
+
|
|
726
|
+
def _looks_like_typed_variable(self) -> bool:
|
|
727
|
+
"""Check if current position looks like a typed variable declaration.
|
|
728
|
+
|
|
729
|
+
Patterns:
|
|
730
|
+
- int x;
|
|
731
|
+
- stack<string> myStack;
|
|
732
|
+
- vector<int> nums = [1,2,3];
|
|
733
|
+
|
|
734
|
+
Distinguishes from function declarations by checking for '(' after identifier.
|
|
735
|
+
"""
|
|
736
|
+
saved_pos = self.pos
|
|
737
|
+
|
|
738
|
+
# Check for type keyword
|
|
739
|
+
if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
|
|
740
|
+
self._advance()
|
|
741
|
+
|
|
742
|
+
# Skip generic type parameters <T>
|
|
743
|
+
if self._check(TokenType.COMPARE_LT):
|
|
744
|
+
depth = 1
|
|
745
|
+
self._advance()
|
|
746
|
+
while depth > 0 and not self._is_at_end():
|
|
747
|
+
if self._check(TokenType.COMPARE_LT):
|
|
748
|
+
depth += 1
|
|
749
|
+
elif self._check(TokenType.COMPARE_GT):
|
|
750
|
+
depth -= 1
|
|
751
|
+
self._advance()
|
|
752
|
+
|
|
753
|
+
# Check for identifier NOT followed by ( (that would be a function)
|
|
754
|
+
if self._check(TokenType.IDENTIFIER):
|
|
755
|
+
self._advance()
|
|
756
|
+
# If followed by '(' it's a function, not a variable
|
|
757
|
+
is_var = not self._check(TokenType.PAREN_START)
|
|
758
|
+
self.pos = saved_pos
|
|
759
|
+
return is_var
|
|
760
|
+
|
|
761
|
+
self.pos = saved_pos
|
|
762
|
+
return False
|
|
763
|
+
|
|
764
|
+
def _parse_typed_function(self) -> ASTNode:
|
|
765
|
+
"""Parse C-style typed function declaration.
|
|
766
|
+
|
|
767
|
+
Patterns:
|
|
768
|
+
- int Add(int a, int b) { }
|
|
769
|
+
- undefined int Func() { }
|
|
770
|
+
- open void Handler(open Params) { }
|
|
771
|
+
- vector<string> GetNames() { }
|
|
772
|
+
"""
|
|
773
|
+
modifiers = []
|
|
774
|
+
return_type = None
|
|
775
|
+
generic_type = None
|
|
776
|
+
|
|
777
|
+
# Collect modifiers (undefined, open, meta, super, closed, private, virtual)
|
|
778
|
+
while self._check(TokenType.KEYWORD) and self._is_function_modifier(self._current().value):
|
|
779
|
+
modifiers.append(self._advance().value)
|
|
780
|
+
|
|
781
|
+
# Get return type
|
|
782
|
+
if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
|
|
783
|
+
return_type = self._advance().value
|
|
784
|
+
|
|
785
|
+
# Check for generic type <T>
|
|
786
|
+
if self._check(TokenType.COMPARE_LT):
|
|
787
|
+
self._advance() # skip <
|
|
788
|
+
generic_parts = []
|
|
789
|
+
depth = 1
|
|
790
|
+
while depth > 0 and not self._is_at_end():
|
|
791
|
+
if self._check(TokenType.COMPARE_LT):
|
|
792
|
+
depth += 1
|
|
793
|
+
generic_parts.append('<')
|
|
794
|
+
elif self._check(TokenType.COMPARE_GT):
|
|
795
|
+
depth -= 1
|
|
796
|
+
if depth > 0:
|
|
797
|
+
generic_parts.append('>')
|
|
798
|
+
elif self._check(TokenType.COMMA):
|
|
799
|
+
generic_parts.append(',')
|
|
800
|
+
else:
|
|
801
|
+
generic_parts.append(self._current().value)
|
|
802
|
+
self._advance()
|
|
803
|
+
generic_type = ''.join(generic_parts)
|
|
804
|
+
|
|
805
|
+
# Get function name
|
|
806
|
+
name = self._advance().value
|
|
807
|
+
|
|
808
|
+
# Parse parameters
|
|
809
|
+
params = []
|
|
810
|
+
self._expect(TokenType.PAREN_START)
|
|
811
|
+
|
|
812
|
+
while not self._check(TokenType.PAREN_END) and not self._is_at_end():
|
|
813
|
+
param_info = {}
|
|
814
|
+
|
|
815
|
+
# Handle 'open' keyword for open parameters
|
|
816
|
+
if self._match_keyword('open'):
|
|
817
|
+
param_info['open'] = True
|
|
818
|
+
|
|
819
|
+
# Handle type annotations
|
|
820
|
+
if self._check(TokenType.KEYWORD) and self._is_type_keyword(self._current().value):
|
|
821
|
+
param_info['type'] = self._advance().value
|
|
822
|
+
|
|
823
|
+
# Check for generic type parameter <T>
|
|
824
|
+
if self._check(TokenType.COMPARE_LT):
|
|
825
|
+
self._advance()
|
|
826
|
+
generic_parts = []
|
|
827
|
+
depth = 1
|
|
828
|
+
while depth > 0 and not self._is_at_end():
|
|
829
|
+
if self._check(TokenType.COMPARE_LT):
|
|
830
|
+
depth += 1
|
|
831
|
+
generic_parts.append('<')
|
|
832
|
+
elif self._check(TokenType.COMPARE_GT):
|
|
833
|
+
depth -= 1
|
|
834
|
+
if depth > 0:
|
|
835
|
+
generic_parts.append('>')
|
|
836
|
+
elif self._check(TokenType.COMMA):
|
|
837
|
+
generic_parts.append(',')
|
|
838
|
+
else:
|
|
839
|
+
generic_parts.append(self._current().value)
|
|
840
|
+
self._advance()
|
|
841
|
+
param_info['generic'] = ''.join(generic_parts)
|
|
842
|
+
|
|
843
|
+
# Handle reference operator &
|
|
844
|
+
if self._match(TokenType.AMPERSAND):
|
|
845
|
+
param_info['ref'] = True
|
|
846
|
+
|
|
847
|
+
# Get parameter name
|
|
848
|
+
if self._check(TokenType.IDENTIFIER):
|
|
849
|
+
param_name = self._advance().value
|
|
850
|
+
if param_info:
|
|
851
|
+
params.append({'name': param_name, **param_info})
|
|
852
|
+
else:
|
|
853
|
+
params.append(param_name)
|
|
854
|
+
elif self._check(TokenType.KEYWORD):
|
|
855
|
+
# Parameter name could be a keyword
|
|
856
|
+
param_name = self._advance().value
|
|
857
|
+
if param_info:
|
|
858
|
+
params.append({'name': param_name, **param_info})
|
|
859
|
+
else:
|
|
860
|
+
params.append(param_name)
|
|
861
|
+
|
|
862
|
+
self._match(TokenType.COMMA)
|
|
863
|
+
|
|
864
|
+
self._expect(TokenType.PAREN_END)
|
|
865
|
+
|
|
866
|
+
# Parse function body
|
|
867
|
+
node = ASTNode('function', value={
|
|
868
|
+
'name': name,
|
|
869
|
+
'params': params,
|
|
870
|
+
'return_type': return_type,
|
|
871
|
+
'generic_type': generic_type,
|
|
872
|
+
'modifiers': modifiers
|
|
873
|
+
}, children=[])
|
|
874
|
+
|
|
875
|
+
self._expect(TokenType.BLOCK_START)
|
|
876
|
+
|
|
877
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
878
|
+
stmt = self._parse_statement()
|
|
879
|
+
if stmt:
|
|
880
|
+
node.children.append(stmt)
|
|
881
|
+
|
|
882
|
+
self._expect(TokenType.BLOCK_END)
|
|
883
|
+
return node
|
|
884
|
+
|
|
885
|
+
def _looks_like_typed_variable(self) -> bool:
|
|
886
|
+
"""Check if current position looks like a typed variable declaration:
|
|
887
|
+
type_name varName; or type_name<T> varName; or type_name varName = value;
|
|
888
|
+
"""
|
|
889
|
+
# Save position
|
|
890
|
+
saved_pos = self.pos
|
|
891
|
+
|
|
892
|
+
# Must start with a type keyword (int, string, stack, vector, etc.)
|
|
893
|
+
if not self._check(TokenType.KEYWORD):
|
|
894
|
+
return False
|
|
895
|
+
|
|
896
|
+
type_name = self._current().value
|
|
897
|
+
|
|
898
|
+
# Skip known type keywords
|
|
899
|
+
type_keywords = {'int', 'string', 'float', 'bool', 'dynamic', 'void',
|
|
900
|
+
'stack', 'vector', 'datastruct', 'dataspace', 'shuffled',
|
|
901
|
+
'iterator', 'combo', 'array', 'openquote', 'json',
|
|
902
|
+
'list', 'dictionary', 'dict', 'instance', 'map'}
|
|
903
|
+
if type_name not in type_keywords:
|
|
904
|
+
return False
|
|
905
|
+
|
|
906
|
+
self._advance()
|
|
907
|
+
|
|
908
|
+
# Check for optional generic <T>
|
|
909
|
+
if self._match(TokenType.COMPARE_LT):
|
|
910
|
+
# Skip until >
|
|
911
|
+
depth = 1
|
|
912
|
+
while depth > 0 and not self._is_at_end():
|
|
913
|
+
if self._check(TokenType.COMPARE_LT):
|
|
914
|
+
depth += 1
|
|
915
|
+
elif self._check(TokenType.COMPARE_GT):
|
|
916
|
+
depth -= 1
|
|
917
|
+
self._advance()
|
|
918
|
+
|
|
919
|
+
# Next should be an identifier (variable name), not '(' (function) or ';'
|
|
920
|
+
result = self._check(TokenType.IDENTIFIER)
|
|
921
|
+
|
|
922
|
+
# Restore position
|
|
923
|
+
self.pos = saved_pos
|
|
924
|
+
return result
|
|
925
|
+
|
|
926
|
+
def _parse_typed_variable(self) -> Optional[ASTNode]:
|
|
927
|
+
"""Parse a typed variable declaration: type varName; or type<T> varName = value;"""
|
|
928
|
+
# Get type name
|
|
929
|
+
type_name = self._advance().value # Consume type keyword
|
|
930
|
+
|
|
931
|
+
# Check for generic type <T> or instance<"name">
|
|
932
|
+
element_type = None
|
|
933
|
+
if self._match(TokenType.COMPARE_LT):
|
|
934
|
+
# For instance<"name">, element_type can be a string literal
|
|
935
|
+
if type_name == 'instance' and self._check(TokenType.STRING):
|
|
936
|
+
element_type = self._advance().value
|
|
937
|
+
elif self._check(TokenType.KEYWORD) or self._check(TokenType.IDENTIFIER):
|
|
938
|
+
element_type = self._advance().value
|
|
939
|
+
self._expect(TokenType.COMPARE_GT)
|
|
940
|
+
|
|
941
|
+
# Get variable name
|
|
942
|
+
if not self._check(TokenType.IDENTIFIER):
|
|
943
|
+
return None
|
|
944
|
+
var_name = self._advance().value
|
|
945
|
+
|
|
946
|
+
# Check for assignment or just declaration
|
|
947
|
+
value = None
|
|
948
|
+
if self._match(TokenType.EQUALS):
|
|
949
|
+
value = self._parse_expression()
|
|
950
|
+
|
|
951
|
+
self._match(TokenType.SEMICOLON)
|
|
952
|
+
|
|
953
|
+
# For instance<"name">, create a special node type
|
|
954
|
+
if type_name == 'instance':
|
|
955
|
+
return ASTNode('instance_declaration', value={
|
|
956
|
+
'instance_name': element_type,
|
|
957
|
+
'name': var_name,
|
|
958
|
+
'value': value
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
return ASTNode('typed_declaration', value={
|
|
962
|
+
'type': type_name,
|
|
963
|
+
'element_type': element_type,
|
|
964
|
+
'name': var_name,
|
|
965
|
+
'value': value
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
def parse_program(self) -> ASTNode:
|
|
969
|
+
"""Parse a standalone program (no service wrapper)"""
|
|
970
|
+
root = ASTNode('program', children=[])
|
|
971
|
+
|
|
972
|
+
while not self._is_at_end():
|
|
973
|
+
if self._match_keyword('struct'):
|
|
974
|
+
root.children.append(self._parse_struct())
|
|
975
|
+
elif self._match_keyword('class'):
|
|
976
|
+
root.children.append(self._parse_class())
|
|
977
|
+
elif self._match_keyword('define'):
|
|
978
|
+
root.children.append(self._parse_define())
|
|
979
|
+
# Check for C-style typed function declarations
|
|
980
|
+
elif self._looks_like_function_declaration():
|
|
981
|
+
root.children.append(self._parse_typed_function())
|
|
982
|
+
# Check for typed variable declarations (int x;, stack<string> s;)
|
|
983
|
+
elif self._looks_like_typed_variable():
|
|
984
|
+
decl = self._parse_typed_variable()
|
|
985
|
+
if decl:
|
|
986
|
+
root.children.append(decl)
|
|
987
|
+
# Handle service blocks
|
|
988
|
+
elif self._match_keyword('service-init'):
|
|
989
|
+
root.children.append(self._parse_service_init())
|
|
990
|
+
elif self._match_keyword('service-include'):
|
|
991
|
+
root.children.append(self._parse_service_include())
|
|
992
|
+
elif self._match_keyword('service-run'):
|
|
993
|
+
root.children.append(self._parse_service_run())
|
|
994
|
+
elif self._match_keyword('package'):
|
|
995
|
+
root.children.append(self._parse_package())
|
|
996
|
+
elif self._match_keyword('package-includes'):
|
|
997
|
+
root.children.append(self._parse_package_includes())
|
|
998
|
+
# Handle global declarations
|
|
999
|
+
elif self._match_keyword('global'):
|
|
1000
|
+
stmt = self._parse_expression_statement()
|
|
1001
|
+
if stmt:
|
|
1002
|
+
# Wrap in global_assignment to mark as global variable
|
|
1003
|
+
global_stmt = ASTNode('global_assignment', value=stmt)
|
|
1004
|
+
root.children.append(global_stmt)
|
|
1005
|
+
elif self._check(TokenType.GLOBAL_REF):
|
|
1006
|
+
stmt = self._parse_expression_statement()
|
|
1007
|
+
if stmt:
|
|
1008
|
+
# Wrap in global_assignment to mark as global variable (same as 'global' keyword)
|
|
1009
|
+
global_stmt = ASTNode('global_assignment', value=stmt)
|
|
1010
|
+
root.children.append(global_stmt)
|
|
1011
|
+
# Handle statements
|
|
1012
|
+
elif self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or self._check(TokenType.SELF_REF) or self._check(TokenType.SHARED_REF):
|
|
1013
|
+
stmt = self._parse_expression_statement()
|
|
1014
|
+
if stmt:
|
|
1015
|
+
root.children.append(stmt)
|
|
1016
|
+
elif self._match_keyword('if'):
|
|
1017
|
+
root.children.append(self._parse_if())
|
|
1018
|
+
elif self._match_keyword('while'):
|
|
1019
|
+
root.children.append(self._parse_while())
|
|
1020
|
+
elif self._match_keyword('for'):
|
|
1021
|
+
root.children.append(self._parse_for())
|
|
1022
|
+
elif self._match_keyword('foreach'):
|
|
1023
|
+
root.children.append(self._parse_foreach())
|
|
1024
|
+
# Skip comments and newlines
|
|
1025
|
+
elif self._check(TokenType.COMMENT) or self._check(TokenType.NEWLINE):
|
|
1026
|
+
self._advance()
|
|
1027
|
+
else:
|
|
1028
|
+
self._advance()
|
|
1029
|
+
|
|
1030
|
+
return root
|
|
1031
|
+
|
|
1032
|
+
def _current(self) -> Token:
|
|
1033
|
+
if self.pos < len(self.tokens):
|
|
1034
|
+
return self.tokens[self.pos]
|
|
1035
|
+
return Token(TokenType.EOF, '', 0, 0)
|
|
1036
|
+
|
|
1037
|
+
def _peek(self, offset=0) -> Token:
|
|
1038
|
+
pos = self.pos + offset
|
|
1039
|
+
if pos < len(self.tokens):
|
|
1040
|
+
return self.tokens[pos]
|
|
1041
|
+
return Token(TokenType.EOF, '', 0, 0)
|
|
1042
|
+
|
|
1043
|
+
def _advance(self) -> Token:
|
|
1044
|
+
token = self._current()
|
|
1045
|
+
self.pos += 1
|
|
1046
|
+
return token
|
|
1047
|
+
|
|
1048
|
+
def _is_at_end(self) -> bool:
|
|
1049
|
+
return self._current().type == TokenType.EOF
|
|
1050
|
+
|
|
1051
|
+
def _check(self, token_type: TokenType) -> bool:
|
|
1052
|
+
return self._current().type == token_type
|
|
1053
|
+
|
|
1054
|
+
def _match(self, token_type: TokenType) -> bool:
|
|
1055
|
+
if self._check(token_type):
|
|
1056
|
+
self._advance()
|
|
1057
|
+
return True
|
|
1058
|
+
return False
|
|
1059
|
+
|
|
1060
|
+
def _match_keyword(self, keyword: str) -> bool:
|
|
1061
|
+
if self._current().type == TokenType.KEYWORD and self._current().value == keyword:
|
|
1062
|
+
self._advance()
|
|
1063
|
+
return True
|
|
1064
|
+
return False
|
|
1065
|
+
|
|
1066
|
+
def _expect(self, token_type: TokenType, message: str = None):
|
|
1067
|
+
if not self._match(token_type):
|
|
1068
|
+
msg = message or f"Expected {token_type.name}, got {self._current().type.name}"
|
|
1069
|
+
self.error(msg)
|
|
1070
|
+
return self.tokens[self.pos - 1]
|
|
1071
|
+
|
|
1072
|
+
def _parse_service_init(self) -> ASTNode:
|
|
1073
|
+
node = ASTNode('service-init', children=[])
|
|
1074
|
+
self._expect(TokenType.BLOCK_START)
|
|
1075
|
+
|
|
1076
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1077
|
+
if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
|
|
1078
|
+
key = self._advance().value
|
|
1079
|
+
self._expect(TokenType.COLON)
|
|
1080
|
+
value = self._parse_value()
|
|
1081
|
+
node.children.append(ASTNode('property', value={'key': key, 'value': value}))
|
|
1082
|
+
self._match(TokenType.SEMICOLON)
|
|
1083
|
+
else:
|
|
1084
|
+
self._advance()
|
|
1085
|
+
|
|
1086
|
+
self._expect(TokenType.BLOCK_END)
|
|
1087
|
+
return node
|
|
1088
|
+
|
|
1089
|
+
def _parse_service_include(self) -> ASTNode:
|
|
1090
|
+
"""Parse service-include block for importing modules and files
|
|
1091
|
+
|
|
1092
|
+
Syntax:
|
|
1093
|
+
service-include {
|
|
1094
|
+
@KernelClient <== get(include(cso_root('/root32/etc/tasks/kernel.cssl')));
|
|
1095
|
+
@Time <== get('time');
|
|
1096
|
+
@Secrets <== get('secrets');
|
|
1097
|
+
}
|
|
1098
|
+
"""
|
|
1099
|
+
node = ASTNode('service-include', children=[])
|
|
1100
|
+
self._expect(TokenType.BLOCK_START)
|
|
1101
|
+
|
|
1102
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1103
|
+
# Parse module injection statements like @ModuleName <== get(...);
|
|
1104
|
+
if self._check(TokenType.AT):
|
|
1105
|
+
stmt = self._parse_expression_statement()
|
|
1106
|
+
if stmt:
|
|
1107
|
+
node.children.append(stmt)
|
|
1108
|
+
elif self._check(TokenType.IDENTIFIER):
|
|
1109
|
+
# Also support identifier-based assignments: moduleName <== get(...);
|
|
1110
|
+
stmt = self._parse_expression_statement()
|
|
1111
|
+
if stmt:
|
|
1112
|
+
node.children.append(stmt)
|
|
1113
|
+
else:
|
|
1114
|
+
self._advance()
|
|
1115
|
+
|
|
1116
|
+
self._expect(TokenType.BLOCK_END)
|
|
1117
|
+
return node
|
|
1118
|
+
|
|
1119
|
+
def _parse_service_run(self) -> ASTNode:
|
|
1120
|
+
node = ASTNode('service-run', children=[])
|
|
1121
|
+
self._expect(TokenType.BLOCK_START)
|
|
1122
|
+
|
|
1123
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1124
|
+
if self._match_keyword('struct'):
|
|
1125
|
+
node.children.append(self._parse_struct())
|
|
1126
|
+
elif self._match_keyword('define'):
|
|
1127
|
+
node.children.append(self._parse_define())
|
|
1128
|
+
else:
|
|
1129
|
+
self._advance()
|
|
1130
|
+
|
|
1131
|
+
self._expect(TokenType.BLOCK_END)
|
|
1132
|
+
return node
|
|
1133
|
+
|
|
1134
|
+
def _parse_package(self) -> ASTNode:
|
|
1135
|
+
"""Parse package {} block for service metadata - NEW
|
|
1136
|
+
|
|
1137
|
+
Syntax:
|
|
1138
|
+
package {
|
|
1139
|
+
service = "ServiceName";
|
|
1140
|
+
exec = @Start();
|
|
1141
|
+
version = "1.0.0";
|
|
1142
|
+
description = "Beschreibung";
|
|
1143
|
+
}
|
|
1144
|
+
"""
|
|
1145
|
+
node = ASTNode('package', children=[])
|
|
1146
|
+
self._expect(TokenType.BLOCK_START)
|
|
1147
|
+
|
|
1148
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1149
|
+
if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
|
|
1150
|
+
key = self._advance().value
|
|
1151
|
+
self._expect(TokenType.EQUALS)
|
|
1152
|
+
value = self._parse_expression()
|
|
1153
|
+
node.children.append(ASTNode('package_property', value={'key': key, 'value': value}))
|
|
1154
|
+
self._match(TokenType.SEMICOLON)
|
|
1155
|
+
else:
|
|
1156
|
+
self._advance()
|
|
1157
|
+
|
|
1158
|
+
self._expect(TokenType.BLOCK_END)
|
|
1159
|
+
return node
|
|
1160
|
+
|
|
1161
|
+
def _parse_package_includes(self) -> ASTNode:
|
|
1162
|
+
"""Parse package-includes {} block for imports - NEW
|
|
1163
|
+
|
|
1164
|
+
Syntax:
|
|
1165
|
+
package-includes {
|
|
1166
|
+
@Lists = get('list');
|
|
1167
|
+
@OS = get('os');
|
|
1168
|
+
@Time = get('time');
|
|
1169
|
+
@VSRam = get('vsramsdk');
|
|
1170
|
+
}
|
|
1171
|
+
"""
|
|
1172
|
+
node = ASTNode('package-includes', children=[])
|
|
1173
|
+
self._expect(TokenType.BLOCK_START)
|
|
1174
|
+
|
|
1175
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1176
|
+
# Parse module injection statements like @ModuleName = get(...);
|
|
1177
|
+
if self._check(TokenType.AT):
|
|
1178
|
+
stmt = self._parse_expression_statement()
|
|
1179
|
+
if stmt:
|
|
1180
|
+
node.children.append(stmt)
|
|
1181
|
+
elif self._check(TokenType.IDENTIFIER):
|
|
1182
|
+
# Also support identifier-based assignments
|
|
1183
|
+
stmt = self._parse_expression_statement()
|
|
1184
|
+
if stmt:
|
|
1185
|
+
node.children.append(stmt)
|
|
1186
|
+
else:
|
|
1187
|
+
self._advance()
|
|
1188
|
+
|
|
1189
|
+
self._expect(TokenType.BLOCK_END)
|
|
1190
|
+
return node
|
|
1191
|
+
|
|
1192
|
+
def _parse_struct(self) -> ASTNode:
|
|
1193
|
+
name = self._advance().value
|
|
1194
|
+
is_global = False
|
|
1195
|
+
|
|
1196
|
+
# Check for (@) decorator: struct Name(@) { ... }
|
|
1197
|
+
if self._match(TokenType.PAREN_START):
|
|
1198
|
+
if self._check(TokenType.AT):
|
|
1199
|
+
self._advance() # skip @
|
|
1200
|
+
is_global = True
|
|
1201
|
+
self._expect(TokenType.PAREN_END)
|
|
1202
|
+
|
|
1203
|
+
node = ASTNode('struct', value={'name': name, 'global': is_global}, children=[])
|
|
1204
|
+
self._expect(TokenType.BLOCK_START)
|
|
1205
|
+
|
|
1206
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1207
|
+
if self._match_keyword('define'):
|
|
1208
|
+
node.children.append(self._parse_define())
|
|
1209
|
+
elif self._check(TokenType.IDENTIFIER):
|
|
1210
|
+
# Look ahead to determine what kind of statement this is
|
|
1211
|
+
saved_pos = self.pos
|
|
1212
|
+
var_name = self._advance().value
|
|
1213
|
+
|
|
1214
|
+
if self._match(TokenType.INJECT_LEFT):
|
|
1215
|
+
# Injection: var <== expr
|
|
1216
|
+
value = self._parse_expression()
|
|
1217
|
+
node.children.append(ASTNode('injection', value={'name': var_name, 'source': value}))
|
|
1218
|
+
self._match(TokenType.SEMICOLON)
|
|
1219
|
+
elif self._match(TokenType.EQUALS):
|
|
1220
|
+
# Assignment: var = expr
|
|
1221
|
+
value = self._parse_expression()
|
|
1222
|
+
node.children.append(ASTNode('assignment', value={'name': var_name, 'value': value}))
|
|
1223
|
+
self._match(TokenType.SEMICOLON)
|
|
1224
|
+
elif self._check(TokenType.PAREN_START):
|
|
1225
|
+
# Function call: func(args)
|
|
1226
|
+
self.pos = saved_pos # Go back to parse full expression
|
|
1227
|
+
stmt = self._parse_expression_statement()
|
|
1228
|
+
if stmt:
|
|
1229
|
+
node.children.append(stmt)
|
|
1230
|
+
elif self._match(TokenType.DOT):
|
|
1231
|
+
# Method call: obj.method(args)
|
|
1232
|
+
self.pos = saved_pos # Go back to parse full expression
|
|
1233
|
+
stmt = self._parse_expression_statement()
|
|
1234
|
+
if stmt:
|
|
1235
|
+
node.children.append(stmt)
|
|
1236
|
+
else:
|
|
1237
|
+
self._match(TokenType.SEMICOLON)
|
|
1238
|
+
elif self._check(TokenType.AT):
|
|
1239
|
+
# Module reference statement
|
|
1240
|
+
stmt = self._parse_expression_statement()
|
|
1241
|
+
if stmt:
|
|
1242
|
+
node.children.append(stmt)
|
|
1243
|
+
else:
|
|
1244
|
+
self._advance()
|
|
1245
|
+
|
|
1246
|
+
self._expect(TokenType.BLOCK_END)
|
|
1247
|
+
return node
|
|
1248
|
+
|
|
1249
|
+
def _parse_class(self) -> ASTNode:
|
|
1250
|
+
"""Parse class declaration with members and methods.
|
|
1251
|
+
|
|
1252
|
+
Syntax:
|
|
1253
|
+
class ClassName {
|
|
1254
|
+
string name;
|
|
1255
|
+
int age;
|
|
1256
|
+
|
|
1257
|
+
void ClassName(string n) { this->name = n; }
|
|
1258
|
+
|
|
1259
|
+
void sayHello() {
|
|
1260
|
+
printl("Hello " + this->name);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
"""
|
|
1264
|
+
class_name = self._advance().value
|
|
1265
|
+
|
|
1266
|
+
node = ASTNode('class', value={'name': class_name}, children=[])
|
|
1267
|
+
self._expect(TokenType.BLOCK_START)
|
|
1268
|
+
|
|
1269
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1270
|
+
# Check for typed function (method) declaration
|
|
1271
|
+
if self._looks_like_function_declaration():
|
|
1272
|
+
method = self._parse_typed_function()
|
|
1273
|
+
method_info = method.value
|
|
1274
|
+
method_name = method_info.get('name')
|
|
1275
|
+
|
|
1276
|
+
# Mark constructor (same name as class or __init__)
|
|
1277
|
+
if method_name == class_name or method_name == '__init__':
|
|
1278
|
+
method.value['is_constructor'] = True
|
|
1279
|
+
|
|
1280
|
+
node.children.append(method)
|
|
1281
|
+
|
|
1282
|
+
# Check for typed member variable declaration
|
|
1283
|
+
elif self._looks_like_typed_variable():
|
|
1284
|
+
member = self._parse_typed_variable()
|
|
1285
|
+
if member:
|
|
1286
|
+
# Mark as class member
|
|
1287
|
+
member.value['is_member'] = True
|
|
1288
|
+
node.children.append(member)
|
|
1289
|
+
|
|
1290
|
+
# Check for define-style method
|
|
1291
|
+
elif self._match_keyword('define'):
|
|
1292
|
+
method = self._parse_define()
|
|
1293
|
+
node.children.append(method)
|
|
1294
|
+
|
|
1295
|
+
else:
|
|
1296
|
+
self._advance()
|
|
1297
|
+
|
|
1298
|
+
self._expect(TokenType.BLOCK_END)
|
|
1299
|
+
return node
|
|
1300
|
+
|
|
1301
|
+
def _parse_define(self) -> ASTNode:
|
|
1302
|
+
name = self._advance().value
|
|
1303
|
+
params = []
|
|
1304
|
+
|
|
1305
|
+
if self._match(TokenType.PAREN_START):
|
|
1306
|
+
while not self._check(TokenType.PAREN_END):
|
|
1307
|
+
param_info = {}
|
|
1308
|
+
# Handle 'open' keyword for open parameters
|
|
1309
|
+
if self._match_keyword('open'):
|
|
1310
|
+
param_info['open'] = True
|
|
1311
|
+
# Handle type annotations (e.g., string, int, dynamic, etc.)
|
|
1312
|
+
if self._check(TokenType.KEYWORD):
|
|
1313
|
+
param_info['type'] = self._advance().value
|
|
1314
|
+
# Handle reference operator &
|
|
1315
|
+
if self._match(TokenType.AMPERSAND):
|
|
1316
|
+
param_info['ref'] = True
|
|
1317
|
+
# Get parameter name
|
|
1318
|
+
if self._check(TokenType.IDENTIFIER):
|
|
1319
|
+
param_name = self._advance().value
|
|
1320
|
+
if param_info:
|
|
1321
|
+
params.append({'name': param_name, **param_info})
|
|
1322
|
+
else:
|
|
1323
|
+
params.append(param_name)
|
|
1324
|
+
self._match(TokenType.COMMA)
|
|
1325
|
+
elif self._check(TokenType.KEYWORD):
|
|
1326
|
+
# Parameter name could be a keyword like 'Params'
|
|
1327
|
+
param_name = self._advance().value
|
|
1328
|
+
if param_info:
|
|
1329
|
+
params.append({'name': param_name, **param_info})
|
|
1330
|
+
else:
|
|
1331
|
+
params.append(param_name)
|
|
1332
|
+
self._match(TokenType.COMMA)
|
|
1333
|
+
else:
|
|
1334
|
+
break
|
|
1335
|
+
self._expect(TokenType.PAREN_END)
|
|
1336
|
+
|
|
1337
|
+
node = ASTNode('function', value={'name': name, 'params': params}, children=[])
|
|
1338
|
+
self._expect(TokenType.BLOCK_START)
|
|
1339
|
+
|
|
1340
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1341
|
+
stmt = self._parse_statement()
|
|
1342
|
+
if stmt:
|
|
1343
|
+
node.children.append(stmt)
|
|
1344
|
+
|
|
1345
|
+
self._expect(TokenType.BLOCK_END)
|
|
1346
|
+
return node
|
|
1347
|
+
|
|
1348
|
+
def _parse_statement(self) -> Optional[ASTNode]:
|
|
1349
|
+
if self._match_keyword('if'):
|
|
1350
|
+
return self._parse_if()
|
|
1351
|
+
elif self._match_keyword('while'):
|
|
1352
|
+
return self._parse_while()
|
|
1353
|
+
elif self._match_keyword('for'):
|
|
1354
|
+
return self._parse_for()
|
|
1355
|
+
elif self._match_keyword('foreach'):
|
|
1356
|
+
return self._parse_foreach()
|
|
1357
|
+
elif self._match_keyword('switch'):
|
|
1358
|
+
return self._parse_switch()
|
|
1359
|
+
elif self._match_keyword('return'):
|
|
1360
|
+
return self._parse_return()
|
|
1361
|
+
elif self._match_keyword('break'):
|
|
1362
|
+
self._match(TokenType.SEMICOLON)
|
|
1363
|
+
return ASTNode('break')
|
|
1364
|
+
elif self._match_keyword('continue'):
|
|
1365
|
+
self._match(TokenType.SEMICOLON)
|
|
1366
|
+
return ASTNode('continue')
|
|
1367
|
+
elif self._match_keyword('try'):
|
|
1368
|
+
return self._parse_try()
|
|
1369
|
+
elif self._match_keyword('await'):
|
|
1370
|
+
return self._parse_await()
|
|
1371
|
+
elif self._match_keyword('define'):
|
|
1372
|
+
# Nested define function
|
|
1373
|
+
return self._parse_define()
|
|
1374
|
+
elif self._looks_like_typed_variable():
|
|
1375
|
+
# Typed variable declaration (e.g., stack<string> myStack;)
|
|
1376
|
+
return self._parse_typed_variable()
|
|
1377
|
+
elif self._looks_like_function_declaration():
|
|
1378
|
+
# Nested typed function (e.g., void Level2() { ... })
|
|
1379
|
+
return self._parse_typed_function()
|
|
1380
|
+
elif (self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or
|
|
1381
|
+
self._check(TokenType.CAPTURED_REF) or self._check(TokenType.SHARED_REF) or
|
|
1382
|
+
self._check(TokenType.GLOBAL_REF) or self._check(TokenType.SELF_REF) or
|
|
1383
|
+
(self._check(TokenType.KEYWORD) and self._current().value in ('this', 'new'))):
|
|
1384
|
+
return self._parse_expression_statement()
|
|
1385
|
+
else:
|
|
1386
|
+
self._advance()
|
|
1387
|
+
return None
|
|
1388
|
+
|
|
1389
|
+
def _parse_if(self) -> ASTNode:
|
|
1390
|
+
"""Parse if statement with support for else if AND elif syntax."""
|
|
1391
|
+
self._expect(TokenType.PAREN_START)
|
|
1392
|
+
condition = self._parse_expression()
|
|
1393
|
+
self._expect(TokenType.PAREN_END)
|
|
1394
|
+
|
|
1395
|
+
node = ASTNode('if', value={'condition': condition}, children=[])
|
|
1396
|
+
|
|
1397
|
+
self._expect(TokenType.BLOCK_START)
|
|
1398
|
+
then_block = ASTNode('then', children=[])
|
|
1399
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1400
|
+
stmt = self._parse_statement()
|
|
1401
|
+
if stmt:
|
|
1402
|
+
then_block.children.append(stmt)
|
|
1403
|
+
self._expect(TokenType.BLOCK_END)
|
|
1404
|
+
node.children.append(then_block)
|
|
1405
|
+
|
|
1406
|
+
# Support both 'else if' AND 'elif' syntax
|
|
1407
|
+
if self._match_keyword('elif'):
|
|
1408
|
+
# elif is shorthand for else if
|
|
1409
|
+
else_block = ASTNode('else', children=[])
|
|
1410
|
+
else_block.children.append(self._parse_if())
|
|
1411
|
+
node.children.append(else_block)
|
|
1412
|
+
elif self._match_keyword('else'):
|
|
1413
|
+
else_block = ASTNode('else', children=[])
|
|
1414
|
+
if self._match_keyword('if'):
|
|
1415
|
+
else_block.children.append(self._parse_if())
|
|
1416
|
+
else:
|
|
1417
|
+
self._expect(TokenType.BLOCK_START)
|
|
1418
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1419
|
+
stmt = self._parse_statement()
|
|
1420
|
+
if stmt:
|
|
1421
|
+
else_block.children.append(stmt)
|
|
1422
|
+
self._expect(TokenType.BLOCK_END)
|
|
1423
|
+
node.children.append(else_block)
|
|
1424
|
+
|
|
1425
|
+
return node
|
|
1426
|
+
|
|
1427
|
+
def _parse_while(self) -> ASTNode:
|
|
1428
|
+
self._expect(TokenType.PAREN_START)
|
|
1429
|
+
condition = self._parse_expression()
|
|
1430
|
+
self._expect(TokenType.PAREN_END)
|
|
1431
|
+
|
|
1432
|
+
node = ASTNode('while', value={'condition': condition}, children=[])
|
|
1433
|
+
self._expect(TokenType.BLOCK_START)
|
|
1434
|
+
|
|
1435
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1436
|
+
stmt = self._parse_statement()
|
|
1437
|
+
if stmt:
|
|
1438
|
+
node.children.append(stmt)
|
|
1439
|
+
|
|
1440
|
+
self._expect(TokenType.BLOCK_END)
|
|
1441
|
+
return node
|
|
1442
|
+
|
|
1443
|
+
def _parse_for(self) -> ASTNode:
|
|
1444
|
+
"""Parse for loop - supports both syntaxes:
|
|
1445
|
+
|
|
1446
|
+
Python-style: for (i in range(0, n)) { }
|
|
1447
|
+
C-style: for (int i = 0; i < n; i = i + 1) { }
|
|
1448
|
+
for (i = 0; i < n; i++) { }
|
|
1449
|
+
"""
|
|
1450
|
+
self._expect(TokenType.PAREN_START)
|
|
1451
|
+
|
|
1452
|
+
# Detect C-style by checking for semicolons in the for header
|
|
1453
|
+
# Look ahead without consuming tokens
|
|
1454
|
+
is_c_style = self._detect_c_style_for()
|
|
1455
|
+
|
|
1456
|
+
if is_c_style:
|
|
1457
|
+
# C-style: for (init; condition; update) { }
|
|
1458
|
+
return self._parse_c_style_for()
|
|
1459
|
+
else:
|
|
1460
|
+
# Python-style: for (var in range(start, end)) { }
|
|
1461
|
+
return self._parse_python_style_for()
|
|
1462
|
+
|
|
1463
|
+
def _detect_c_style_for(self) -> bool:
|
|
1464
|
+
"""Detect if this is a C-style for loop by looking for semicolons."""
|
|
1465
|
+
# Scan the tokens list directly without modifying self.pos
|
|
1466
|
+
pos = self.pos
|
|
1467
|
+
paren_depth = 1
|
|
1468
|
+
|
|
1469
|
+
while pos < len(self.tokens) and paren_depth > 0:
|
|
1470
|
+
token = self.tokens[pos]
|
|
1471
|
+
if token.type == TokenType.PAREN_START:
|
|
1472
|
+
paren_depth += 1
|
|
1473
|
+
elif token.type == TokenType.PAREN_END:
|
|
1474
|
+
paren_depth -= 1
|
|
1475
|
+
elif token.type == TokenType.SEMICOLON and paren_depth == 1:
|
|
1476
|
+
# Found semicolon at top level - C-style
|
|
1477
|
+
return True
|
|
1478
|
+
elif token.type == TokenType.KEYWORD and token.value == 'in':
|
|
1479
|
+
# Found 'in' keyword - Python-style
|
|
1480
|
+
return False
|
|
1481
|
+
pos += 1
|
|
1482
|
+
|
|
1483
|
+
return False # Default to Python-style
|
|
1484
|
+
|
|
1485
|
+
def _parse_c_style_for(self) -> ASTNode:
|
|
1486
|
+
"""Parse C-style for loop: for (init; condition; update) { }"""
|
|
1487
|
+
# Parse init statement
|
|
1488
|
+
init = None
|
|
1489
|
+
if not self._check(TokenType.SEMICOLON):
|
|
1490
|
+
# Check if it's a typed declaration: int i = 0
|
|
1491
|
+
if self._check(TokenType.KEYWORD) and self._peek().value in ('int', 'float', 'string', 'bool', 'dynamic'):
|
|
1492
|
+
type_name = self._advance().value
|
|
1493
|
+
var_name = self._advance().value
|
|
1494
|
+
self._expect(TokenType.EQUALS)
|
|
1495
|
+
value = self._parse_expression()
|
|
1496
|
+
init = ASTNode('c_for_init', value={
|
|
1497
|
+
'type': type_name,
|
|
1498
|
+
'var': var_name,
|
|
1499
|
+
'value': value
|
|
1500
|
+
})
|
|
1501
|
+
else:
|
|
1502
|
+
# Simple assignment: i = 0
|
|
1503
|
+
var_name = self._advance().value
|
|
1504
|
+
self._expect(TokenType.EQUALS)
|
|
1505
|
+
value = self._parse_expression()
|
|
1506
|
+
init = ASTNode('c_for_init', value={
|
|
1507
|
+
'type': None,
|
|
1508
|
+
'var': var_name,
|
|
1509
|
+
'value': value
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
self._expect(TokenType.SEMICOLON)
|
|
1513
|
+
|
|
1514
|
+
# Parse condition
|
|
1515
|
+
condition = None
|
|
1516
|
+
if not self._check(TokenType.SEMICOLON):
|
|
1517
|
+
condition = self._parse_expression()
|
|
1518
|
+
|
|
1519
|
+
self._expect(TokenType.SEMICOLON)
|
|
1520
|
+
|
|
1521
|
+
# Parse update statement
|
|
1522
|
+
update = None
|
|
1523
|
+
if not self._check(TokenType.PAREN_END):
|
|
1524
|
+
# Could be: i = i + 1, i++, ++i, i += 1
|
|
1525
|
+
update = self._parse_c_for_update()
|
|
1526
|
+
|
|
1527
|
+
self._expect(TokenType.PAREN_END)
|
|
1528
|
+
|
|
1529
|
+
node = ASTNode('c_for', value={
|
|
1530
|
+
'init': init,
|
|
1531
|
+
'condition': condition,
|
|
1532
|
+
'update': update
|
|
1533
|
+
}, children=[])
|
|
1534
|
+
|
|
1535
|
+
self._expect(TokenType.BLOCK_START)
|
|
1536
|
+
|
|
1537
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1538
|
+
stmt = self._parse_statement()
|
|
1539
|
+
if stmt:
|
|
1540
|
+
node.children.append(stmt)
|
|
1541
|
+
|
|
1542
|
+
self._expect(TokenType.BLOCK_END)
|
|
1543
|
+
return node
|
|
1544
|
+
|
|
1545
|
+
def _parse_c_for_update(self) -> ASTNode:
|
|
1546
|
+
"""Parse the update part of a C-style for loop.
|
|
1547
|
+
|
|
1548
|
+
Supports: i = i + 1, i++, ++i, i += 1, i -= 1
|
|
1549
|
+
"""
|
|
1550
|
+
# Check for prefix increment/decrement: ++i or --i
|
|
1551
|
+
if self._check(TokenType.PLUS) or self._check(TokenType.MINUS):
|
|
1552
|
+
op_token = self._advance()
|
|
1553
|
+
# Check for double operator (++ or --)
|
|
1554
|
+
if self._check(op_token.type):
|
|
1555
|
+
self._advance()
|
|
1556
|
+
var_name = self._advance().value
|
|
1557
|
+
op = 'increment' if op_token.type == TokenType.PLUS else 'decrement'
|
|
1558
|
+
return ASTNode('c_for_update', value={'var': var_name, 'op': op})
|
|
1559
|
+
|
|
1560
|
+
# Regular variable assignment or postfix
|
|
1561
|
+
var_name = self._advance().value
|
|
1562
|
+
|
|
1563
|
+
# Check for postfix increment/decrement: i++ or i--
|
|
1564
|
+
if self._check(TokenType.PLUS):
|
|
1565
|
+
self._advance()
|
|
1566
|
+
if self._check(TokenType.PLUS):
|
|
1567
|
+
self._advance()
|
|
1568
|
+
return ASTNode('c_for_update', value={'var': var_name, 'op': 'increment'})
|
|
1569
|
+
else:
|
|
1570
|
+
# i += value
|
|
1571
|
+
if self._check(TokenType.EQUALS):
|
|
1572
|
+
self._advance()
|
|
1573
|
+
value = self._parse_expression()
|
|
1574
|
+
return ASTNode('c_for_update', value={'var': var_name, 'op': 'add', 'value': value})
|
|
1575
|
+
elif self._check(TokenType.MINUS):
|
|
1576
|
+
self._advance()
|
|
1577
|
+
if self._check(TokenType.MINUS):
|
|
1578
|
+
self._advance()
|
|
1579
|
+
return ASTNode('c_for_update', value={'var': var_name, 'op': 'decrement'})
|
|
1580
|
+
else:
|
|
1581
|
+
# i -= value
|
|
1582
|
+
if self._check(TokenType.EQUALS):
|
|
1583
|
+
self._advance()
|
|
1584
|
+
value = self._parse_expression()
|
|
1585
|
+
return ASTNode('c_for_update', value={'var': var_name, 'op': 'subtract', 'value': value})
|
|
1586
|
+
|
|
1587
|
+
# Regular assignment: i = expression
|
|
1588
|
+
if self._check(TokenType.EQUALS):
|
|
1589
|
+
self._advance()
|
|
1590
|
+
value = self._parse_expression()
|
|
1591
|
+
return ASTNode('c_for_update', value={'var': var_name, 'op': 'assign', 'value': value})
|
|
1592
|
+
|
|
1593
|
+
# Just the variable (shouldn't happen but handle it)
|
|
1594
|
+
return ASTNode('c_for_update', value={'var': var_name, 'op': 'none'})
|
|
1595
|
+
|
|
1596
|
+
def _parse_python_style_for(self) -> ASTNode:
|
|
1597
|
+
"""Parse Python-style for loop: for (i in range(...)) { }
|
|
1598
|
+
|
|
1599
|
+
Supports:
|
|
1600
|
+
for (i in range(n)) { } - 0 to n-1
|
|
1601
|
+
for (i in range(start, end)) { } - start to end-1
|
|
1602
|
+
for (i in range(start, end, step)) { }
|
|
1603
|
+
"""
|
|
1604
|
+
var_name = self._advance().value
|
|
1605
|
+
self._expect(TokenType.KEYWORD) # 'in'
|
|
1606
|
+
|
|
1607
|
+
# 'range' can be keyword or identifier
|
|
1608
|
+
if self._check(TokenType.KEYWORD) and self._peek().value == 'range':
|
|
1609
|
+
self._advance() # consume 'range' keyword
|
|
1610
|
+
elif self._check(TokenType.IDENTIFIER) and self._peek().value == 'range':
|
|
1611
|
+
self._advance() # consume 'range' identifier
|
|
1612
|
+
else:
|
|
1613
|
+
self.error(f"Expected 'range', got {self._peek().value}")
|
|
1614
|
+
|
|
1615
|
+
self._expect(TokenType.PAREN_START)
|
|
1616
|
+
first_arg = self._parse_expression()
|
|
1617
|
+
|
|
1618
|
+
# Check if there are more arguments
|
|
1619
|
+
start = None
|
|
1620
|
+
end = None
|
|
1621
|
+
step = None
|
|
1622
|
+
|
|
1623
|
+
if self._check(TokenType.COMMA):
|
|
1624
|
+
# range(start, end) or range(start, end, step)
|
|
1625
|
+
self._advance() # consume comma
|
|
1626
|
+
start = first_arg
|
|
1627
|
+
end = self._parse_expression()
|
|
1628
|
+
|
|
1629
|
+
# Optional step parameter
|
|
1630
|
+
if self._check(TokenType.COMMA):
|
|
1631
|
+
self._advance() # consume comma
|
|
1632
|
+
step = self._parse_expression()
|
|
1633
|
+
else:
|
|
1634
|
+
# range(n) - single argument means 0 to n-1
|
|
1635
|
+
start = ASTNode('literal', value={'type': 'int', 'value': 0})
|
|
1636
|
+
end = first_arg
|
|
1637
|
+
|
|
1638
|
+
self._expect(TokenType.PAREN_END)
|
|
1639
|
+
self._expect(TokenType.PAREN_END)
|
|
1640
|
+
|
|
1641
|
+
node = ASTNode('for', value={'var': var_name, 'start': start, 'end': end, 'step': step}, children=[])
|
|
1642
|
+
self._expect(TokenType.BLOCK_START)
|
|
1643
|
+
|
|
1644
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1645
|
+
stmt = self._parse_statement()
|
|
1646
|
+
if stmt:
|
|
1647
|
+
node.children.append(stmt)
|
|
1648
|
+
|
|
1649
|
+
self._expect(TokenType.BLOCK_END)
|
|
1650
|
+
return node
|
|
1651
|
+
|
|
1652
|
+
def _parse_foreach(self) -> ASTNode:
|
|
1653
|
+
"""Parse foreach loop - supports both syntaxes:
|
|
1654
|
+
|
|
1655
|
+
Traditional: foreach (var in iterable) { }
|
|
1656
|
+
New 'as' syntax: foreach iterable as var { }
|
|
1657
|
+
"""
|
|
1658
|
+
# Check if this is the new 'as' syntax or traditional syntax
|
|
1659
|
+
if self._check(TokenType.PAREN_START):
|
|
1660
|
+
# Traditional syntax: foreach (var in iterable) { }
|
|
1661
|
+
self._expect(TokenType.PAREN_START)
|
|
1662
|
+
var_name = self._advance().value
|
|
1663
|
+
self._match_keyword('in')
|
|
1664
|
+
iterable = self._parse_expression()
|
|
1665
|
+
self._expect(TokenType.PAREN_END)
|
|
1666
|
+
else:
|
|
1667
|
+
# NEW: 'as' syntax: foreach iterable as var { }
|
|
1668
|
+
iterable = self._parse_expression()
|
|
1669
|
+
if self._check(TokenType.AS):
|
|
1670
|
+
self._advance() # consume 'as'
|
|
1671
|
+
else:
|
|
1672
|
+
self._match_keyword('as') # try keyword match as fallback
|
|
1673
|
+
var_name = self._advance().value
|
|
1674
|
+
|
|
1675
|
+
node = ASTNode('foreach', value={'var': var_name, 'iterable': iterable}, children=[])
|
|
1676
|
+
self._expect(TokenType.BLOCK_START)
|
|
1677
|
+
|
|
1678
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1679
|
+
stmt = self._parse_statement()
|
|
1680
|
+
if stmt:
|
|
1681
|
+
node.children.append(stmt)
|
|
1682
|
+
|
|
1683
|
+
self._expect(TokenType.BLOCK_END)
|
|
1684
|
+
return node
|
|
1685
|
+
|
|
1686
|
+
def _parse_switch(self) -> ASTNode:
|
|
1687
|
+
self._expect(TokenType.PAREN_START)
|
|
1688
|
+
value = self._parse_expression()
|
|
1689
|
+
self._expect(TokenType.PAREN_END)
|
|
1690
|
+
|
|
1691
|
+
node = ASTNode('switch', value={'value': value}, children=[])
|
|
1692
|
+
self._expect(TokenType.BLOCK_START)
|
|
1693
|
+
|
|
1694
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1695
|
+
if self._match_keyword('case'):
|
|
1696
|
+
case_value = self._parse_expression()
|
|
1697
|
+
self._expect(TokenType.COLON)
|
|
1698
|
+
case_node = ASTNode('case', value={'value': case_value}, children=[])
|
|
1699
|
+
|
|
1700
|
+
while not self._check_keyword('case') and not self._check_keyword('default') and not self._check(TokenType.BLOCK_END):
|
|
1701
|
+
stmt = self._parse_statement()
|
|
1702
|
+
if stmt:
|
|
1703
|
+
case_node.children.append(stmt)
|
|
1704
|
+
if self._check_keyword('break'):
|
|
1705
|
+
break
|
|
1706
|
+
|
|
1707
|
+
node.children.append(case_node)
|
|
1708
|
+
elif self._match_keyword('default'):
|
|
1709
|
+
self._expect(TokenType.COLON)
|
|
1710
|
+
default_node = ASTNode('default', children=[])
|
|
1711
|
+
|
|
1712
|
+
while not self._check(TokenType.BLOCK_END):
|
|
1713
|
+
stmt = self._parse_statement()
|
|
1714
|
+
if stmt:
|
|
1715
|
+
default_node.children.append(stmt)
|
|
1716
|
+
|
|
1717
|
+
node.children.append(default_node)
|
|
1718
|
+
else:
|
|
1719
|
+
self._advance()
|
|
1720
|
+
|
|
1721
|
+
self._expect(TokenType.BLOCK_END)
|
|
1722
|
+
return node
|
|
1723
|
+
|
|
1724
|
+
def _parse_return(self) -> ASTNode:
|
|
1725
|
+
value = None
|
|
1726
|
+
if not self._check(TokenType.SEMICOLON) and not self._check(TokenType.BLOCK_END):
|
|
1727
|
+
value = self._parse_expression()
|
|
1728
|
+
self._match(TokenType.SEMICOLON)
|
|
1729
|
+
return ASTNode('return', value=value)
|
|
1730
|
+
|
|
1731
|
+
def _parse_try(self) -> ASTNode:
|
|
1732
|
+
node = ASTNode('try', children=[])
|
|
1733
|
+
|
|
1734
|
+
try_block = ASTNode('try-block', children=[])
|
|
1735
|
+
self._expect(TokenType.BLOCK_START)
|
|
1736
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1737
|
+
stmt = self._parse_statement()
|
|
1738
|
+
if stmt:
|
|
1739
|
+
try_block.children.append(stmt)
|
|
1740
|
+
self._expect(TokenType.BLOCK_END)
|
|
1741
|
+
node.children.append(try_block)
|
|
1742
|
+
|
|
1743
|
+
if self._match_keyword('catch'):
|
|
1744
|
+
error_var = None
|
|
1745
|
+
if self._match(TokenType.PAREN_START):
|
|
1746
|
+
error_var = self._advance().value
|
|
1747
|
+
self._expect(TokenType.PAREN_END)
|
|
1748
|
+
|
|
1749
|
+
catch_block = ASTNode('catch-block', value={'error_var': error_var}, children=[])
|
|
1750
|
+
self._expect(TokenType.BLOCK_START)
|
|
1751
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1752
|
+
stmt = self._parse_statement()
|
|
1753
|
+
if stmt:
|
|
1754
|
+
catch_block.children.append(stmt)
|
|
1755
|
+
self._expect(TokenType.BLOCK_END)
|
|
1756
|
+
node.children.append(catch_block)
|
|
1757
|
+
|
|
1758
|
+
return node
|
|
1759
|
+
|
|
1760
|
+
def _parse_await(self) -> ASTNode:
|
|
1761
|
+
"""Parse await statement: await expression;"""
|
|
1762
|
+
expr = self._parse_expression()
|
|
1763
|
+
self._match(TokenType.SEMICOLON)
|
|
1764
|
+
return ASTNode('await', value=expr)
|
|
1765
|
+
|
|
1766
|
+
def _parse_action_block(self) -> ASTNode:
|
|
1767
|
+
"""Parse an action block { ... } containing statements for createcmd"""
|
|
1768
|
+
node = ASTNode('action_block', children=[])
|
|
1769
|
+
self._expect(TokenType.BLOCK_START)
|
|
1770
|
+
|
|
1771
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
1772
|
+
# Check for define statements inside action block
|
|
1773
|
+
if self._match_keyword('define'):
|
|
1774
|
+
node.children.append(self._parse_define())
|
|
1775
|
+
# Check for typed function definitions (nested functions)
|
|
1776
|
+
elif self._looks_like_function_declaration():
|
|
1777
|
+
node.children.append(self._parse_typed_function())
|
|
1778
|
+
else:
|
|
1779
|
+
stmt = self._parse_statement()
|
|
1780
|
+
if stmt:
|
|
1781
|
+
node.children.append(stmt)
|
|
1782
|
+
|
|
1783
|
+
self._expect(TokenType.BLOCK_END)
|
|
1784
|
+
return node
|
|
1785
|
+
|
|
1786
|
+
def _parse_injection_filter(self) -> Optional[dict]:
|
|
1787
|
+
"""Parse injection filter: [type::helper=value]"""
|
|
1788
|
+
if not self._match(TokenType.BRACKET_START):
|
|
1789
|
+
return None
|
|
1790
|
+
|
|
1791
|
+
filter_info = {}
|
|
1792
|
+
# Parse type::helper=value patterns
|
|
1793
|
+
while not self._check(TokenType.BRACKET_END) and not self._is_at_end():
|
|
1794
|
+
if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
|
|
1795
|
+
filter_type = self._advance().value
|
|
1796
|
+
if self._match(TokenType.DOUBLE_COLON):
|
|
1797
|
+
helper = self._advance().value
|
|
1798
|
+
if self._match(TokenType.EQUALS):
|
|
1799
|
+
value = self._parse_expression()
|
|
1800
|
+
filter_info[f'{filter_type}::{helper}'] = value
|
|
1801
|
+
else:
|
|
1802
|
+
filter_info[f'{filter_type}::{helper}'] = True
|
|
1803
|
+
else:
|
|
1804
|
+
filter_info['type'] = filter_type
|
|
1805
|
+
elif self._check(TokenType.COMMA):
|
|
1806
|
+
self._advance()
|
|
1807
|
+
else:
|
|
1808
|
+
break
|
|
1809
|
+
|
|
1810
|
+
self._expect(TokenType.BRACKET_END)
|
|
1811
|
+
return filter_info if filter_info else None
|
|
1812
|
+
|
|
1813
|
+
def _parse_expression_statement(self) -> Optional[ASTNode]:
|
|
1814
|
+
expr = self._parse_expression()
|
|
1815
|
+
|
|
1816
|
+
# === BASIC INJECTION: <== (replace target with source) ===
|
|
1817
|
+
if self._match(TokenType.INJECT_LEFT):
|
|
1818
|
+
# Check if this is a createcmd injection with a code block
|
|
1819
|
+
is_createcmd = (
|
|
1820
|
+
expr.type == 'call' and
|
|
1821
|
+
expr.value.get('callee') and
|
|
1822
|
+
expr.value.get('callee').type == 'identifier' and
|
|
1823
|
+
expr.value.get('callee').value == 'createcmd'
|
|
1824
|
+
)
|
|
1825
|
+
|
|
1826
|
+
if is_createcmd and self._check(TokenType.BLOCK_START):
|
|
1827
|
+
action_block = self._parse_action_block()
|
|
1828
|
+
self._match(TokenType.SEMICOLON)
|
|
1829
|
+
return ASTNode('createcmd_inject', value={'command_call': expr, 'action': action_block})
|
|
1830
|
+
else:
|
|
1831
|
+
# Check for injection filter [type::helper=value]
|
|
1832
|
+
filter_info = self._parse_injection_filter()
|
|
1833
|
+
source = self._parse_expression()
|
|
1834
|
+
self._match(TokenType.SEMICOLON)
|
|
1835
|
+
return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'replace', 'filter': filter_info})
|
|
1836
|
+
|
|
1837
|
+
# === PLUS INJECTION: +<== (copy & add to target) ===
|
|
1838
|
+
if self._match(TokenType.INJECT_PLUS_LEFT):
|
|
1839
|
+
filter_info = self._parse_injection_filter()
|
|
1840
|
+
source = self._parse_expression()
|
|
1841
|
+
self._match(TokenType.SEMICOLON)
|
|
1842
|
+
return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'add', 'filter': filter_info})
|
|
1843
|
+
|
|
1844
|
+
# === MINUS INJECTION: -<== or -<==[n] (move & remove from source) ===
|
|
1845
|
+
if self._match(TokenType.INJECT_MINUS_LEFT):
|
|
1846
|
+
# Check for indexed deletion: -<==[n] (only numbers, not filters)
|
|
1847
|
+
remove_index = None
|
|
1848
|
+
if self._check(TokenType.BRACKET_START):
|
|
1849
|
+
# Peek ahead to see if this is an index [n] or a filter [type::helper=...]
|
|
1850
|
+
# Only consume if it's a simple number index
|
|
1851
|
+
saved_pos = self.pos
|
|
1852
|
+
self._advance() # consume [
|
|
1853
|
+
if self._check(TokenType.NUMBER):
|
|
1854
|
+
remove_index = int(self._advance().value)
|
|
1855
|
+
self._expect(TokenType.BRACKET_END)
|
|
1856
|
+
else:
|
|
1857
|
+
# Not a number - restore position for filter parsing
|
|
1858
|
+
self.pos = saved_pos
|
|
1859
|
+
|
|
1860
|
+
filter_info = self._parse_injection_filter()
|
|
1861
|
+
source = self._parse_expression()
|
|
1862
|
+
self._match(TokenType.SEMICOLON)
|
|
1863
|
+
return ASTNode('inject', value={'target': expr, 'source': source, 'mode': 'move', 'filter': filter_info, 'index': remove_index})
|
|
1864
|
+
|
|
1865
|
+
# === CODE INFUSION: <<== (inject code into function) ===
|
|
1866
|
+
if self._match(TokenType.INFUSE_LEFT):
|
|
1867
|
+
if self._check(TokenType.BLOCK_START):
|
|
1868
|
+
code_block = self._parse_action_block()
|
|
1869
|
+
self._match(TokenType.SEMICOLON)
|
|
1870
|
+
return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'replace'})
|
|
1871
|
+
else:
|
|
1872
|
+
source = self._parse_expression()
|
|
1873
|
+
self._match(TokenType.SEMICOLON)
|
|
1874
|
+
return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'replace'})
|
|
1875
|
+
|
|
1876
|
+
# === CODE INFUSION PLUS: +<<== (add code to function) ===
|
|
1877
|
+
if self._match(TokenType.INFUSE_PLUS_LEFT):
|
|
1878
|
+
if self._check(TokenType.BLOCK_START):
|
|
1879
|
+
code_block = self._parse_action_block()
|
|
1880
|
+
self._match(TokenType.SEMICOLON)
|
|
1881
|
+
return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'add'})
|
|
1882
|
+
else:
|
|
1883
|
+
source = self._parse_expression()
|
|
1884
|
+
self._match(TokenType.SEMICOLON)
|
|
1885
|
+
return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'add'})
|
|
1886
|
+
|
|
1887
|
+
# === CODE INFUSION MINUS: -<<== or -<<==[n] (remove code from function) ===
|
|
1888
|
+
if self._match(TokenType.INFUSE_MINUS_LEFT):
|
|
1889
|
+
# Check for indexed deletion: -<<==[n] (only numbers)
|
|
1890
|
+
remove_index = None
|
|
1891
|
+
if self._check(TokenType.BRACKET_START):
|
|
1892
|
+
# Peek ahead to see if this is an index [n] or something else
|
|
1893
|
+
saved_pos = self.pos
|
|
1894
|
+
self._advance() # consume [
|
|
1895
|
+
if self._check(TokenType.NUMBER):
|
|
1896
|
+
remove_index = int(self._advance().value)
|
|
1897
|
+
self._expect(TokenType.BRACKET_END)
|
|
1898
|
+
else:
|
|
1899
|
+
# Not a number - restore position
|
|
1900
|
+
self.pos = saved_pos
|
|
1901
|
+
|
|
1902
|
+
if self._check(TokenType.BLOCK_START):
|
|
1903
|
+
code_block = self._parse_action_block()
|
|
1904
|
+
self._match(TokenType.SEMICOLON)
|
|
1905
|
+
return ASTNode('infuse', value={'target': expr, 'code': code_block, 'mode': 'remove', 'index': remove_index})
|
|
1906
|
+
else:
|
|
1907
|
+
source = self._parse_expression()
|
|
1908
|
+
self._match(TokenType.SEMICOLON)
|
|
1909
|
+
return ASTNode('infuse', value={'target': expr, 'source': source, 'mode': 'remove', 'index': remove_index})
|
|
1910
|
+
|
|
1911
|
+
# === RIGHT-SIDE OPERATORS ===
|
|
1912
|
+
|
|
1913
|
+
# === BASIC RECEIVE: ==> (move source to target) ===
|
|
1914
|
+
if self._match(TokenType.INJECT_RIGHT):
|
|
1915
|
+
filter_info = self._parse_injection_filter()
|
|
1916
|
+
target = self._parse_expression()
|
|
1917
|
+
self._match(TokenType.SEMICOLON)
|
|
1918
|
+
return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'replace', 'filter': filter_info})
|
|
1919
|
+
|
|
1920
|
+
# === PLUS RECEIVE: ==>+ (copy source to target) ===
|
|
1921
|
+
if self._match(TokenType.INJECT_PLUS_RIGHT):
|
|
1922
|
+
filter_info = self._parse_injection_filter()
|
|
1923
|
+
target = self._parse_expression()
|
|
1924
|
+
self._match(TokenType.SEMICOLON)
|
|
1925
|
+
return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'add', 'filter': filter_info})
|
|
1926
|
+
|
|
1927
|
+
# === MINUS RECEIVE: -==> (move & remove from source) ===
|
|
1928
|
+
if self._match(TokenType.INJECT_MINUS_RIGHT):
|
|
1929
|
+
filter_info = self._parse_injection_filter()
|
|
1930
|
+
target = self._parse_expression()
|
|
1931
|
+
self._match(TokenType.SEMICOLON)
|
|
1932
|
+
return ASTNode('receive', value={'source': expr, 'target': target, 'mode': 'move', 'filter': filter_info})
|
|
1933
|
+
|
|
1934
|
+
# === CODE INFUSION RIGHT: ==>> ===
|
|
1935
|
+
if self._match(TokenType.INFUSE_RIGHT):
|
|
1936
|
+
target = self._parse_expression()
|
|
1937
|
+
self._match(TokenType.SEMICOLON)
|
|
1938
|
+
return ASTNode('infuse_right', value={'source': expr, 'target': target, 'mode': 'replace'})
|
|
1939
|
+
|
|
1940
|
+
# === FLOW OPERATORS ===
|
|
1941
|
+
if self._match(TokenType.FLOW_RIGHT):
|
|
1942
|
+
target = self._parse_expression()
|
|
1943
|
+
self._match(TokenType.SEMICOLON)
|
|
1944
|
+
return ASTNode('flow', value={'source': expr, 'target': target})
|
|
1945
|
+
|
|
1946
|
+
if self._match(TokenType.FLOW_LEFT):
|
|
1947
|
+
source = self._parse_expression()
|
|
1948
|
+
self._match(TokenType.SEMICOLON)
|
|
1949
|
+
return ASTNode('flow', value={'source': source, 'target': expr})
|
|
1950
|
+
|
|
1951
|
+
# === BASIC ASSIGNMENT ===
|
|
1952
|
+
if self._match(TokenType.EQUALS):
|
|
1953
|
+
value = self._parse_expression()
|
|
1954
|
+
self._match(TokenType.SEMICOLON)
|
|
1955
|
+
return ASTNode('assignment', value={'target': expr, 'value': value})
|
|
1956
|
+
|
|
1957
|
+
self._match(TokenType.SEMICOLON)
|
|
1958
|
+
return ASTNode('expression', value=expr)
|
|
1959
|
+
|
|
1960
|
+
def _parse_expression(self) -> ASTNode:
|
|
1961
|
+
return self._parse_or()
|
|
1962
|
+
|
|
1963
|
+
def _parse_or(self) -> ASTNode:
|
|
1964
|
+
left = self._parse_and()
|
|
1965
|
+
|
|
1966
|
+
while self._match(TokenType.OR) or self._match_keyword('or'):
|
|
1967
|
+
right = self._parse_and()
|
|
1968
|
+
left = ASTNode('binary', value={'op': 'or', 'left': left, 'right': right})
|
|
1969
|
+
|
|
1970
|
+
return left
|
|
1971
|
+
|
|
1972
|
+
def _parse_and(self) -> ASTNode:
|
|
1973
|
+
left = self._parse_comparison()
|
|
1974
|
+
|
|
1975
|
+
while self._match(TokenType.AND) or self._match_keyword('and'):
|
|
1976
|
+
right = self._parse_comparison()
|
|
1977
|
+
left = ASTNode('binary', value={'op': 'and', 'left': left, 'right': right})
|
|
1978
|
+
|
|
1979
|
+
return left
|
|
1980
|
+
|
|
1981
|
+
def _parse_comparison(self) -> ASTNode:
|
|
1982
|
+
left = self._parse_term()
|
|
1983
|
+
|
|
1984
|
+
while True:
|
|
1985
|
+
if self._match(TokenType.COMPARE_EQ):
|
|
1986
|
+
right = self._parse_term()
|
|
1987
|
+
left = ASTNode('binary', value={'op': '==', 'left': left, 'right': right})
|
|
1988
|
+
elif self._match(TokenType.COMPARE_NE):
|
|
1989
|
+
right = self._parse_term()
|
|
1990
|
+
left = ASTNode('binary', value={'op': '!=', 'left': left, 'right': right})
|
|
1991
|
+
elif self._match(TokenType.COMPARE_LT):
|
|
1992
|
+
right = self._parse_term()
|
|
1993
|
+
left = ASTNode('binary', value={'op': '<', 'left': left, 'right': right})
|
|
1994
|
+
elif self._match(TokenType.COMPARE_GT):
|
|
1995
|
+
right = self._parse_term()
|
|
1996
|
+
left = ASTNode('binary', value={'op': '>', 'left': left, 'right': right})
|
|
1997
|
+
elif self._match(TokenType.COMPARE_LE):
|
|
1998
|
+
right = self._parse_term()
|
|
1999
|
+
left = ASTNode('binary', value={'op': '<=', 'left': left, 'right': right})
|
|
2000
|
+
elif self._match(TokenType.COMPARE_GE):
|
|
2001
|
+
right = self._parse_term()
|
|
2002
|
+
left = ASTNode('binary', value={'op': '>=', 'left': left, 'right': right})
|
|
2003
|
+
else:
|
|
2004
|
+
break
|
|
2005
|
+
|
|
2006
|
+
return left
|
|
2007
|
+
|
|
2008
|
+
def _parse_term(self) -> ASTNode:
|
|
2009
|
+
left = self._parse_factor()
|
|
2010
|
+
|
|
2011
|
+
while True:
|
|
2012
|
+
if self._match(TokenType.PLUS):
|
|
2013
|
+
right = self._parse_factor()
|
|
2014
|
+
left = ASTNode('binary', value={'op': '+', 'left': left, 'right': right})
|
|
2015
|
+
elif self._match(TokenType.MINUS):
|
|
2016
|
+
right = self._parse_factor()
|
|
2017
|
+
left = ASTNode('binary', value={'op': '-', 'left': left, 'right': right})
|
|
2018
|
+
else:
|
|
2019
|
+
break
|
|
2020
|
+
|
|
2021
|
+
return left
|
|
2022
|
+
|
|
2023
|
+
def _parse_factor(self) -> ASTNode:
|
|
2024
|
+
left = self._parse_unary()
|
|
2025
|
+
|
|
2026
|
+
while True:
|
|
2027
|
+
if self._match(TokenType.MULTIPLY):
|
|
2028
|
+
right = self._parse_unary()
|
|
2029
|
+
left = ASTNode('binary', value={'op': '*', 'left': left, 'right': right})
|
|
2030
|
+
elif self._match(TokenType.DIVIDE):
|
|
2031
|
+
right = self._parse_unary()
|
|
2032
|
+
left = ASTNode('binary', value={'op': '/', 'left': left, 'right': right})
|
|
2033
|
+
elif self._match(TokenType.MODULO):
|
|
2034
|
+
right = self._parse_unary()
|
|
2035
|
+
left = ASTNode('binary', value={'op': '%', 'left': left, 'right': right})
|
|
2036
|
+
else:
|
|
2037
|
+
break
|
|
2038
|
+
|
|
2039
|
+
return left
|
|
2040
|
+
|
|
2041
|
+
def _parse_unary(self) -> ASTNode:
|
|
2042
|
+
if self._match(TokenType.NOT) or self._match_keyword('not'):
|
|
2043
|
+
operand = self._parse_unary()
|
|
2044
|
+
return ASTNode('unary', value={'op': 'not', 'operand': operand})
|
|
2045
|
+
if self._match(TokenType.MINUS):
|
|
2046
|
+
operand = self._parse_unary()
|
|
2047
|
+
return ASTNode('unary', value={'op': '-', 'operand': operand})
|
|
2048
|
+
if self._match(TokenType.AMPERSAND):
|
|
2049
|
+
# Reference operator: &variable or &@module
|
|
2050
|
+
operand = self._parse_unary()
|
|
2051
|
+
return ASTNode('reference', value=operand)
|
|
2052
|
+
|
|
2053
|
+
return self._parse_primary()
|
|
2054
|
+
|
|
2055
|
+
def _parse_primary(self) -> ASTNode:
|
|
2056
|
+
# Handle 'this->' member access
|
|
2057
|
+
if self._check(TokenType.KEYWORD) and self._current().value == 'this':
|
|
2058
|
+
self._advance() # consume 'this'
|
|
2059
|
+
if self._match(TokenType.FLOW_RIGHT): # ->
|
|
2060
|
+
member = self._advance().value
|
|
2061
|
+
node = ASTNode('this_access', value={'member': member})
|
|
2062
|
+
# Continue to check for calls, member access, indexing
|
|
2063
|
+
while True:
|
|
2064
|
+
if self._match(TokenType.PAREN_START):
|
|
2065
|
+
# Method call: this->method()
|
|
2066
|
+
args, kwargs = self._parse_call_arguments()
|
|
2067
|
+
self._expect(TokenType.PAREN_END)
|
|
2068
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2069
|
+
elif self._match(TokenType.DOT):
|
|
2070
|
+
# Chained access: this->obj.method
|
|
2071
|
+
member = self._advance().value
|
|
2072
|
+
node = ASTNode('member_access', value={'object': node, 'member': member})
|
|
2073
|
+
elif self._match(TokenType.BRACKET_START):
|
|
2074
|
+
# Index access: this->arr[0]
|
|
2075
|
+
index = self._parse_expression()
|
|
2076
|
+
self._expect(TokenType.BRACKET_END)
|
|
2077
|
+
node = ASTNode('index_access', value={'object': node, 'index': index})
|
|
2078
|
+
elif self._match(TokenType.FLOW_RIGHT):
|
|
2079
|
+
# Chained this->a->b style access
|
|
2080
|
+
member = self._advance().value
|
|
2081
|
+
node = ASTNode('this_access', value={'member': member, 'object': node})
|
|
2082
|
+
else:
|
|
2083
|
+
break
|
|
2084
|
+
return node
|
|
2085
|
+
else:
|
|
2086
|
+
# Just 'this' keyword alone - return as identifier for now
|
|
2087
|
+
return ASTNode('identifier', value='this')
|
|
2088
|
+
|
|
2089
|
+
# Handle 'new ClassName(args)' instantiation
|
|
2090
|
+
if self._check(TokenType.KEYWORD) and self._current().value == 'new':
|
|
2091
|
+
self._advance() # consume 'new'
|
|
2092
|
+
class_name = self._advance().value # get class name
|
|
2093
|
+
args = []
|
|
2094
|
+
kwargs = {}
|
|
2095
|
+
if self._match(TokenType.PAREN_START):
|
|
2096
|
+
args, kwargs = self._parse_call_arguments()
|
|
2097
|
+
self._expect(TokenType.PAREN_END)
|
|
2098
|
+
node = ASTNode('new', value={'class': class_name, 'args': args, 'kwargs': kwargs})
|
|
2099
|
+
# Continue to check for member access, calls on the new object
|
|
2100
|
+
while True:
|
|
2101
|
+
if self._match(TokenType.DOT):
|
|
2102
|
+
member = self._advance().value
|
|
2103
|
+
node = ASTNode('member_access', value={'object': node, 'member': member})
|
|
2104
|
+
if self._match(TokenType.PAREN_START):
|
|
2105
|
+
args, kwargs = self._parse_call_arguments()
|
|
2106
|
+
self._expect(TokenType.PAREN_END)
|
|
2107
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2108
|
+
elif self._match(TokenType.BRACKET_START):
|
|
2109
|
+
index = self._parse_expression()
|
|
2110
|
+
self._expect(TokenType.BRACKET_END)
|
|
2111
|
+
node = ASTNode('index_access', value={'object': node, 'index': index})
|
|
2112
|
+
else:
|
|
2113
|
+
break
|
|
2114
|
+
return node
|
|
2115
|
+
|
|
2116
|
+
if self._match(TokenType.AT):
|
|
2117
|
+
node = self._parse_module_reference()
|
|
2118
|
+
# Continue to check for calls, indexing, member access on module refs
|
|
2119
|
+
while True:
|
|
2120
|
+
if self._match(TokenType.PAREN_START):
|
|
2121
|
+
# Function call on module ref: @Module.method() - with kwargs support
|
|
2122
|
+
args, kwargs = self._parse_call_arguments()
|
|
2123
|
+
self._expect(TokenType.PAREN_END)
|
|
2124
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2125
|
+
elif self._match(TokenType.DOT):
|
|
2126
|
+
# Member access: @Module.property
|
|
2127
|
+
member = self._advance().value
|
|
2128
|
+
node = ASTNode('member_access', value={'object': node, 'member': member})
|
|
2129
|
+
elif self._match(TokenType.BRACKET_START):
|
|
2130
|
+
# Index access: @Module[index]
|
|
2131
|
+
index = self._parse_expression()
|
|
2132
|
+
self._expect(TokenType.BRACKET_END)
|
|
2133
|
+
node = ASTNode('index_access', value={'object': node, 'index': index})
|
|
2134
|
+
else:
|
|
2135
|
+
break
|
|
2136
|
+
return node
|
|
2137
|
+
|
|
2138
|
+
if self._check(TokenType.SELF_REF):
|
|
2139
|
+
# s@<name> self-reference to global struct
|
|
2140
|
+
token = self._advance()
|
|
2141
|
+
node = ASTNode('self_ref', value=token.value, line=token.line, column=token.column)
|
|
2142
|
+
# Check for function call: s@Backend.Loop.Start() - with kwargs support
|
|
2143
|
+
if self._match(TokenType.PAREN_START):
|
|
2144
|
+
args, kwargs = self._parse_call_arguments()
|
|
2145
|
+
self._expect(TokenType.PAREN_END)
|
|
2146
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2147
|
+
return node
|
|
2148
|
+
|
|
2149
|
+
if self._check(TokenType.GLOBAL_REF):
|
|
2150
|
+
# r@<name> global variable reference/declaration
|
|
2151
|
+
token = self._advance()
|
|
2152
|
+
node = ASTNode('global_ref', value=token.value, line=token.line, column=token.column)
|
|
2153
|
+
# Check for member access, calls, indexing - with kwargs support
|
|
2154
|
+
while True:
|
|
2155
|
+
if self._match(TokenType.PAREN_START):
|
|
2156
|
+
args, kwargs = self._parse_call_arguments()
|
|
2157
|
+
self._expect(TokenType.PAREN_END)
|
|
2158
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2159
|
+
elif self._match(TokenType.DOT):
|
|
2160
|
+
member = self._advance().value
|
|
2161
|
+
node = ASTNode('member_access', value={'object': node, 'member': member})
|
|
2162
|
+
elif self._match(TokenType.BRACKET_START):
|
|
2163
|
+
index = self._parse_expression()
|
|
2164
|
+
self._expect(TokenType.BRACKET_END)
|
|
2165
|
+
node = ASTNode('index_access', value={'object': node, 'index': index})
|
|
2166
|
+
else:
|
|
2167
|
+
break
|
|
2168
|
+
return node
|
|
2169
|
+
|
|
2170
|
+
if self._check(TokenType.SHARED_REF):
|
|
2171
|
+
# $<name> shared object reference
|
|
2172
|
+
token = self._advance()
|
|
2173
|
+
node = ASTNode('shared_ref', value=token.value, line=token.line, column=token.column)
|
|
2174
|
+
# Check for member access, calls, indexing - with kwargs support
|
|
2175
|
+
while True:
|
|
2176
|
+
if self._match(TokenType.PAREN_START):
|
|
2177
|
+
args, kwargs = self._parse_call_arguments()
|
|
2178
|
+
self._expect(TokenType.PAREN_END)
|
|
2179
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2180
|
+
elif self._match(TokenType.DOT):
|
|
2181
|
+
member = self._advance().value
|
|
2182
|
+
node = ASTNode('member_access', value={'object': node, 'member': member})
|
|
2183
|
+
elif self._match(TokenType.BRACKET_START):
|
|
2184
|
+
index = self._parse_expression()
|
|
2185
|
+
self._expect(TokenType.BRACKET_END)
|
|
2186
|
+
node = ASTNode('index_access', value={'object': node, 'index': index})
|
|
2187
|
+
else:
|
|
2188
|
+
break
|
|
2189
|
+
return node
|
|
2190
|
+
|
|
2191
|
+
if self._check(TokenType.CAPTURED_REF):
|
|
2192
|
+
# %<name> captured reference (captures value at infusion registration time)
|
|
2193
|
+
token = self._advance()
|
|
2194
|
+
node = ASTNode('captured_ref', value=token.value, line=token.line, column=token.column)
|
|
2195
|
+
# Check for member access, calls, indexing - with kwargs support
|
|
2196
|
+
while True:
|
|
2197
|
+
if self._match(TokenType.PAREN_START):
|
|
2198
|
+
args, kwargs = self._parse_call_arguments()
|
|
2199
|
+
self._expect(TokenType.PAREN_END)
|
|
2200
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2201
|
+
elif self._match(TokenType.DOT):
|
|
2202
|
+
member = self._advance().value
|
|
2203
|
+
node = ASTNode('member_access', value={'object': node, 'member': member})
|
|
2204
|
+
elif self._match(TokenType.BRACKET_START):
|
|
2205
|
+
index = self._parse_expression()
|
|
2206
|
+
self._expect(TokenType.BRACKET_END)
|
|
2207
|
+
node = ASTNode('index_access', value={'object': node, 'index': index})
|
|
2208
|
+
else:
|
|
2209
|
+
break
|
|
2210
|
+
return node
|
|
2211
|
+
|
|
2212
|
+
if self._check(TokenType.NUMBER):
|
|
2213
|
+
return ASTNode('literal', value=self._advance().value)
|
|
2214
|
+
|
|
2215
|
+
if self._check(TokenType.STRING):
|
|
2216
|
+
return ASTNode('literal', value=self._advance().value)
|
|
2217
|
+
|
|
2218
|
+
if self._check(TokenType.BOOLEAN):
|
|
2219
|
+
return ASTNode('literal', value=self._advance().value)
|
|
2220
|
+
|
|
2221
|
+
if self._check(TokenType.NULL):
|
|
2222
|
+
self._advance()
|
|
2223
|
+
return ASTNode('literal', value=None)
|
|
2224
|
+
|
|
2225
|
+
# NEW: Type literals (list, dict) - create empty instances
|
|
2226
|
+
if self._check(TokenType.TYPE_LITERAL):
|
|
2227
|
+
type_name = self._advance().value
|
|
2228
|
+
return ASTNode('type_literal', value=type_name)
|
|
2229
|
+
|
|
2230
|
+
if self._match(TokenType.PAREN_START):
|
|
2231
|
+
expr = self._parse_expression()
|
|
2232
|
+
self._expect(TokenType.PAREN_END)
|
|
2233
|
+
return expr
|
|
2234
|
+
|
|
2235
|
+
if self._match(TokenType.BLOCK_START):
|
|
2236
|
+
# Distinguish between object literal { key = value } and action block { expr; }
|
|
2237
|
+
# Object literal: starts with IDENTIFIER = or STRING =
|
|
2238
|
+
# Action block: starts with expression (captured_ref, call, literal, etc.)
|
|
2239
|
+
if self._is_object_literal():
|
|
2240
|
+
return self._parse_object()
|
|
2241
|
+
else:
|
|
2242
|
+
return self._parse_action_block_expression()
|
|
2243
|
+
|
|
2244
|
+
if self._match(TokenType.BRACKET_START):
|
|
2245
|
+
return self._parse_array()
|
|
2246
|
+
|
|
2247
|
+
if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
|
|
2248
|
+
return self._parse_identifier_or_call()
|
|
2249
|
+
|
|
2250
|
+
return ASTNode('literal', value=None)
|
|
2251
|
+
|
|
2252
|
+
def _parse_module_reference(self) -> ASTNode:
|
|
2253
|
+
"""Parse @name, handling method calls and property access.
|
|
2254
|
+
|
|
2255
|
+
@name alone -> module_ref
|
|
2256
|
+
@name.method() -> call with member_access
|
|
2257
|
+
@name.property -> member_access
|
|
2258
|
+
"""
|
|
2259
|
+
# Get base name
|
|
2260
|
+
name = self._advance().value
|
|
2261
|
+
node = ASTNode('module_ref', value=name)
|
|
2262
|
+
|
|
2263
|
+
# Continue to handle member access, calls, and indexing
|
|
2264
|
+
while True:
|
|
2265
|
+
if self._match(TokenType.DOT):
|
|
2266
|
+
member = self._advance().value
|
|
2267
|
+
node = ASTNode('member_access', value={'object': node, 'member': member})
|
|
2268
|
+
elif self._match(TokenType.PAREN_START):
|
|
2269
|
+
# Function call - use _parse_call_arguments for kwargs support
|
|
2270
|
+
args, kwargs = self._parse_call_arguments()
|
|
2271
|
+
self._expect(TokenType.PAREN_END)
|
|
2272
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2273
|
+
elif self._match(TokenType.BRACKET_START):
|
|
2274
|
+
# Index access
|
|
2275
|
+
index = self._parse_expression()
|
|
2276
|
+
self._expect(TokenType.BRACKET_END)
|
|
2277
|
+
node = ASTNode('index_access', value={'object': node, 'index': index})
|
|
2278
|
+
else:
|
|
2279
|
+
break
|
|
2280
|
+
|
|
2281
|
+
return node
|
|
2282
|
+
|
|
2283
|
+
def _parse_call_arguments(self) -> tuple:
|
|
2284
|
+
"""Parse function call arguments, supporting both positional and named (key=value).
|
|
2285
|
+
|
|
2286
|
+
Returns: (args, kwargs) where:
|
|
2287
|
+
args = list of positional argument expressions
|
|
2288
|
+
kwargs = dict of {name: expression} for named arguments
|
|
2289
|
+
"""
|
|
2290
|
+
args = []
|
|
2291
|
+
kwargs = {}
|
|
2292
|
+
|
|
2293
|
+
while not self._check(TokenType.PAREN_END) and not self._is_at_end():
|
|
2294
|
+
# Check for named argument: identifier = expression
|
|
2295
|
+
if self._check(TokenType.IDENTIFIER):
|
|
2296
|
+
saved_pos = self.pos # Save token position
|
|
2297
|
+
name_token = self._advance()
|
|
2298
|
+
|
|
2299
|
+
if self._check(TokenType.EQUALS):
|
|
2300
|
+
# Named argument: name=value
|
|
2301
|
+
self._advance() # consume =
|
|
2302
|
+
value = self._parse_expression()
|
|
2303
|
+
kwargs[name_token.value] = value
|
|
2304
|
+
else:
|
|
2305
|
+
# Not named, restore and parse as expression
|
|
2306
|
+
self.pos = saved_pos # Restore token position
|
|
2307
|
+
args.append(self._parse_expression())
|
|
2308
|
+
else:
|
|
2309
|
+
args.append(self._parse_expression())
|
|
2310
|
+
|
|
2311
|
+
if not self._check(TokenType.PAREN_END):
|
|
2312
|
+
self._expect(TokenType.COMMA)
|
|
2313
|
+
|
|
2314
|
+
return args, kwargs
|
|
2315
|
+
|
|
2316
|
+
def _parse_identifier_or_call(self) -> ASTNode:
|
|
2317
|
+
name = self._advance().value
|
|
2318
|
+
|
|
2319
|
+
# Check for namespace syntax: json::read, string::cut, etc.
|
|
2320
|
+
if self._match(TokenType.DOUBLE_COLON):
|
|
2321
|
+
if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
|
|
2322
|
+
namespace_member = self._advance().value
|
|
2323
|
+
name = f"{name}::{namespace_member}"
|
|
2324
|
+
|
|
2325
|
+
# Check for instance<"name"> syntax - gets/creates shared instance
|
|
2326
|
+
if name == 'instance' and self._check(TokenType.COMPARE_LT):
|
|
2327
|
+
self._advance() # consume <
|
|
2328
|
+
# Expect string literal for instance name
|
|
2329
|
+
if self._check(TokenType.STRING):
|
|
2330
|
+
instance_name = self._advance().value
|
|
2331
|
+
elif self._check(TokenType.IDENTIFIER):
|
|
2332
|
+
instance_name = self._advance().value
|
|
2333
|
+
else:
|
|
2334
|
+
raise CSSLParserError("Expected instance name (string or identifier)", self._current_line())
|
|
2335
|
+
self._expect(TokenType.COMPARE_GT) # consume >
|
|
2336
|
+
return ASTNode('instance_ref', value=instance_name)
|
|
2337
|
+
|
|
2338
|
+
# Check for type generic instantiation: stack<string>, vector<int>, map<string, int>, etc.
|
|
2339
|
+
# This creates a new instance of the type with the specified element type
|
|
2340
|
+
if name in TYPE_GENERICS and self._check(TokenType.COMPARE_LT):
|
|
2341
|
+
self._advance() # consume <
|
|
2342
|
+
element_type = 'dynamic'
|
|
2343
|
+
value_type = None # For map<K, V>
|
|
2344
|
+
|
|
2345
|
+
if self._check(TokenType.KEYWORD) or self._check(TokenType.IDENTIFIER):
|
|
2346
|
+
element_type = self._advance().value
|
|
2347
|
+
|
|
2348
|
+
# Check for second type parameter (map<K, V>)
|
|
2349
|
+
if name == 'map' and self._check(TokenType.COMMA):
|
|
2350
|
+
self._advance() # consume ,
|
|
2351
|
+
if self._check(TokenType.KEYWORD) or self._check(TokenType.IDENTIFIER):
|
|
2352
|
+
value_type = self._advance().value
|
|
2353
|
+
else:
|
|
2354
|
+
value_type = 'dynamic'
|
|
2355
|
+
|
|
2356
|
+
self._expect(TokenType.COMPARE_GT) # consume >
|
|
2357
|
+
|
|
2358
|
+
# Check for inline initialization: map<K,V>{"key": "value", ...}
|
|
2359
|
+
init_values = None
|
|
2360
|
+
if self._check(TokenType.BLOCK_START):
|
|
2361
|
+
self._advance() # consume {
|
|
2362
|
+
init_values = {}
|
|
2363
|
+
|
|
2364
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
2365
|
+
# Parse key
|
|
2366
|
+
if self._check(TokenType.STRING):
|
|
2367
|
+
key = self._advance().value
|
|
2368
|
+
elif self._check(TokenType.IDENTIFIER):
|
|
2369
|
+
key = self._advance().value
|
|
2370
|
+
else:
|
|
2371
|
+
key = str(self._parse_expression().value) if hasattr(self._parse_expression(), 'value') else 'key'
|
|
2372
|
+
|
|
2373
|
+
# Expect : or =
|
|
2374
|
+
if self._check(TokenType.COLON):
|
|
2375
|
+
self._advance()
|
|
2376
|
+
elif self._check(TokenType.EQUALS):
|
|
2377
|
+
self._advance()
|
|
2378
|
+
|
|
2379
|
+
# Parse value
|
|
2380
|
+
value = self._parse_expression()
|
|
2381
|
+
init_values[key] = value
|
|
2382
|
+
|
|
2383
|
+
# Optional comma
|
|
2384
|
+
if self._check(TokenType.COMMA):
|
|
2385
|
+
self._advance()
|
|
2386
|
+
|
|
2387
|
+
self._expect(TokenType.BLOCK_END) # consume }
|
|
2388
|
+
|
|
2389
|
+
return ASTNode('type_instantiation', value={
|
|
2390
|
+
'type': name,
|
|
2391
|
+
'element_type': element_type,
|
|
2392
|
+
'value_type': value_type,
|
|
2393
|
+
'init_values': init_values
|
|
2394
|
+
})
|
|
2395
|
+
|
|
2396
|
+
# Check for type-parameterized function call: OpenFind<string>(0)
|
|
2397
|
+
if name in TYPE_PARAM_FUNCTIONS and self._check(TokenType.COMPARE_LT):
|
|
2398
|
+
self._advance() # consume <
|
|
2399
|
+
type_param = 'dynamic'
|
|
2400
|
+
if self._check(TokenType.KEYWORD) or self._check(TokenType.IDENTIFIER):
|
|
2401
|
+
type_param = self._advance().value
|
|
2402
|
+
self._expect(TokenType.COMPARE_GT) # consume >
|
|
2403
|
+
|
|
2404
|
+
# Must be followed by ()
|
|
2405
|
+
if self._check(TokenType.PAREN_START):
|
|
2406
|
+
self._advance() # consume (
|
|
2407
|
+
args = []
|
|
2408
|
+
while not self._check(TokenType.PAREN_END):
|
|
2409
|
+
args.append(self._parse_expression())
|
|
2410
|
+
if not self._check(TokenType.PAREN_END):
|
|
2411
|
+
self._expect(TokenType.COMMA)
|
|
2412
|
+
self._expect(TokenType.PAREN_END)
|
|
2413
|
+
|
|
2414
|
+
# Return as typed function call
|
|
2415
|
+
return ASTNode('typed_call', value={
|
|
2416
|
+
'name': name,
|
|
2417
|
+
'type_param': type_param,
|
|
2418
|
+
'args': args
|
|
2419
|
+
})
|
|
2420
|
+
|
|
2421
|
+
node = ASTNode('identifier', value=name)
|
|
2422
|
+
|
|
2423
|
+
while True:
|
|
2424
|
+
if self._match(TokenType.DOT):
|
|
2425
|
+
member = self._advance().value
|
|
2426
|
+
node = ASTNode('member_access', value={'object': node, 'member': member})
|
|
2427
|
+
elif self._match(TokenType.PAREN_START):
|
|
2428
|
+
args, kwargs = self._parse_call_arguments()
|
|
2429
|
+
self._expect(TokenType.PAREN_END)
|
|
2430
|
+
node = ASTNode('call', value={'callee': node, 'args': args, 'kwargs': kwargs})
|
|
2431
|
+
elif self._match(TokenType.BRACKET_START):
|
|
2432
|
+
index = self._parse_expression()
|
|
2433
|
+
self._expect(TokenType.BRACKET_END)
|
|
2434
|
+
node = ASTNode('index_access', value={'object': node, 'index': index})
|
|
2435
|
+
else:
|
|
2436
|
+
break
|
|
2437
|
+
|
|
2438
|
+
return node
|
|
2439
|
+
|
|
2440
|
+
def _is_object_literal(self) -> bool:
|
|
2441
|
+
"""Check if current position is an object literal { key = value } vs action block { expr; }
|
|
2442
|
+
|
|
2443
|
+
Object literal: { name = value; } or { "key" = value; }
|
|
2444
|
+
Action block: { %version; } or { "1.0.0" } or { call(); }
|
|
2445
|
+
"""
|
|
2446
|
+
# Empty block is action block
|
|
2447
|
+
if self._check(TokenType.BLOCK_END):
|
|
2448
|
+
return False
|
|
2449
|
+
|
|
2450
|
+
# Save position for lookahead
|
|
2451
|
+
saved_pos = self.pos
|
|
2452
|
+
|
|
2453
|
+
# Check if it looks like key = value pattern
|
|
2454
|
+
is_object = False
|
|
2455
|
+
if self._check(TokenType.IDENTIFIER) or self._check(TokenType.STRING):
|
|
2456
|
+
self._advance() # skip key
|
|
2457
|
+
if self._check(TokenType.EQUALS):
|
|
2458
|
+
# Looks like object literal: { key = ...
|
|
2459
|
+
is_object = True
|
|
2460
|
+
|
|
2461
|
+
# Restore position
|
|
2462
|
+
self.pos = saved_pos
|
|
2463
|
+
return is_object
|
|
2464
|
+
|
|
2465
|
+
def _parse_action_block_expression(self) -> ASTNode:
|
|
2466
|
+
"""Parse an action block expression: { expr; expr2; } returns last value
|
|
2467
|
+
|
|
2468
|
+
Used for: v <== { %version; } or v <== { "1.0.0" }
|
|
2469
|
+
"""
|
|
2470
|
+
children = []
|
|
2471
|
+
|
|
2472
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
2473
|
+
# Parse statement or expression
|
|
2474
|
+
if (self._check(TokenType.IDENTIFIER) or self._check(TokenType.AT) or
|
|
2475
|
+
self._check(TokenType.CAPTURED_REF) or self._check(TokenType.SHARED_REF) or
|
|
2476
|
+
self._check(TokenType.GLOBAL_REF) or self._check(TokenType.SELF_REF) or
|
|
2477
|
+
self._check(TokenType.STRING) or self._check(TokenType.NUMBER) or
|
|
2478
|
+
self._check(TokenType.BOOLEAN) or self._check(TokenType.NULL) or
|
|
2479
|
+
self._check(TokenType.PAREN_START)):
|
|
2480
|
+
# Parse as expression and wrap in expression node for _execute_node
|
|
2481
|
+
expr = self._parse_expression()
|
|
2482
|
+
self._match(TokenType.SEMICOLON)
|
|
2483
|
+
children.append(ASTNode('expression', value=expr))
|
|
2484
|
+
elif self._check(TokenType.KEYWORD):
|
|
2485
|
+
# Parse as statement
|
|
2486
|
+
stmt = self._parse_statement()
|
|
2487
|
+
if stmt:
|
|
2488
|
+
children.append(stmt)
|
|
2489
|
+
else:
|
|
2490
|
+
self._advance()
|
|
2491
|
+
|
|
2492
|
+
self._expect(TokenType.BLOCK_END)
|
|
2493
|
+
return ASTNode('action_block', children=children)
|
|
2494
|
+
|
|
2495
|
+
def _parse_object(self) -> ASTNode:
|
|
2496
|
+
properties = {}
|
|
2497
|
+
|
|
2498
|
+
while not self._check(TokenType.BLOCK_END) and not self._is_at_end():
|
|
2499
|
+
if self._check(TokenType.IDENTIFIER) or self._check(TokenType.STRING):
|
|
2500
|
+
key = self._advance().value
|
|
2501
|
+
self._expect(TokenType.EQUALS)
|
|
2502
|
+
value = self._parse_expression()
|
|
2503
|
+
properties[key] = value
|
|
2504
|
+
self._match(TokenType.SEMICOLON)
|
|
2505
|
+
self._match(TokenType.COMMA)
|
|
2506
|
+
else:
|
|
2507
|
+
self._advance()
|
|
2508
|
+
|
|
2509
|
+
self._expect(TokenType.BLOCK_END)
|
|
2510
|
+
return ASTNode('object', value=properties)
|
|
2511
|
+
|
|
2512
|
+
def _parse_array(self) -> ASTNode:
|
|
2513
|
+
elements = []
|
|
2514
|
+
|
|
2515
|
+
while not self._check(TokenType.BRACKET_END) and not self._is_at_end():
|
|
2516
|
+
elements.append(self._parse_expression())
|
|
2517
|
+
if not self._check(TokenType.BRACKET_END):
|
|
2518
|
+
self._expect(TokenType.COMMA)
|
|
2519
|
+
|
|
2520
|
+
self._expect(TokenType.BRACKET_END)
|
|
2521
|
+
return ASTNode('array', value=elements)
|
|
2522
|
+
|
|
2523
|
+
def _parse_value(self) -> Any:
|
|
2524
|
+
if self._check(TokenType.STRING):
|
|
2525
|
+
return self._advance().value
|
|
2526
|
+
if self._check(TokenType.NUMBER):
|
|
2527
|
+
return self._advance().value
|
|
2528
|
+
if self._check(TokenType.BOOLEAN):
|
|
2529
|
+
return self._advance().value
|
|
2530
|
+
if self._check(TokenType.NULL):
|
|
2531
|
+
self._advance()
|
|
2532
|
+
return None
|
|
2533
|
+
if self._check(TokenType.IDENTIFIER) or self._check(TokenType.KEYWORD):
|
|
2534
|
+
return self._advance().value
|
|
2535
|
+
return None
|
|
2536
|
+
|
|
2537
|
+
def _check_keyword(self, keyword: str) -> bool:
|
|
2538
|
+
return self._current().type == TokenType.KEYWORD and self._current().value == keyword
|
|
2539
|
+
|
|
2540
|
+
|
|
2541
|
+
def parse_cssl(source: str) -> ASTNode:
|
|
2542
|
+
"""Parse CSSL source code into an AST - auto-detects service vs program format"""
|
|
2543
|
+
lexer = CSSLLexer(source)
|
|
2544
|
+
tokens = lexer.tokenize()
|
|
2545
|
+
parser = CSSLParser(tokens, lexer.source_lines)
|
|
2546
|
+
|
|
2547
|
+
# Auto-detect: if first token is '{', it's a service file
|
|
2548
|
+
# Otherwise treat as standalone program (whitespace is already filtered by lexer)
|
|
2549
|
+
if tokens and tokens[0].type == TokenType.BLOCK_START:
|
|
2550
|
+
return parser.parse() # Service file format
|
|
2551
|
+
else:
|
|
2552
|
+
return parser.parse_program() # Standalone program format
|
|
2553
|
+
|
|
2554
|
+
|
|
2555
|
+
def parse_cssl_program(source: str) -> ASTNode:
|
|
2556
|
+
"""Parse standalone CSSL program (no service wrapper) into an AST"""
|
|
2557
|
+
lexer = CSSLLexer(source)
|
|
2558
|
+
tokens = lexer.tokenize()
|
|
2559
|
+
parser = CSSLParser(tokens, lexer.source_lines)
|
|
2560
|
+
return parser.parse_program()
|
|
2561
|
+
|
|
2562
|
+
|
|
2563
|
+
def tokenize_cssl(source: str) -> List[Token]:
|
|
2564
|
+
"""Tokenize CSSL source code (useful for syntax highlighting)"""
|
|
2565
|
+
lexer = CSSLLexer(source)
|
|
2566
|
+
return lexer.tokenize()
|
|
2567
|
+
|
|
2568
|
+
|
|
2569
|
+
# Export public API
|
|
2570
|
+
__all__ = [
|
|
2571
|
+
'TokenType', 'Token', 'ASTNode',
|
|
2572
|
+
'CSSLLexer', 'CSSLParser', 'CSSLSyntaxError',
|
|
2573
|
+
'parse_cssl', 'parse_cssl_program', 'tokenize_cssl',
|
|
2574
|
+
'KEYWORDS', 'TYPE_LITERALS'
|
|
2575
|
+
]
|