minecraft-datapack-language 15.4.28__py3-none-any.whl → 15.4.29__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.
- minecraft_datapack_language/__init__.py +17 -2
- minecraft_datapack_language/_version.py +2 -2
- minecraft_datapack_language/ast_nodes.py +87 -59
- minecraft_datapack_language/mdl_compiler.py +470 -0
- minecraft_datapack_language/mdl_errors.py +14 -0
- minecraft_datapack_language/mdl_lexer.py +624 -0
- minecraft_datapack_language/mdl_parser.py +573 -0
- minecraft_datapack_language-15.4.29.dist-info/METADATA +266 -0
- minecraft_datapack_language-15.4.29.dist-info/RECORD +16 -0
- minecraft_datapack_language/cli.py +0 -159
- minecraft_datapack_language/cli_build.py +0 -1292
- minecraft_datapack_language/cli_check.py +0 -155
- minecraft_datapack_language/cli_colors.py +0 -264
- minecraft_datapack_language/cli_help.py +0 -508
- minecraft_datapack_language/cli_new.py +0 -300
- minecraft_datapack_language/cli_utils.py +0 -276
- minecraft_datapack_language/expression_processor.py +0 -352
- minecraft_datapack_language/linter.py +0 -409
- minecraft_datapack_language/mdl_lexer_js.py +0 -754
- minecraft_datapack_language/mdl_parser_js.py +0 -1049
- minecraft_datapack_language/pack.py +0 -758
- minecraft_datapack_language-15.4.28.dist-info/METADATA +0 -1274
- minecraft_datapack_language-15.4.28.dist-info/RECORD +0 -25
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/WHEEL +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/entry_points.txt +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/licenses/LICENSE +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/top_level.txt +0 -0
@@ -1,1049 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
MDL Parser - Simplified JavaScript-style syntax with curly braces and semicolons
|
3
|
-
Handles basic control structures and number variables only
|
4
|
-
"""
|
5
|
-
|
6
|
-
from typing import List, Optional, Dict, Any, Union
|
7
|
-
from .mdl_lexer_js import Token, TokenType, lex_mdl_js
|
8
|
-
from .mdl_errors import MDLParserError, create_parser_error, MDLLexerError
|
9
|
-
from .ast_nodes import (
|
10
|
-
ASTNode, PackDeclaration, NamespaceDeclaration, FunctionDeclaration,
|
11
|
-
VariableDeclaration, VariableAssignment, IfStatement, WhileLoop,
|
12
|
-
FunctionCall, ExecuteStatement, RawText, Command, VariableExpression,
|
13
|
-
VariableSubstitutionExpression, LiteralExpression, BinaryExpression,
|
14
|
-
HookDeclaration, TagDeclaration, RecipeDeclaration, LootTableDeclaration,
|
15
|
-
AdvancementDeclaration, PredicateDeclaration, ItemModifierDeclaration,
|
16
|
-
StructureDeclaration
|
17
|
-
)
|
18
|
-
|
19
|
-
|
20
|
-
class MDLParser:
|
21
|
-
"""Parser for simplified MDL language."""
|
22
|
-
|
23
|
-
def __init__(self, tokens: List[Token], source_file: str = None):
|
24
|
-
self.tokens = tokens
|
25
|
-
self.current = 0
|
26
|
-
self.current_namespace = "mdl" # Track current namespace
|
27
|
-
self.source_file = source_file
|
28
|
-
|
29
|
-
def parse(self) -> Dict[str, Any]:
|
30
|
-
"""Parse tokens into AST."""
|
31
|
-
ast = {
|
32
|
-
'pack': None,
|
33
|
-
'namespace': None,
|
34
|
-
'functions': [],
|
35
|
-
'hooks': [],
|
36
|
-
'tags': [],
|
37
|
-
'imports': [],
|
38
|
-
'exports': [],
|
39
|
-
'variables': [], # Add support for top-level variable declarations
|
40
|
-
'recipes': [],
|
41
|
-
'loot_tables': [],
|
42
|
-
'advancements': [],
|
43
|
-
'predicates': [],
|
44
|
-
'item_modifiers': [],
|
45
|
-
'structures': []
|
46
|
-
}
|
47
|
-
|
48
|
-
while not self._is_at_end():
|
49
|
-
try:
|
50
|
-
if self._peek().type == TokenType.PACK:
|
51
|
-
ast['pack'] = self._parse_pack_declaration()
|
52
|
-
elif self._peek().type == TokenType.NAMESPACE:
|
53
|
-
namespace_decl = self._parse_namespace_declaration()
|
54
|
-
# Store namespace in both places for compatibility
|
55
|
-
ast['namespace'] = namespace_decl
|
56
|
-
# Also collect all namespaces in a list
|
57
|
-
if 'namespaces' not in ast:
|
58
|
-
ast['namespaces'] = []
|
59
|
-
ast['namespaces'].append(namespace_decl)
|
60
|
-
self.current_namespace = namespace_decl['name'] # Update current namespace
|
61
|
-
print(f"DEBUG: Parser updated current_namespace to: {self.current_namespace}")
|
62
|
-
elif self._peek().type == TokenType.FUNCTION:
|
63
|
-
# Check if this is a function call or declaration
|
64
|
-
# Look ahead to see if there's a semicolon or brace
|
65
|
-
function_token = self._peek()
|
66
|
-
self._advance() # Consume 'function'
|
67
|
-
|
68
|
-
# Look ahead to see what comes after the function name
|
69
|
-
name_token = self._peek()
|
70
|
-
if name_token.type != TokenType.STRING:
|
71
|
-
raise create_parser_error(
|
72
|
-
message="Expected function name after 'function'",
|
73
|
-
file_path=self.source_file,
|
74
|
-
line=name_token.line,
|
75
|
-
column=name_token.column,
|
76
|
-
line_content=name_token.value,
|
77
|
-
suggestion="Provide a function name in quotes"
|
78
|
-
)
|
79
|
-
|
80
|
-
# Look ahead to see if there's a semicolon (function call) or brace (function declaration)
|
81
|
-
self._advance() # Consume function name
|
82
|
-
next_token = self._peek()
|
83
|
-
|
84
|
-
if next_token.type == TokenType.SEMICOLON:
|
85
|
-
# This is a function call
|
86
|
-
self._advance() # Consume semicolon
|
87
|
-
name = name_token.value.strip('"').strip("'")
|
88
|
-
ast['functions'].append({"type": "function_call", "name": name})
|
89
|
-
else:
|
90
|
-
# This is a function declaration - reset and parse properly
|
91
|
-
self.current -= 2 # Go back to 'function' token
|
92
|
-
ast['functions'].append(self._parse_function_declaration())
|
93
|
-
elif self._peek().type == TokenType.ON_LOAD:
|
94
|
-
ast['hooks'].append(self._parse_hook_declaration())
|
95
|
-
elif self._peek().type == TokenType.ON_TICK:
|
96
|
-
ast['hooks'].append(self._parse_hook_declaration())
|
97
|
-
elif self._peek().type == TokenType.TAG:
|
98
|
-
ast['tags'].append(self._parse_tag_declaration())
|
99
|
-
elif self._peek().type == TokenType.VAR:
|
100
|
-
# Handle top-level variable declarations
|
101
|
-
ast['variables'].append(self._parse_variable_declaration())
|
102
|
-
elif self._peek().type == TokenType.RECIPE:
|
103
|
-
print(f"DEBUG: Found RECIPE token, current_namespace: {self.current_namespace}")
|
104
|
-
ast['recipes'].append(self._parse_recipe_declaration())
|
105
|
-
elif self._peek().type == TokenType.LOOT_TABLE:
|
106
|
-
ast['loot_tables'].append(self._parse_loot_table_declaration())
|
107
|
-
elif self._peek().type == TokenType.ADVANCEMENT:
|
108
|
-
ast['advancements'].append(self._parse_advancement_declaration())
|
109
|
-
elif self._peek().type == TokenType.PREDICATE:
|
110
|
-
ast['predicates'].append(self._parse_predicate_declaration())
|
111
|
-
elif self._peek().type == TokenType.ITEM_MODIFIER:
|
112
|
-
ast['item_modifiers'].append(self._parse_item_modifier_declaration())
|
113
|
-
elif self._peek().type == TokenType.STRUCTURE:
|
114
|
-
ast['structures'].append(self._parse_structure_declaration())
|
115
|
-
else:
|
116
|
-
# Skip unknown tokens
|
117
|
-
self._advance()
|
118
|
-
except MDLLexerError:
|
119
|
-
# Re-raise lexer errors as they already have proper formatting
|
120
|
-
raise
|
121
|
-
except Exception as e:
|
122
|
-
# Convert other exceptions to parser errors
|
123
|
-
current_token = self._peek()
|
124
|
-
raise create_parser_error(
|
125
|
-
message=str(e),
|
126
|
-
file_path=self.source_file,
|
127
|
-
line=current_token.line,
|
128
|
-
column=current_token.column,
|
129
|
-
line_content=current_token.value,
|
130
|
-
suggestion="Check the syntax and ensure all required tokens are present"
|
131
|
-
)
|
132
|
-
|
133
|
-
# Check for missing closing braces by looking for unmatched opening braces
|
134
|
-
self._check_for_missing_braces()
|
135
|
-
|
136
|
-
return ast
|
137
|
-
|
138
|
-
def _parse_pack_declaration(self) -> PackDeclaration:
|
139
|
-
"""Parse pack declaration."""
|
140
|
-
self._match(TokenType.PACK)
|
141
|
-
|
142
|
-
# Parse pack name
|
143
|
-
name_token = self._match(TokenType.STRING)
|
144
|
-
name = name_token.value.strip('"').strip("'")
|
145
|
-
|
146
|
-
# Parse description
|
147
|
-
description_token = self._match(TokenType.STRING)
|
148
|
-
description = description_token.value.strip('"').strip("'")
|
149
|
-
|
150
|
-
# Parse pack_format
|
151
|
-
pack_format_token = self._match(TokenType.NUMBER)
|
152
|
-
pack_format = int(pack_format_token.value)
|
153
|
-
|
154
|
-
self._match(TokenType.SEMICOLON)
|
155
|
-
|
156
|
-
return {"type": "pack_declaration", "name": name, "description": description, "pack_format": pack_format}
|
157
|
-
|
158
|
-
def _parse_namespace_declaration(self) -> NamespaceDeclaration:
|
159
|
-
"""Parse namespace declaration."""
|
160
|
-
self._match(TokenType.NAMESPACE)
|
161
|
-
|
162
|
-
name_token = self._match(TokenType.STRING)
|
163
|
-
name = name_token.value.strip('"').strip("'")
|
164
|
-
|
165
|
-
self._match(TokenType.SEMICOLON)
|
166
|
-
|
167
|
-
return {"type": "namespace_declaration", "name": name}
|
168
|
-
|
169
|
-
def _parse_function_declaration(self) -> FunctionDeclaration:
|
170
|
-
"""Parse function declaration."""
|
171
|
-
self._match(TokenType.FUNCTION)
|
172
|
-
|
173
|
-
name_token = self._match(TokenType.STRING)
|
174
|
-
name = name_token.value.strip('"').strip("'")
|
175
|
-
|
176
|
-
self._match(TokenType.LBRACE)
|
177
|
-
|
178
|
-
body = []
|
179
|
-
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
180
|
-
body.append(self._parse_statement())
|
181
|
-
|
182
|
-
if self._is_at_end():
|
183
|
-
raise create_parser_error(
|
184
|
-
message="Missing closing brace for function",
|
185
|
-
file_path=self.source_file,
|
186
|
-
line=self._peek().line,
|
187
|
-
column=self._peek().column,
|
188
|
-
line_content=self._peek().value,
|
189
|
-
suggestion="Add a closing brace (}) to match the opening brace"
|
190
|
-
)
|
191
|
-
|
192
|
-
self._match(TokenType.RBRACE)
|
193
|
-
|
194
|
-
return {"type": "function_declaration", "name": name, "body": body}
|
195
|
-
|
196
|
-
def _parse_recipe_declaration(self) -> RecipeDeclaration:
|
197
|
-
"""Parse recipe declaration."""
|
198
|
-
self._match(TokenType.RECIPE)
|
199
|
-
|
200
|
-
name_token = self._match(TokenType.STRING)
|
201
|
-
name = name_token.value.strip('"').strip("'")
|
202
|
-
|
203
|
-
# Expect a JSON file path
|
204
|
-
json_file_token = self._match(TokenType.STRING)
|
205
|
-
json_file = json_file_token.value.strip('"').strip("'")
|
206
|
-
|
207
|
-
self._match(TokenType.SEMICOLON)
|
208
|
-
|
209
|
-
# Store reference to JSON file and current namespace
|
210
|
-
data = {"json_file": json_file}
|
211
|
-
|
212
|
-
result = {"name": name, "data": data, "_source_namespace": self.current_namespace}
|
213
|
-
print(f"DEBUG: Recipe '{name}' declared with namespace: {self.current_namespace}")
|
214
|
-
return result
|
215
|
-
|
216
|
-
def _parse_loot_table_declaration(self) -> LootTableDeclaration:
|
217
|
-
"""Parse loot table declaration."""
|
218
|
-
self._match(TokenType.LOOT_TABLE)
|
219
|
-
|
220
|
-
name_token = self._match(TokenType.STRING)
|
221
|
-
name = name_token.value.strip('"').strip("'")
|
222
|
-
|
223
|
-
json_file_token = self._match(TokenType.STRING)
|
224
|
-
json_file = json_file_token.value.strip('"').strip("'")
|
225
|
-
|
226
|
-
self._match(TokenType.SEMICOLON)
|
227
|
-
|
228
|
-
data = {"json_file": json_file}
|
229
|
-
return {"name": name, "data": data, "_source_namespace": self.current_namespace}
|
230
|
-
|
231
|
-
def _parse_advancement_declaration(self) -> AdvancementDeclaration:
|
232
|
-
"""Parse advancement declaration."""
|
233
|
-
self._match(TokenType.ADVANCEMENT)
|
234
|
-
|
235
|
-
name_token = self._match(TokenType.STRING)
|
236
|
-
name = name_token.value.strip('"').strip("'")
|
237
|
-
|
238
|
-
json_file_token = self._match(TokenType.STRING)
|
239
|
-
json_file = json_file_token.value.strip('"').strip("'")
|
240
|
-
|
241
|
-
self._match(TokenType.SEMICOLON)
|
242
|
-
|
243
|
-
data = {"json_file": json_file}
|
244
|
-
return {"name": name, "data": data, "_source_namespace": self.current_namespace}
|
245
|
-
|
246
|
-
def _parse_predicate_declaration(self) -> PredicateDeclaration:
|
247
|
-
"""Parse predicate declaration."""
|
248
|
-
self._match(TokenType.PREDICATE)
|
249
|
-
|
250
|
-
name_token = self._match(TokenType.STRING)
|
251
|
-
name = name_token.value.strip('"').strip("'")
|
252
|
-
|
253
|
-
json_file_token = self._match(TokenType.STRING)
|
254
|
-
json_file = json_file_token.value.strip('"').strip("'")
|
255
|
-
|
256
|
-
self._match(TokenType.SEMICOLON)
|
257
|
-
|
258
|
-
data = {"json_file": json_file}
|
259
|
-
return {"name": name, "data": data, "_source_namespace": self.current_namespace}
|
260
|
-
|
261
|
-
def _parse_item_modifier_declaration(self) -> ItemModifierDeclaration:
|
262
|
-
"""Parse item modifier declaration."""
|
263
|
-
self._match(TokenType.ITEM_MODIFIER)
|
264
|
-
|
265
|
-
name_token = self._match(TokenType.STRING)
|
266
|
-
name = name_token.value.strip('"').strip("'")
|
267
|
-
|
268
|
-
json_file_token = self._match(TokenType.STRING)
|
269
|
-
json_file = json_file_token.value.strip('"').strip("'")
|
270
|
-
|
271
|
-
self._match(TokenType.SEMICOLON)
|
272
|
-
|
273
|
-
data = {"json_file": json_file}
|
274
|
-
return {"name": name, "data": data, "_source_namespace": self.current_namespace}
|
275
|
-
|
276
|
-
def _parse_structure_declaration(self) -> StructureDeclaration:
|
277
|
-
"""Parse structure declaration."""
|
278
|
-
self._match(TokenType.STRUCTURE)
|
279
|
-
|
280
|
-
name_token = self._match(TokenType.STRING)
|
281
|
-
name = name_token.value.strip('"').strip("'")
|
282
|
-
|
283
|
-
json_file_token = self._match(TokenType.STRING)
|
284
|
-
json_file = json_file_token.value.strip('"').strip("'")
|
285
|
-
|
286
|
-
self._match(TokenType.SEMICOLON)
|
287
|
-
|
288
|
-
data = {"json_file": json_file}
|
289
|
-
return {"name": name, "data": data, "_source_namespace": self.current_namespace}
|
290
|
-
|
291
|
-
def _parse_statement(self) -> ASTNode:
|
292
|
-
"""Parse a statement."""
|
293
|
-
if self._peek().type == TokenType.VAR:
|
294
|
-
return self._parse_variable_declaration()
|
295
|
-
elif self._peek().type == TokenType.IF:
|
296
|
-
return self._parse_if_statement()
|
297
|
-
elif self._peek().type == TokenType.WHILE:
|
298
|
-
return self._parse_while_loop()
|
299
|
-
elif self._peek().type == TokenType.FUNCTION:
|
300
|
-
return self._parse_function_call()
|
301
|
-
elif self._peek().type == TokenType.EXECUTE:
|
302
|
-
return self._parse_execute_command()
|
303
|
-
elif self._peek().type == TokenType.RAW_START:
|
304
|
-
return self._parse_raw_text()
|
305
|
-
elif self._peek().type == TokenType.SAY:
|
306
|
-
return self._parse_say_command()
|
307
|
-
elif self._peek().type == TokenType.IDENTIFIER:
|
308
|
-
# Check for for loops (which are no longer supported)
|
309
|
-
if self._peek().value == "for":
|
310
|
-
raise create_parser_error(
|
311
|
-
message="For loops are no longer supported in MDL. Use while loops instead.",
|
312
|
-
file_path=self.source_file,
|
313
|
-
line=self._peek().line,
|
314
|
-
column=self._peek().column,
|
315
|
-
line_content=self._peek().value,
|
316
|
-
suggestion="Replace 'for' with 'while' and adjust the loop structure"
|
317
|
-
)
|
318
|
-
|
319
|
-
# Check if this is a variable assignment (identifier followed by =)
|
320
|
-
# Need to handle scope selectors like: identifier<scope> = ...
|
321
|
-
if (self.current + 1 < len(self.tokens) and
|
322
|
-
self.tokens[self.current + 1].type == TokenType.ASSIGN):
|
323
|
-
return self._parse_variable_assignment()
|
324
|
-
elif (self.current + 1 < len(self.tokens) and
|
325
|
-
self.tokens[self.current + 1].type == TokenType.LANGLE):
|
326
|
-
# This might be a scoped variable assignment: identifier<scope> = ...
|
327
|
-
# Look ahead to see if there's an assignment after the scope selector
|
328
|
-
temp_current = self.current + 1
|
329
|
-
while (temp_current < len(self.tokens) and
|
330
|
-
self.tokens[temp_current].type != TokenType.RANGLE and
|
331
|
-
self.tokens[temp_current].type != TokenType.ASSIGN):
|
332
|
-
temp_current += 1
|
333
|
-
|
334
|
-
if (temp_current < len(self.tokens) and
|
335
|
-
self.tokens[temp_current].type == TokenType.RANGLE and
|
336
|
-
temp_current + 1 < len(self.tokens) and
|
337
|
-
self.tokens[temp_current + 1].type == TokenType.ASSIGN):
|
338
|
-
# This is a scoped variable assignment
|
339
|
-
return self._parse_variable_assignment()
|
340
|
-
else:
|
341
|
-
# This is a command with scope selector
|
342
|
-
return self._parse_command()
|
343
|
-
else:
|
344
|
-
# Assume it's a command
|
345
|
-
return self._parse_command()
|
346
|
-
else:
|
347
|
-
# Assume it's a command
|
348
|
-
return self._parse_command()
|
349
|
-
|
350
|
-
def _parse_variable_declaration(self) -> VariableDeclaration:
|
351
|
-
"""Parse variable declaration."""
|
352
|
-
self._match(TokenType.VAR)
|
353
|
-
self._match(TokenType.NUM)
|
354
|
-
|
355
|
-
name_token = self._match(TokenType.IDENTIFIER)
|
356
|
-
name = name_token.value
|
357
|
-
|
358
|
-
# Check for scope selector after variable name
|
359
|
-
scope = None
|
360
|
-
if not self._is_at_end() and self._peek().type == TokenType.LANGLE:
|
361
|
-
self._match(TokenType.LANGLE) # consume '<'
|
362
|
-
|
363
|
-
# Parse scope selector content
|
364
|
-
scope_parts = []
|
365
|
-
while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
|
366
|
-
scope_parts.append(self._peek().value)
|
367
|
-
self._advance()
|
368
|
-
|
369
|
-
if self._is_at_end():
|
370
|
-
raise create_parser_error(
|
371
|
-
message="Unterminated scope selector",
|
372
|
-
file_path=self.source_file,
|
373
|
-
line=self._peek().line,
|
374
|
-
column=self._peek().column,
|
375
|
-
line_content=self._peek().value,
|
376
|
-
suggestion="Add a closing '>' to terminate the scope selector"
|
377
|
-
)
|
378
|
-
|
379
|
-
self._match(TokenType.RANGLE) # consume '>'
|
380
|
-
scope = ''.join(scope_parts)
|
381
|
-
# Update the name to include the scope selector
|
382
|
-
name = f"{name}<{scope}>"
|
383
|
-
|
384
|
-
self._match(TokenType.ASSIGN)
|
385
|
-
|
386
|
-
# Parse the value (could be a number or expression)
|
387
|
-
value = self._parse_expression()
|
388
|
-
|
389
|
-
self._match(TokenType.SEMICOLON)
|
390
|
-
|
391
|
-
return {"type": "variable_declaration", "name": name, "scope": scope, "value": value}
|
392
|
-
|
393
|
-
def _parse_variable_assignment(self) -> VariableAssignment:
|
394
|
-
"""Parse variable assignment."""
|
395
|
-
name_token = self._match(TokenType.IDENTIFIER)
|
396
|
-
name = name_token.value
|
397
|
-
|
398
|
-
# Check for scope selector after variable name
|
399
|
-
scope = None
|
400
|
-
if not self._is_at_end() and self._peek().type == TokenType.LANGLE:
|
401
|
-
self._match(TokenType.LANGLE) # consume '<'
|
402
|
-
|
403
|
-
# Parse scope selector content
|
404
|
-
scope_parts = []
|
405
|
-
while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
|
406
|
-
scope_parts.append(self._peek().value)
|
407
|
-
self._advance()
|
408
|
-
|
409
|
-
if self._is_at_end():
|
410
|
-
raise create_parser_error(
|
411
|
-
message="Unterminated scope selector",
|
412
|
-
file_path=self.source_file,
|
413
|
-
line=self._peek().line,
|
414
|
-
column=self._peek().column,
|
415
|
-
line_content=self._peek().value,
|
416
|
-
suggestion="Add a closing '>' to terminate the scope selector"
|
417
|
-
)
|
418
|
-
|
419
|
-
self._match(TokenType.RANGLE) # consume '>'
|
420
|
-
scope = ''.join(scope_parts)
|
421
|
-
# Update the name to include the scope selector
|
422
|
-
name = f"{name}<{scope}>"
|
423
|
-
|
424
|
-
self._match(TokenType.ASSIGN)
|
425
|
-
|
426
|
-
# Parse the value (could be a number or expression)
|
427
|
-
value = self._parse_expression()
|
428
|
-
|
429
|
-
self._match(TokenType.SEMICOLON)
|
430
|
-
|
431
|
-
return {"type": "variable_assignment", "name": name, "scope": scope, "value": value}
|
432
|
-
|
433
|
-
def _parse_if_statement(self) -> IfStatement:
|
434
|
-
"""Parse if statement."""
|
435
|
-
self._match(TokenType.IF)
|
436
|
-
|
437
|
-
# Parse condition
|
438
|
-
condition_token = self._match(TokenType.STRING)
|
439
|
-
condition = condition_token.value.strip('"').strip("'")
|
440
|
-
|
441
|
-
self._match(TokenType.LBRACE)
|
442
|
-
|
443
|
-
# Parse then body
|
444
|
-
then_body = []
|
445
|
-
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
446
|
-
then_body.append(self._parse_statement())
|
447
|
-
|
448
|
-
if self._is_at_end():
|
449
|
-
raise create_parser_error(
|
450
|
-
message="Missing closing brace for if statement",
|
451
|
-
file_path=self.source_file,
|
452
|
-
line=self._peek().line,
|
453
|
-
column=self._peek().column,
|
454
|
-
line_content=self._peek().value,
|
455
|
-
suggestion="Add a closing brace (}) to match the opening brace"
|
456
|
-
)
|
457
|
-
|
458
|
-
self._match(TokenType.RBRACE)
|
459
|
-
|
460
|
-
# Check for else or else if
|
461
|
-
else_body = None
|
462
|
-
if not self._is_at_end() and self._peek().type == TokenType.ELSE:
|
463
|
-
self._match(TokenType.ELSE)
|
464
|
-
|
465
|
-
# Check if this is an else if statement
|
466
|
-
if not self._is_at_end() and self._peek().type == TokenType.IF:
|
467
|
-
# This is an else if statement - parse it as a nested if
|
468
|
-
self._match(TokenType.IF)
|
469
|
-
|
470
|
-
# Parse the else if condition
|
471
|
-
condition_token = self._match(TokenType.STRING)
|
472
|
-
condition = condition_token.value.strip('"').strip("'")
|
473
|
-
|
474
|
-
self._match(TokenType.LBRACE)
|
475
|
-
|
476
|
-
# Parse the else if body
|
477
|
-
else_body = []
|
478
|
-
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
479
|
-
else_body.append(self._parse_statement())
|
480
|
-
|
481
|
-
if self._is_at_end():
|
482
|
-
raise create_parser_error(
|
483
|
-
message="Missing closing brace for else if statement",
|
484
|
-
file_path=self.source_file,
|
485
|
-
line=self._peek().line,
|
486
|
-
column=self._peek().column,
|
487
|
-
line_content=self._peek().value,
|
488
|
-
suggestion="Add a closing brace (}) to match the opening brace"
|
489
|
-
)
|
490
|
-
|
491
|
-
self._match(TokenType.RBRACE)
|
492
|
-
|
493
|
-
# Recursively check for more else if or else statements
|
494
|
-
if not self._is_at_end() and self._peek().type == TokenType.ELSE:
|
495
|
-
# Parse the remaining else/else if chain
|
496
|
-
remaining_else = self._parse_else_chain()
|
497
|
-
if remaining_else:
|
498
|
-
# Combine the else if body with the remaining else chain
|
499
|
-
else_body.extend(remaining_else)
|
500
|
-
else:
|
501
|
-
# This is a regular else statement
|
502
|
-
self._match(TokenType.LBRACE)
|
503
|
-
|
504
|
-
else_body = []
|
505
|
-
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
506
|
-
else_body.append(self._parse_statement())
|
507
|
-
|
508
|
-
if self._is_at_end():
|
509
|
-
raise create_parser_error(
|
510
|
-
message="Missing closing brace for else statement",
|
511
|
-
file_path=self.source_file,
|
512
|
-
line=self._peek().line,
|
513
|
-
column=self._peek().column,
|
514
|
-
line_content=self._peek().value,
|
515
|
-
suggestion="Add a closing brace (}) to match the opening brace"
|
516
|
-
)
|
517
|
-
|
518
|
-
self._match(TokenType.RBRACE)
|
519
|
-
|
520
|
-
return {"type": "if_statement", "condition": condition, "then_body": then_body, "else_body": else_body}
|
521
|
-
|
522
|
-
def _parse_else_chain(self) -> List[Any]:
|
523
|
-
"""Parse a chain of else if statements and final else statement."""
|
524
|
-
statements = []
|
525
|
-
|
526
|
-
while not self._is_at_end() and self._peek().type == TokenType.ELSE:
|
527
|
-
self._match(TokenType.ELSE)
|
528
|
-
|
529
|
-
# Check if this is an else if statement
|
530
|
-
if not self._is_at_end() and self._peek().type == TokenType.IF:
|
531
|
-
# This is an else if statement
|
532
|
-
self._match(TokenType.IF)
|
533
|
-
|
534
|
-
# Parse the else if condition
|
535
|
-
condition_token = self._match(TokenType.STRING)
|
536
|
-
condition = condition_token.value.strip('"').strip("'")
|
537
|
-
|
538
|
-
self._match(TokenType.LBRACE)
|
539
|
-
|
540
|
-
# Parse the else if body
|
541
|
-
else_if_body = []
|
542
|
-
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
543
|
-
else_if_body.append(self._parse_statement())
|
544
|
-
|
545
|
-
if self._is_at_end():
|
546
|
-
raise create_parser_error(
|
547
|
-
message="Missing closing brace for else if statement",
|
548
|
-
file_path=self.source_file,
|
549
|
-
line=self._peek().line,
|
550
|
-
column=self._peek().column,
|
551
|
-
line_content=self._peek().value,
|
552
|
-
suggestion="Add a closing brace (}) to match the opening brace"
|
553
|
-
)
|
554
|
-
|
555
|
-
self._match(TokenType.RBRACE)
|
556
|
-
|
557
|
-
# Add the else if statement to the chain
|
558
|
-
statements.append({
|
559
|
-
"type": "else_if_statement",
|
560
|
-
"condition": condition,
|
561
|
-
"body": else_if_body
|
562
|
-
})
|
563
|
-
else:
|
564
|
-
# This is a final else statement
|
565
|
-
self._match(TokenType.LBRACE)
|
566
|
-
|
567
|
-
else_body = []
|
568
|
-
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
569
|
-
else_body.append(self._parse_statement())
|
570
|
-
|
571
|
-
if self._is_at_end():
|
572
|
-
raise create_parser_error(
|
573
|
-
message="Missing closing brace for else statement",
|
574
|
-
file_path=self.source_file,
|
575
|
-
line=self._peek().line,
|
576
|
-
column=self._peek().column,
|
577
|
-
line_content=self._peek().value,
|
578
|
-
suggestion="Add a closing brace (}) to match the opening brace"
|
579
|
-
)
|
580
|
-
|
581
|
-
self._match(TokenType.RBRACE)
|
582
|
-
|
583
|
-
# Add the final else statement to the chain
|
584
|
-
statements.append({
|
585
|
-
"type": "else_statement",
|
586
|
-
"body": else_body
|
587
|
-
})
|
588
|
-
break # Final else statement ends the chain
|
589
|
-
|
590
|
-
return statements
|
591
|
-
|
592
|
-
def _parse_while_loop(self) -> WhileLoop:
|
593
|
-
"""Parse while loop."""
|
594
|
-
self._match(TokenType.WHILE)
|
595
|
-
|
596
|
-
# Parse condition
|
597
|
-
condition_token = self._match(TokenType.STRING)
|
598
|
-
condition = condition_token.value.strip('"').strip("'")
|
599
|
-
|
600
|
-
# Check for method parameter
|
601
|
-
method = None
|
602
|
-
if not self._is_at_end() and self._peek().type == TokenType.IDENTIFIER and self._peek().value == "method":
|
603
|
-
self._match(TokenType.IDENTIFIER) # consume "method"
|
604
|
-
self._match(TokenType.ASSIGN)
|
605
|
-
method_token = self._match(TokenType.STRING)
|
606
|
-
method = method_token.value.strip('"').strip("'")
|
607
|
-
|
608
|
-
self._match(TokenType.LBRACE)
|
609
|
-
|
610
|
-
# Parse body
|
611
|
-
body = []
|
612
|
-
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
613
|
-
body.append(self._parse_statement())
|
614
|
-
|
615
|
-
if self._is_at_end():
|
616
|
-
raise create_parser_error(
|
617
|
-
message="Missing closing brace for while loop",
|
618
|
-
file_path=self.source_file,
|
619
|
-
line=self._peek().line,
|
620
|
-
column=self._peek().column,
|
621
|
-
line_content=self._peek().value,
|
622
|
-
suggestion="Add a closing brace (}) to match the opening brace"
|
623
|
-
)
|
624
|
-
|
625
|
-
self._match(TokenType.RBRACE)
|
626
|
-
|
627
|
-
return {"type": "while_statement", "condition": condition, "method": method, "body": body}
|
628
|
-
|
629
|
-
def _parse_function_call(self) -> FunctionCall:
|
630
|
-
"""Parse function call."""
|
631
|
-
self._match(TokenType.FUNCTION)
|
632
|
-
|
633
|
-
name_token = self._match(TokenType.STRING)
|
634
|
-
name = name_token.value.strip('"').strip("'")
|
635
|
-
|
636
|
-
# Check for scope selector after function name
|
637
|
-
scope = None
|
638
|
-
if '<' in name and name.endswith('>'):
|
639
|
-
# Extract scope selector from function name
|
640
|
-
parts = name.split('<', 1)
|
641
|
-
if len(parts) == 2:
|
642
|
-
function_name = parts[0]
|
643
|
-
scope_selector = parts[1][:-1] # Remove closing >
|
644
|
-
scope = scope_selector
|
645
|
-
name = function_name
|
646
|
-
else:
|
647
|
-
# Malformed scope selector
|
648
|
-
raise create_parser_error(
|
649
|
-
message="Malformed scope selector in function call",
|
650
|
-
file_path=self.source_file,
|
651
|
-
line=self._peek().line,
|
652
|
-
column=self._peek().column,
|
653
|
-
line_content=name,
|
654
|
-
suggestion="Use format: function \"namespace:function_name<@selector>\""
|
655
|
-
)
|
656
|
-
|
657
|
-
# Extract function name from namespace:function_name format
|
658
|
-
if ':' in name:
|
659
|
-
namespace_parts = name.split(':', 1)
|
660
|
-
if len(namespace_parts) == 2:
|
661
|
-
namespace_name = namespace_parts[0]
|
662
|
-
function_name = namespace_parts[1]
|
663
|
-
# Store both namespace and function name
|
664
|
-
return {"type": "function_call", "name": function_name, "scope": scope, "namespace": namespace_name}
|
665
|
-
|
666
|
-
self._match(TokenType.SEMICOLON)
|
667
|
-
|
668
|
-
return {"type": "function_call", "name": name, "scope": scope}
|
669
|
-
|
670
|
-
def _parse_execute_statement(self) -> ExecuteStatement:
|
671
|
-
"""Parse execute statement."""
|
672
|
-
self._match(TokenType.EXECUTE)
|
673
|
-
|
674
|
-
# Parse the command
|
675
|
-
command_parts = []
|
676
|
-
while not self._is_at_end() and self._peek().type != TokenType.SEMICOLON:
|
677
|
-
command_parts.append(self._peek().value)
|
678
|
-
self._advance()
|
679
|
-
|
680
|
-
if self._is_at_end():
|
681
|
-
raise create_parser_error(
|
682
|
-
message="Missing semicolon after execute statement",
|
683
|
-
file_path=self.source_file,
|
684
|
-
line=self._peek().line,
|
685
|
-
column=self._peek().column,
|
686
|
-
line_content=self._peek().value,
|
687
|
-
suggestion="Add a semicolon (;) at the end of the execute statement"
|
688
|
-
)
|
689
|
-
|
690
|
-
self._match(TokenType.SEMICOLON)
|
691
|
-
|
692
|
-
command = _smart_join_command_parts(command_parts)
|
693
|
-
return {"type": "command", "command": command}
|
694
|
-
|
695
|
-
def _parse_raw_text(self) -> RawText:
|
696
|
-
"""Parse raw text block."""
|
697
|
-
self._match(TokenType.RAW_START)
|
698
|
-
|
699
|
-
# Parse the raw content
|
700
|
-
content_parts = []
|
701
|
-
while not self._is_at_end() and self._peek().type != TokenType.RAW_END:
|
702
|
-
if self._peek().type == TokenType.RAW:
|
703
|
-
# Add raw content
|
704
|
-
content_parts.append(self._peek().value)
|
705
|
-
self._advance()
|
706
|
-
else:
|
707
|
-
# Skip other tokens (shouldn't happen in raw mode)
|
708
|
-
self._advance()
|
709
|
-
|
710
|
-
if self._is_at_end():
|
711
|
-
raise create_parser_error(
|
712
|
-
message="Missing closing 'raw!$' for raw text block",
|
713
|
-
file_path=self.source_file,
|
714
|
-
line=self._peek().line,
|
715
|
-
column=self._peek().column,
|
716
|
-
line_content=self._peek().value,
|
717
|
-
suggestion="Add 'raw!$' to close the raw text block"
|
718
|
-
)
|
719
|
-
|
720
|
-
self._match(TokenType.RAW_END)
|
721
|
-
|
722
|
-
content = "".join(content_parts)
|
723
|
-
# Split content into individual commands by newlines
|
724
|
-
# Raw blocks contain raw Minecraft commands, not MDL commands with semicolons
|
725
|
-
commands = [cmd.strip() for cmd in content.split('\n') if cmd.strip()]
|
726
|
-
return {"type": "raw_text", "commands": commands}
|
727
|
-
|
728
|
-
def _parse_command(self) -> Command:
|
729
|
-
"""Parse a command."""
|
730
|
-
command_parts = []
|
731
|
-
while not self._is_at_end() and self._peek().type != TokenType.SEMICOLON:
|
732
|
-
current_token = self._peek()
|
733
|
-
|
734
|
-
# Check if this is an identifier that might be followed by a scope selector
|
735
|
-
if current_token.type == TokenType.IDENTIFIER:
|
736
|
-
identifier_name = current_token.value
|
737
|
-
command_parts.append(identifier_name)
|
738
|
-
self._advance() # consume the identifier
|
739
|
-
|
740
|
-
# Look ahead to see if there's a scope selector
|
741
|
-
if not self._is_at_end() and self._peek().type == TokenType.LANGLE:
|
742
|
-
# This is a scoped variable - parse the scope selector
|
743
|
-
self._match(TokenType.LANGLE) # consume '<'
|
744
|
-
|
745
|
-
# Parse scope selector content
|
746
|
-
scope_parts = []
|
747
|
-
while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
|
748
|
-
scope_parts.append(self._peek().value)
|
749
|
-
self._advance()
|
750
|
-
|
751
|
-
if self._is_at_end():
|
752
|
-
raise create_parser_error(
|
753
|
-
message="Unterminated scope selector in command",
|
754
|
-
file_path=self.source_file,
|
755
|
-
line=self._peek().line,
|
756
|
-
column=self._peek().column,
|
757
|
-
line_content=self._peek().value,
|
758
|
-
suggestion="Add a closing '>' to terminate the scope selector"
|
759
|
-
)
|
760
|
-
|
761
|
-
self._match(TokenType.RANGLE) # consume '>'
|
762
|
-
scope_selector = ''.join(scope_parts)
|
763
|
-
|
764
|
-
# Add the scope selector to the command parts
|
765
|
-
command_parts.append(f"<{scope_selector}>")
|
766
|
-
else:
|
767
|
-
# No scope selector, continue with next token
|
768
|
-
continue
|
769
|
-
else:
|
770
|
-
# Regular token, just add it
|
771
|
-
command_parts.append(current_token.value)
|
772
|
-
self._advance()
|
773
|
-
|
774
|
-
if self._is_at_end():
|
775
|
-
raise create_parser_error(
|
776
|
-
message="Missing semicolon after command",
|
777
|
-
file_path=self.source_file,
|
778
|
-
line=self._peek().line,
|
779
|
-
column=self._peek().column,
|
780
|
-
line_content=self._peek().value,
|
781
|
-
suggestion="Add a semicolon (;) at the end of the command"
|
782
|
-
)
|
783
|
-
|
784
|
-
self._match(TokenType.SEMICOLON)
|
785
|
-
|
786
|
-
command = _smart_join_command_parts(command_parts)
|
787
|
-
return {"type": "command", "command": command}
|
788
|
-
|
789
|
-
def _parse_say_command(self) -> Command:
|
790
|
-
"""Parse a say command."""
|
791
|
-
say_token = self._match(TokenType.SAY)
|
792
|
-
content = say_token.value
|
793
|
-
|
794
|
-
# The lexer no longer includes the semicolon, so we need to consume it here
|
795
|
-
self._match(TokenType.SEMICOLON)
|
796
|
-
|
797
|
-
return {"type": "command", "command": f"say {content}"}
|
798
|
-
|
799
|
-
def _parse_execute_command(self) -> Command:
|
800
|
-
"""Parse an execute command."""
|
801
|
-
execute_token = self._match(TokenType.EXECUTE)
|
802
|
-
content = execute_token.value
|
803
|
-
|
804
|
-
# The semicolon should already be consumed by the lexer
|
805
|
-
# But let's make sure we have it
|
806
|
-
if not self._is_at_end() and self._peek().type == TokenType.SEMICOLON:
|
807
|
-
self._match(TokenType.SEMICOLON)
|
808
|
-
|
809
|
-
return {"type": "command", "command": content}
|
810
|
-
|
811
|
-
def _parse_hook_declaration(self) -> HookDeclaration:
|
812
|
-
"""Parse hook declaration."""
|
813
|
-
if self._peek().type == TokenType.ON_TICK:
|
814
|
-
self._match(TokenType.ON_TICK)
|
815
|
-
hook_type = "tick"
|
816
|
-
else:
|
817
|
-
self._match(TokenType.ON_LOAD)
|
818
|
-
hook_type = "load"
|
819
|
-
|
820
|
-
function_name_token = self._match(TokenType.STRING)
|
821
|
-
function_name = function_name_token.value.strip('"').strip("'")
|
822
|
-
|
823
|
-
self._match(TokenType.SEMICOLON)
|
824
|
-
|
825
|
-
return {"type": "hook_declaration", "hook_type": hook_type, "function_name": function_name}
|
826
|
-
|
827
|
-
def _parse_tag_declaration(self) -> TagDeclaration:
|
828
|
-
"""Parse tag declaration."""
|
829
|
-
self._match(TokenType.TAG)
|
830
|
-
|
831
|
-
# Parse tag type
|
832
|
-
tag_type_token = self._match(TokenType.IDENTIFIER)
|
833
|
-
tag_type = tag_type_token.value
|
834
|
-
|
835
|
-
# Parse tag name
|
836
|
-
name_token = self._match(TokenType.STRING)
|
837
|
-
name = name_token.value.strip('"').strip("'")
|
838
|
-
|
839
|
-
self._match(TokenType.LBRACE)
|
840
|
-
|
841
|
-
# Parse tag values
|
842
|
-
values = []
|
843
|
-
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
844
|
-
if self._peek().type == TokenType.STRING:
|
845
|
-
value_token = self._match(TokenType.STRING)
|
846
|
-
values.append(value_token.value.strip('"').strip("'"))
|
847
|
-
else:
|
848
|
-
# Skip non-string tokens
|
849
|
-
self._advance()
|
850
|
-
|
851
|
-
if self._is_at_end():
|
852
|
-
raise create_parser_error(
|
853
|
-
message="Missing closing brace for tag declaration",
|
854
|
-
file_path=self.source_file,
|
855
|
-
line=self._peek().line,
|
856
|
-
column=self._peek().column,
|
857
|
-
line_content=self._peek().value,
|
858
|
-
suggestion="Add a closing brace (}) to match the opening brace"
|
859
|
-
)
|
860
|
-
|
861
|
-
self._match(TokenType.RBRACE)
|
862
|
-
|
863
|
-
return {"type": "tag_declaration", "tag_type": tag_type, "name": name, "values": values}
|
864
|
-
|
865
|
-
def _parse_expression(self) -> Any:
|
866
|
-
"""Parse an expression with operator precedence."""
|
867
|
-
return self._parse_addition()
|
868
|
-
|
869
|
-
def _parse_addition(self) -> Any:
|
870
|
-
"""Parse addition and subtraction."""
|
871
|
-
expr = self._parse_multiplication()
|
872
|
-
|
873
|
-
while not self._is_at_end() and self._peek().type in [TokenType.PLUS, TokenType.MINUS]:
|
874
|
-
operator = self._peek().type
|
875
|
-
self._advance() # consume operator
|
876
|
-
right = self._parse_multiplication()
|
877
|
-
expr = BinaryExpression(expr, operator, right)
|
878
|
-
|
879
|
-
return expr
|
880
|
-
|
881
|
-
def _parse_multiplication(self) -> Any:
|
882
|
-
"""Parse multiplication, division, and modulo."""
|
883
|
-
expr = self._parse_primary()
|
884
|
-
|
885
|
-
while not self._is_at_end() and self._peek().type in [TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO]:
|
886
|
-
operator = self._peek().type
|
887
|
-
self._advance() # consume operator
|
888
|
-
right = self._parse_primary()
|
889
|
-
expr = BinaryExpression(expr, operator, right)
|
890
|
-
|
891
|
-
return expr
|
892
|
-
|
893
|
-
def _parse_primary(self) -> Any:
|
894
|
-
"""Parse primary expressions (numbers, strings, variables, parenthesized expressions)."""
|
895
|
-
token = self._peek()
|
896
|
-
|
897
|
-
if token.type == TokenType.NUMBER:
|
898
|
-
self._advance()
|
899
|
-
return LiteralExpression(token.value, "number")
|
900
|
-
elif token.type == TokenType.STRING:
|
901
|
-
self._advance()
|
902
|
-
return LiteralExpression(token.value.strip('"').strip("'"), "string")
|
903
|
-
elif token.type == TokenType.VARIABLE_SUB:
|
904
|
-
self._advance()
|
905
|
-
variable_name = token.value
|
906
|
-
|
907
|
-
# Check if the variable contains a scope selector
|
908
|
-
if '<' in variable_name and variable_name.endswith('>'):
|
909
|
-
# Extract variable name and scope selector
|
910
|
-
parts = variable_name.split('<', 1)
|
911
|
-
if len(parts) == 2:
|
912
|
-
var_name = parts[0]
|
913
|
-
scope_selector = parts[1][:-1] # Remove the closing >
|
914
|
-
return VariableSubstitutionExpression(var_name, scope_selector)
|
915
|
-
|
916
|
-
# Regular variable substitution without scope
|
917
|
-
return VariableSubstitutionExpression(variable_name, None)
|
918
|
-
elif token.type == TokenType.IDENTIFIER:
|
919
|
-
identifier_name = token.value
|
920
|
-
self._advance() # consume the identifier
|
921
|
-
|
922
|
-
# Check if this identifier is followed by a scope selector
|
923
|
-
if not self._is_at_end() and self._peek().type == TokenType.LANGLE:
|
924
|
-
# This is a scoped variable - parse the scope selector
|
925
|
-
self._match(TokenType.LANGLE) # consume '<'
|
926
|
-
|
927
|
-
# Parse scope selector content
|
928
|
-
scope_parts = []
|
929
|
-
while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
|
930
|
-
scope_parts.append(self._peek().value)
|
931
|
-
self._advance()
|
932
|
-
|
933
|
-
if self._is_at_end():
|
934
|
-
raise create_parser_error(
|
935
|
-
message="Unterminated scope selector in expression",
|
936
|
-
file_path=self.source_file,
|
937
|
-
line=self._peek().line,
|
938
|
-
column=self._peek().column,
|
939
|
-
line_content=self._peek().value,
|
940
|
-
suggestion="Add a closing '>' to terminate the scope selector"
|
941
|
-
)
|
942
|
-
|
943
|
-
self._match(TokenType.RANGLE) # consume '>'
|
944
|
-
scope_selector = ''.join(scope_parts)
|
945
|
-
|
946
|
-
# Create a scoped variable expression
|
947
|
-
full_name = f"{identifier_name}<{scope_selector}>"
|
948
|
-
return VariableExpression(full_name)
|
949
|
-
|
950
|
-
# Regular variable expression without scope
|
951
|
-
return VariableExpression(identifier_name)
|
952
|
-
elif token.type == TokenType.LPAREN:
|
953
|
-
self._advance() # consume (
|
954
|
-
expr = self._parse_expression()
|
955
|
-
self._match(TokenType.RPAREN)
|
956
|
-
return expr
|
957
|
-
else:
|
958
|
-
# Unknown token - create a literal expression
|
959
|
-
self._advance()
|
960
|
-
return LiteralExpression(token.value, "unknown")
|
961
|
-
|
962
|
-
def _match(self, expected_type: TokenType) -> Token:
|
963
|
-
"""Match and consume a token of the expected type."""
|
964
|
-
if self._is_at_end():
|
965
|
-
raise create_parser_error(
|
966
|
-
message=f"Unexpected end of input, expected {expected_type}",
|
967
|
-
file_path=self.source_file,
|
968
|
-
line=self._peek().line,
|
969
|
-
column=self._peek().column,
|
970
|
-
line_content=self._peek().value,
|
971
|
-
suggestion="Check for missing tokens or incomplete statements"
|
972
|
-
)
|
973
|
-
|
974
|
-
token = self._peek()
|
975
|
-
if token.type == expected_type:
|
976
|
-
return self._advance()
|
977
|
-
else:
|
978
|
-
raise create_parser_error(
|
979
|
-
message=f"Expected {expected_type}, got {token.type}",
|
980
|
-
file_path=self.source_file,
|
981
|
-
line=token.line,
|
982
|
-
column=token.column,
|
983
|
-
line_content=token.value,
|
984
|
-
suggestion=f"Replace '{token.value}' with the expected {expected_type}"
|
985
|
-
)
|
986
|
-
|
987
|
-
def _advance(self) -> Token:
|
988
|
-
"""Advance to the next token."""
|
989
|
-
if not self._is_at_end():
|
990
|
-
self.current += 1
|
991
|
-
return self.tokens[self.current - 1]
|
992
|
-
|
993
|
-
def _peek(self) -> Token:
|
994
|
-
"""Peek at the current token."""
|
995
|
-
if self._is_at_end():
|
996
|
-
return self.tokens[-1] # Return EOF token
|
997
|
-
return self.tokens[self.current]
|
998
|
-
|
999
|
-
def _is_at_end(self) -> bool:
|
1000
|
-
"""Check if we're at the end of the tokens."""
|
1001
|
-
return self.current >= len(self.tokens)
|
1002
|
-
|
1003
|
-
def _check_for_missing_braces(self):
|
1004
|
-
"""Check for missing closing braces in the source code."""
|
1005
|
-
# This is a simple check - in a more robust implementation,
|
1006
|
-
# we would track brace matching during parsing
|
1007
|
-
# For now, we'll rely on the existing error handling in the parser
|
1008
|
-
pass
|
1009
|
-
|
1010
|
-
|
1011
|
-
def _smart_join_command_parts(parts: List[str]) -> str:
|
1012
|
-
"""Smart join command parts with proper spacing."""
|
1013
|
-
if not parts:
|
1014
|
-
return ""
|
1015
|
-
|
1016
|
-
result = parts[0]
|
1017
|
-
|
1018
|
-
for i in range(1, len(parts)):
|
1019
|
-
prev_part = parts[i - 1]
|
1020
|
-
curr_part = parts[i]
|
1021
|
-
|
1022
|
-
# Special case: don't add space when previous part ends with a namespace (like minecraft)
|
1023
|
-
# and current part starts with a colon (like :iron_ingot)
|
1024
|
-
if curr_part.startswith(':'):
|
1025
|
-
# Don't add space for namespace:item patterns
|
1026
|
-
result += curr_part
|
1027
|
-
else:
|
1028
|
-
# Add space if needed
|
1029
|
-
if (prev_part and curr_part and
|
1030
|
-
not prev_part.endswith('[') and not prev_part.endswith('{') and
|
1031
|
-
not curr_part.startswith(']') and not curr_part.startswith('}') and
|
1032
|
-
not curr_part.startswith(',') and not curr_part.startswith(':') and
|
1033
|
-
not prev_part.endswith('"') and not curr_part.startswith('"')):
|
1034
|
-
result += " "
|
1035
|
-
|
1036
|
-
# Special case: add space after 'say' before quoted string
|
1037
|
-
if prev_part == 'say' and curr_part.startswith('"'):
|
1038
|
-
result += " "
|
1039
|
-
|
1040
|
-
result += curr_part
|
1041
|
-
|
1042
|
-
return result
|
1043
|
-
|
1044
|
-
|
1045
|
-
def parse_mdl_js(source: str, source_file: str = None) -> Dict[str, Any]:
|
1046
|
-
"""Parse JavaScript-style MDL source code into AST."""
|
1047
|
-
tokens = lex_mdl_js(source, source_file)
|
1048
|
-
parser = MDLParser(tokens, source_file)
|
1049
|
-
return parser.parse()
|