minecraft-datapack-language 15.4.28__py3-none-any.whl → 15.4.30__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 +23 -2
- minecraft_datapack_language/_version.py +2 -2
- minecraft_datapack_language/ast_nodes.py +87 -59
- minecraft_datapack_language/cli.py +276 -139
- 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.30.dist-info/METADATA +266 -0
- minecraft_datapack_language-15.4.30.dist-info/RECORD +17 -0
- 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.30.dist-info}/WHEEL +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/entry_points.txt +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/licenses/LICENSE +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,573 @@
|
|
1
|
+
"""
|
2
|
+
MDL Parser - Parses MDL source code into an Abstract Syntax Tree
|
3
|
+
Implements the complete language specification from language-reference.md
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import List, Optional, Dict, Any, Union
|
7
|
+
from .mdl_lexer import Token, TokenType, MDLLexer
|
8
|
+
from .mdl_errors import MDLParserError
|
9
|
+
from .ast_nodes import (
|
10
|
+
ASTNode, Program, PackDeclaration, NamespaceDeclaration, TagDeclaration,
|
11
|
+
VariableDeclaration, VariableAssignment, VariableSubstitution, FunctionDeclaration,
|
12
|
+
FunctionCall, IfStatement, WhileLoop, HookDeclaration, RawBlock,
|
13
|
+
SayCommand, TellrawCommand, ExecuteCommand, ScoreboardCommand,
|
14
|
+
BinaryExpression, UnaryExpression, ParenthesizedExpression, LiteralExpression,
|
15
|
+
ScopeSelector
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
class MDLParser:
|
20
|
+
"""
|
21
|
+
Parser for the MDL language.
|
22
|
+
|
23
|
+
Features:
|
24
|
+
- Full support for all language constructs from the specification
|
25
|
+
- Proper scope handling for variables
|
26
|
+
- Tag declarations for datapack resources
|
27
|
+
- Say command auto-conversion to tellraw
|
28
|
+
- Comprehensive error handling with context
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, source_file: str = None):
|
32
|
+
self.source_file = source_file
|
33
|
+
self.tokens: List[Token] = []
|
34
|
+
self.current = 0
|
35
|
+
self.current_namespace = "mdl"
|
36
|
+
|
37
|
+
def parse(self, source: str) -> Program:
|
38
|
+
"""
|
39
|
+
Parse MDL source code into an AST.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
source: The MDL source code string
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
Program AST node representing the complete program
|
46
|
+
|
47
|
+
Raises:
|
48
|
+
MDLParserError: If there's a parsing error
|
49
|
+
"""
|
50
|
+
# Lex the source into tokens
|
51
|
+
lexer = MDLLexer(self.source_file)
|
52
|
+
self.tokens = lexer.lex(source)
|
53
|
+
self.current = 0
|
54
|
+
|
55
|
+
# Parse the program
|
56
|
+
return self._parse_program()
|
57
|
+
|
58
|
+
def _parse_program(self) -> Program:
|
59
|
+
"""Parse the complete program."""
|
60
|
+
pack = None
|
61
|
+
namespace = None
|
62
|
+
tags = []
|
63
|
+
variables = []
|
64
|
+
functions = []
|
65
|
+
hooks = []
|
66
|
+
statements = []
|
67
|
+
|
68
|
+
while not self._is_at_end():
|
69
|
+
try:
|
70
|
+
if self._peek().type == TokenType.PACK:
|
71
|
+
pack = self._parse_pack_declaration()
|
72
|
+
elif self._peek().type == TokenType.NAMESPACE:
|
73
|
+
namespace = self._parse_namespace_declaration()
|
74
|
+
elif self._peek().type == TokenType.TAG:
|
75
|
+
tags.append(self._parse_tag_declaration())
|
76
|
+
elif self._peek().type == TokenType.VAR:
|
77
|
+
variables.append(self._parse_variable_declaration())
|
78
|
+
elif self._peek().type == TokenType.FUNCTION:
|
79
|
+
functions.append(self._parse_function_declaration())
|
80
|
+
elif self._peek().type == TokenType.ON_LOAD:
|
81
|
+
hooks.append(self._parse_hook_declaration())
|
82
|
+
elif self._peek().type == TokenType.ON_TICK:
|
83
|
+
hooks.append(self._parse_hook_declaration())
|
84
|
+
elif self._peek().type == TokenType.EXEC:
|
85
|
+
statements.append(self._parse_function_call())
|
86
|
+
elif self._peek().type == TokenType.IF:
|
87
|
+
statements.append(self._parse_if_statement())
|
88
|
+
elif self._peek().type == TokenType.WHILE:
|
89
|
+
statements.append(self._parse_while_loop())
|
90
|
+
elif self._peek().type == TokenType.DOLLAR and self._peek(1).type == TokenType.EXCLAMATION:
|
91
|
+
statements.append(self._parse_raw_block())
|
92
|
+
elif self._peek().type == TokenType.IDENTIFIER:
|
93
|
+
# Could be a variable assignment or say command
|
94
|
+
if self._peek().value == "say":
|
95
|
+
statements.append(self._parse_say_command())
|
96
|
+
else:
|
97
|
+
statements.append(self._parse_variable_assignment())
|
98
|
+
else:
|
99
|
+
# Skip unknown tokens (comments, whitespace, etc.)
|
100
|
+
self._advance()
|
101
|
+
except Exception as e:
|
102
|
+
if isinstance(e, MDLParserError):
|
103
|
+
raise e
|
104
|
+
else:
|
105
|
+
self._error(f"Unexpected error during parsing: {str(e)}", "Check the syntax")
|
106
|
+
|
107
|
+
return Program(
|
108
|
+
pack=pack,
|
109
|
+
namespace=namespace,
|
110
|
+
tags=tags,
|
111
|
+
variables=variables,
|
112
|
+
functions=functions,
|
113
|
+
hooks=hooks,
|
114
|
+
statements=statements
|
115
|
+
)
|
116
|
+
|
117
|
+
def _parse_pack_declaration(self) -> PackDeclaration:
|
118
|
+
"""Parse pack declaration: pack "name" "description" format;"""
|
119
|
+
self._expect(TokenType.PACK, "Expected 'pack' keyword")
|
120
|
+
|
121
|
+
self._expect(TokenType.QUOTE, "Expected opening quote for pack name")
|
122
|
+
name = self._expect_identifier("Expected pack name")
|
123
|
+
self._expect(TokenType.QUOTE, "Expected closing quote for pack name")
|
124
|
+
|
125
|
+
self._expect(TokenType.QUOTE, "Expected opening quote for pack description")
|
126
|
+
description = self._expect_identifier("Expected pack description")
|
127
|
+
self._expect(TokenType.QUOTE, "Expected closing quote for pack description")
|
128
|
+
|
129
|
+
self._expect(TokenType.NUMBER, "Expected pack format number")
|
130
|
+
pack_format = int(self._previous().value)
|
131
|
+
|
132
|
+
self._expect(TokenType.SEMICOLON, "Expected semicolon after pack declaration")
|
133
|
+
|
134
|
+
return PackDeclaration(name=name, description=description, pack_format=pack_format)
|
135
|
+
|
136
|
+
def _parse_namespace_declaration(self) -> NamespaceDeclaration:
|
137
|
+
"""Parse namespace declaration: namespace "name";"""
|
138
|
+
self._expect(TokenType.NAMESPACE, "Expected 'namespace' keyword")
|
139
|
+
|
140
|
+
self._expect(TokenType.QUOTE, "Expected opening quote for namespace name")
|
141
|
+
name = self._expect_identifier("Expected namespace name")
|
142
|
+
self._expect(TokenType.QUOTE, "Expected closing quote for namespace name")
|
143
|
+
|
144
|
+
self._expect(TokenType.SEMICOLON, "Expected semicolon after namespace declaration")
|
145
|
+
|
146
|
+
# Update current namespace
|
147
|
+
self.current_namespace = name
|
148
|
+
|
149
|
+
return NamespaceDeclaration(name=name)
|
150
|
+
|
151
|
+
def _parse_tag_declaration(self) -> TagDeclaration:
|
152
|
+
"""Parse tag declaration: tag type "name" "path";"""
|
153
|
+
self._expect(TokenType.TAG, "Expected 'tag' keyword")
|
154
|
+
|
155
|
+
# Parse tag type
|
156
|
+
tag_type = self._parse_tag_type()
|
157
|
+
|
158
|
+
self._expect(TokenType.QUOTE, "Expected opening quote for tag name")
|
159
|
+
name = self._expect_identifier("Expected tag name")
|
160
|
+
self._expect(TokenType.QUOTE, "Expected closing quote for tag name")
|
161
|
+
|
162
|
+
self._expect(TokenType.QUOTE, "Expected opening quote for file path")
|
163
|
+
file_path = self._expect_identifier("Expected file path")
|
164
|
+
self._expect(TokenType.QUOTE, "Expected closing quote for file path")
|
165
|
+
|
166
|
+
self._expect(TokenType.SEMICOLON, "Expected semicolon after tag declaration")
|
167
|
+
|
168
|
+
return TagDeclaration(tag_type=tag_type, name=name, file_path=file_path)
|
169
|
+
|
170
|
+
def _parse_tag_type(self) -> str:
|
171
|
+
"""Parse tag type (recipe, loot_table, etc.)."""
|
172
|
+
token = self._peek()
|
173
|
+
if token.type in [TokenType.RECIPE, TokenType.LOOT_TABLE, TokenType.ADVANCEMENT,
|
174
|
+
TokenType.ITEM_MODIFIER, TokenType.PREDICATE, TokenType.STRUCTURE]:
|
175
|
+
self._advance()
|
176
|
+
return token.value
|
177
|
+
else:
|
178
|
+
self._error(f"Expected tag type, got {token.value}",
|
179
|
+
"Use: recipe, loot_table, advancement, item_modifier, predicate, or structure")
|
180
|
+
|
181
|
+
def _parse_variable_declaration(self) -> VariableDeclaration:
|
182
|
+
"""Parse variable declaration: var num name<scope> = value;"""
|
183
|
+
self._expect(TokenType.VAR, "Expected 'var' keyword")
|
184
|
+
|
185
|
+
self._expect(TokenType.NUM, "Expected 'num' keyword")
|
186
|
+
|
187
|
+
name = self._expect_identifier("Expected variable name")
|
188
|
+
|
189
|
+
scope = self._parse_scope_selector()
|
190
|
+
|
191
|
+
self._expect(TokenType.ASSIGN, "Expected '=' after variable declaration")
|
192
|
+
|
193
|
+
initial_value = self._parse_expression()
|
194
|
+
|
195
|
+
self._expect(TokenType.SEMICOLON, "Expected semicolon after variable declaration")
|
196
|
+
|
197
|
+
return VariableDeclaration(
|
198
|
+
var_type="num",
|
199
|
+
name=name,
|
200
|
+
scope=scope,
|
201
|
+
initial_value=initial_value
|
202
|
+
)
|
203
|
+
|
204
|
+
def _parse_variable_assignment(self) -> VariableAssignment:
|
205
|
+
"""Parse variable assignment: name<scope> = value;"""
|
206
|
+
name = self._expect_identifier("Expected variable name")
|
207
|
+
|
208
|
+
scope = self._parse_scope_selector()
|
209
|
+
|
210
|
+
self._expect(TokenType.ASSIGN, "Expected '=' after variable name")
|
211
|
+
|
212
|
+
value = self._parse_expression()
|
213
|
+
|
214
|
+
self._expect(TokenType.SEMICOLON, "Expected semicolon after variable assignment")
|
215
|
+
|
216
|
+
return VariableAssignment(name=name, scope=scope, value=value)
|
217
|
+
|
218
|
+
def _parse_scope_selector(self) -> str:
|
219
|
+
"""Parse scope selector: <@s>, <@a[team=red]>, etc."""
|
220
|
+
self._expect(TokenType.LANGLE, "Expected '<' for scope selector")
|
221
|
+
|
222
|
+
# Parse the selector content
|
223
|
+
selector_content = ""
|
224
|
+
while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
|
225
|
+
selector_content += self._peek().value
|
226
|
+
self._advance()
|
227
|
+
|
228
|
+
self._expect(TokenType.RANGLE, "Expected '>' to close scope selector")
|
229
|
+
|
230
|
+
return f"<{selector_content}>"
|
231
|
+
|
232
|
+
def _parse_function_declaration(self) -> FunctionDeclaration:
|
233
|
+
"""Parse function declaration: function namespace:name<scope> { body }"""
|
234
|
+
self._expect(TokenType.FUNCTION, "Expected 'function' keyword")
|
235
|
+
|
236
|
+
# Parse namespace:name
|
237
|
+
namespace = self._expect_identifier("Expected namespace")
|
238
|
+
self._expect(TokenType.COLON, "Expected ':' after namespace")
|
239
|
+
name = self._expect_identifier("Expected function name")
|
240
|
+
|
241
|
+
# Parse optional scope
|
242
|
+
scope = None
|
243
|
+
if self._peek().type == TokenType.LANGLE:
|
244
|
+
scope = self._parse_scope_selector()
|
245
|
+
|
246
|
+
self._expect(TokenType.LBRACE, "Expected '{' to start function body")
|
247
|
+
|
248
|
+
body = self._parse_block()
|
249
|
+
|
250
|
+
self._expect(TokenType.RBRACE, "Expected '}' to end function body")
|
251
|
+
|
252
|
+
return FunctionDeclaration(
|
253
|
+
namespace=namespace,
|
254
|
+
name=name,
|
255
|
+
scope=scope,
|
256
|
+
body=body
|
257
|
+
)
|
258
|
+
|
259
|
+
def _parse_function_call(self) -> FunctionCall:
|
260
|
+
"""Parse function call: exec namespace:name<scope>;"""
|
261
|
+
self._expect(TokenType.EXEC, "Expected 'exec' keyword")
|
262
|
+
|
263
|
+
# Parse namespace:name
|
264
|
+
namespace = self._expect_identifier("Expected namespace")
|
265
|
+
self._expect(TokenType.COLON, "Expected ':' after namespace")
|
266
|
+
name = self._expect_identifier("Expected function name")
|
267
|
+
|
268
|
+
# Parse optional scope
|
269
|
+
scope = None
|
270
|
+
if self._peek().type == TokenType.LANGLE:
|
271
|
+
scope = self._parse_scope_selector()
|
272
|
+
|
273
|
+
self._expect(TokenType.SEMICOLON, "Expected semicolon after function call")
|
274
|
+
|
275
|
+
return FunctionCall(
|
276
|
+
namespace=namespace,
|
277
|
+
name=name,
|
278
|
+
scope=scope
|
279
|
+
)
|
280
|
+
|
281
|
+
def _parse_if_statement(self) -> IfStatement:
|
282
|
+
"""Parse if statement: if condition { then_body } else { else_body } or else if { ... }"""
|
283
|
+
self._expect(TokenType.IF, "Expected 'if' keyword")
|
284
|
+
|
285
|
+
condition = self._parse_expression()
|
286
|
+
|
287
|
+
self._expect(TokenType.LBRACE, "Expected '{' to start if body")
|
288
|
+
then_body = self._parse_block()
|
289
|
+
self._expect(TokenType.RBRACE, "Expected '}' to end if body")
|
290
|
+
|
291
|
+
# Parse optional else clause
|
292
|
+
else_body = None
|
293
|
+
if self._peek().type == TokenType.ELSE:
|
294
|
+
self._advance() # consume 'else'
|
295
|
+
|
296
|
+
# Check if this is an else if
|
297
|
+
if self._peek().type == TokenType.IF:
|
298
|
+
# This is an else if - parse it as a nested if statement
|
299
|
+
else_body = [self._parse_if_statement()]
|
300
|
+
else:
|
301
|
+
# This is a regular else
|
302
|
+
self._expect(TokenType.LBRACE, "Expected '{' to start else body")
|
303
|
+
else_body = self._parse_block()
|
304
|
+
self._expect(TokenType.RBRACE, "Expected '}' to end else body")
|
305
|
+
|
306
|
+
return IfStatement(
|
307
|
+
condition=condition,
|
308
|
+
then_body=then_body,
|
309
|
+
else_body=else_body
|
310
|
+
)
|
311
|
+
|
312
|
+
def _parse_while_loop(self) -> WhileLoop:
|
313
|
+
"""Parse while loop: while condition { body }"""
|
314
|
+
self._expect(TokenType.WHILE, "Expected 'while' keyword")
|
315
|
+
|
316
|
+
condition = self._parse_expression()
|
317
|
+
|
318
|
+
self._expect(TokenType.LBRACE, "Expected '{' to start while body")
|
319
|
+
body = self._parse_block()
|
320
|
+
self._expect(TokenType.RBRACE, "Expected '}' to end while body")
|
321
|
+
|
322
|
+
return WhileLoop(condition=condition, body=body)
|
323
|
+
|
324
|
+
def _parse_hook_declaration(self) -> HookDeclaration:
|
325
|
+
"""Parse hook declaration: on_load/on_tick namespace:name<scope>;"""
|
326
|
+
hook_type = self._peek().value
|
327
|
+
self._advance() # consume on_load or on_tick
|
328
|
+
|
329
|
+
# Parse namespace:name
|
330
|
+
namespace = self._expect_identifier("Expected namespace")
|
331
|
+
self._expect(TokenType.COLON, "Expected ':' after namespace")
|
332
|
+
name = self._expect_identifier("Expected function name")
|
333
|
+
|
334
|
+
# Parse optional scope
|
335
|
+
scope = None
|
336
|
+
if self._peek().type == TokenType.LANGLE:
|
337
|
+
scope = self._parse_scope_selector()
|
338
|
+
|
339
|
+
self._expect(TokenType.SEMICOLON, "Expected semicolon after hook declaration")
|
340
|
+
|
341
|
+
return HookDeclaration(
|
342
|
+
hook_type=hook_type,
|
343
|
+
namespace=namespace,
|
344
|
+
name=name,
|
345
|
+
scope=scope
|
346
|
+
)
|
347
|
+
|
348
|
+
def _parse_raw_block(self) -> RawBlock:
|
349
|
+
"""Parse raw block: $!raw ... raw!$"""
|
350
|
+
# Consume $!raw
|
351
|
+
self._expect(TokenType.DOLLAR, "Expected '$' to start raw block")
|
352
|
+
self._expect(TokenType.EXCLAMATION, "Expected '!' after '$' in raw block")
|
353
|
+
self._expect(TokenType.IDENTIFIER, "Expected 'raw' keyword")
|
354
|
+
|
355
|
+
# Look for RAW_CONTENT token
|
356
|
+
if self._peek().type == TokenType.RAW_CONTENT:
|
357
|
+
content = self._peek().value
|
358
|
+
self._advance()
|
359
|
+
else:
|
360
|
+
content = ""
|
361
|
+
|
362
|
+
# Consume raw!$ end marker
|
363
|
+
self._expect(TokenType.IDENTIFIER, "Expected 'raw' to end raw block")
|
364
|
+
self._expect(TokenType.EXCLAMATION, "Expected '!' to end raw block")
|
365
|
+
self._expect(TokenType.DOLLAR, "Expected '$' to end raw block")
|
366
|
+
|
367
|
+
return RawBlock(content=content)
|
368
|
+
|
369
|
+
def _parse_say_command(self) -> SayCommand:
|
370
|
+
"""Parse say command: say "message with $variable<scope>$";"""
|
371
|
+
self._expect(TokenType.IDENTIFIER, "Expected 'say' keyword")
|
372
|
+
|
373
|
+
self._expect(TokenType.QUOTE, "Expected opening quote for say message")
|
374
|
+
|
375
|
+
# Get the string content (which includes variable substitutions)
|
376
|
+
if self._peek().type == TokenType.IDENTIFIER:
|
377
|
+
message = self._peek().value
|
378
|
+
self._advance()
|
379
|
+
else:
|
380
|
+
message = ""
|
381
|
+
|
382
|
+
# Extract variables from the message content
|
383
|
+
variables = []
|
384
|
+
# Simple regex-like extraction of $variable<scope>$ patterns
|
385
|
+
import re
|
386
|
+
var_pattern = r'\$([a-zA-Z_][a-zA-Z0-9_]*<[^>]+>)\$'
|
387
|
+
matches = re.findall(var_pattern, message)
|
388
|
+
|
389
|
+
for match in matches:
|
390
|
+
# Parse the variable name and scope from the match
|
391
|
+
if '<' in match and '>' in match:
|
392
|
+
name = match[:match.index('<')]
|
393
|
+
scope = match[match.index('<'):match.index('>')+1]
|
394
|
+
variables.append(VariableSubstitution(name=name, scope=scope))
|
395
|
+
|
396
|
+
self._expect(TokenType.QUOTE, "Expected closing quote for say message")
|
397
|
+
self._expect(TokenType.SEMICOLON, "Expected semicolon after say command")
|
398
|
+
|
399
|
+
return SayCommand(message=message, variables=variables)
|
400
|
+
|
401
|
+
def _parse_variable_substitution(self) -> VariableSubstitution:
|
402
|
+
"""Parse variable substitution: $variable<scope>$"""
|
403
|
+
self._expect(TokenType.DOLLAR, "Expected '$' to start variable substitution")
|
404
|
+
|
405
|
+
name = self._expect_identifier("Expected variable name")
|
406
|
+
|
407
|
+
# For variable substitutions, use LANGLE/RANGLE
|
408
|
+
self._expect(TokenType.LANGLE, "Expected '<' for scope selector")
|
409
|
+
|
410
|
+
# Parse the selector content
|
411
|
+
selector_content = ""
|
412
|
+
while not self._is_at_end() and self._peek().type != TokenType.RANGLE:
|
413
|
+
selector_content += self._peek().value
|
414
|
+
self._advance()
|
415
|
+
|
416
|
+
self._expect(TokenType.RANGLE, "Expected '>' to close scope selector")
|
417
|
+
|
418
|
+
scope = f"<{selector_content}>"
|
419
|
+
|
420
|
+
self._expect(TokenType.DOLLAR, "Expected '$' to end variable substitution")
|
421
|
+
|
422
|
+
return VariableSubstitution(name=name, scope=scope)
|
423
|
+
|
424
|
+
def _parse_expression(self) -> Any:
|
425
|
+
"""Parse an expression with operator precedence."""
|
426
|
+
return self._parse_comparison()
|
427
|
+
|
428
|
+
def _parse_comparison(self) -> Any:
|
429
|
+
"""Parse comparison expressions (>, <, >=, <=, ==, !=)."""
|
430
|
+
expr = self._parse_term()
|
431
|
+
|
432
|
+
while not self._is_at_end() and self._peek().type in [
|
433
|
+
TokenType.GREATER, TokenType.LESS, TokenType.GREATER_EQUAL,
|
434
|
+
TokenType.LESS_EQUAL, TokenType.EQUAL, TokenType.NOT_EQUAL
|
435
|
+
]:
|
436
|
+
operator = self._peek().type
|
437
|
+
self._advance()
|
438
|
+
right = self._parse_term()
|
439
|
+
expr = BinaryExpression(left=expr, operator=operator, right=right)
|
440
|
+
|
441
|
+
return expr
|
442
|
+
|
443
|
+
def _parse_term(self) -> Any:
|
444
|
+
"""Parse addition and subtraction terms."""
|
445
|
+
expr = self._parse_factor()
|
446
|
+
|
447
|
+
while not self._is_at_end() and self._peek().type in [TokenType.PLUS, TokenType.MINUS]:
|
448
|
+
operator = self._peek().type
|
449
|
+
self._advance()
|
450
|
+
right = self._parse_factor()
|
451
|
+
expr = BinaryExpression(left=expr, operator=operator, right=right)
|
452
|
+
|
453
|
+
return expr
|
454
|
+
|
455
|
+
def _parse_factor(self) -> Any:
|
456
|
+
"""Parse multiplication and division factors."""
|
457
|
+
expr = self._parse_primary()
|
458
|
+
|
459
|
+
while not self._is_at_end() and self._peek().type in [TokenType.MULTIPLY, TokenType.DIVIDE]:
|
460
|
+
operator = self._peek().type
|
461
|
+
self._advance()
|
462
|
+
right = self._parse_primary()
|
463
|
+
expr = BinaryExpression(left=expr, operator=operator, right=right)
|
464
|
+
|
465
|
+
return expr
|
466
|
+
|
467
|
+
def _parse_primary(self) -> Any:
|
468
|
+
"""Parse primary expressions (literals, variables, parenthesized expressions)."""
|
469
|
+
if self._peek().type == TokenType.DOLLAR:
|
470
|
+
return self._parse_variable_substitution()
|
471
|
+
elif self._peek().type == TokenType.NUMBER:
|
472
|
+
value = float(self._peek().value)
|
473
|
+
self._advance()
|
474
|
+
return LiteralExpression(value=value, type="number")
|
475
|
+
elif self._peek().type == TokenType.QUOTE:
|
476
|
+
self._advance() # consume opening quote
|
477
|
+
value = self._expect_identifier("Expected string content")
|
478
|
+
self._expect(TokenType.QUOTE, "Expected closing quote")
|
479
|
+
return LiteralExpression(value=value, type="string")
|
480
|
+
elif self._peek().type == TokenType.LPAREN:
|
481
|
+
self._advance() # consume opening parenthesis
|
482
|
+
expr = self._parse_expression()
|
483
|
+
self._expect(TokenType.RPAREN, "Expected closing parenthesis")
|
484
|
+
return ParenthesizedExpression(expression=expr)
|
485
|
+
else:
|
486
|
+
# Simple identifier
|
487
|
+
value = self._expect_identifier("Expected expression")
|
488
|
+
return LiteralExpression(value=value, type="identifier")
|
489
|
+
|
490
|
+
def _parse_block(self) -> List[ASTNode]:
|
491
|
+
"""Parse a block of statements."""
|
492
|
+
statements = []
|
493
|
+
|
494
|
+
while not self._is_at_end() and self._peek().type != TokenType.RBRACE:
|
495
|
+
if self._peek().type == TokenType.IF:
|
496
|
+
statements.append(self._parse_if_statement())
|
497
|
+
elif self._peek().type == TokenType.WHILE:
|
498
|
+
statements.append(self._parse_while_loop())
|
499
|
+
elif self._peek().type == TokenType.EXEC:
|
500
|
+
statements.append(self._parse_function_call())
|
501
|
+
elif self._peek().type == TokenType.DOLLAR and self._peek(1).type == TokenType.EXCLAMATION:
|
502
|
+
statements.append(self._parse_raw_block())
|
503
|
+
elif self._peek().type == TokenType.IDENTIFIER:
|
504
|
+
if self._peek().value == "say":
|
505
|
+
statements.append(self._parse_say_command())
|
506
|
+
else:
|
507
|
+
statements.append(self._parse_variable_assignment())
|
508
|
+
else:
|
509
|
+
# Skip unknown tokens
|
510
|
+
self._advance()
|
511
|
+
|
512
|
+
return statements
|
513
|
+
|
514
|
+
# Helper methods
|
515
|
+
def _advance(self) -> Token:
|
516
|
+
"""Advance to next token and return the previous one."""
|
517
|
+
if not self._is_at_end():
|
518
|
+
self.current += 1
|
519
|
+
return self._previous()
|
520
|
+
|
521
|
+
def _peek(self, offset: int = 0) -> Token:
|
522
|
+
"""Peek ahead by offset tokens."""
|
523
|
+
if self.current + offset >= len(self.tokens):
|
524
|
+
return self.tokens[-1] # Return EOF token
|
525
|
+
return self.tokens[self.current + offset]
|
526
|
+
|
527
|
+
def _previous(self) -> Token:
|
528
|
+
"""Get the previous token."""
|
529
|
+
return self.tokens[self.current - 1]
|
530
|
+
|
531
|
+
def _is_at_end(self) -> bool:
|
532
|
+
"""Check if we're at the end of tokens."""
|
533
|
+
return self.current >= len(self.tokens) or self._peek().type == TokenType.EOF
|
534
|
+
|
535
|
+
def _expect(self, token_type: str, message: str) -> Token:
|
536
|
+
"""Expect a specific token type and return it."""
|
537
|
+
if self._is_at_end():
|
538
|
+
self._error(f"Unexpected end of file, {message}", "Add the missing token")
|
539
|
+
|
540
|
+
if self._peek().type == token_type:
|
541
|
+
return self._advance()
|
542
|
+
else:
|
543
|
+
self._error(f"Expected {token_type}, got {self._peek().type}", message)
|
544
|
+
|
545
|
+
def _expect_identifier(self, message: str) -> str:
|
546
|
+
"""Expect an identifier token and return its value."""
|
547
|
+
token = self._peek()
|
548
|
+
if token.type == TokenType.IDENTIFIER:
|
549
|
+
self._advance()
|
550
|
+
return token.value
|
551
|
+
else:
|
552
|
+
self._error(f"Expected identifier, got {token.type}", message)
|
553
|
+
|
554
|
+
def _error(self, message: str, suggestion: str):
|
555
|
+
"""Raise a parser error with context."""
|
556
|
+
if self._is_at_end():
|
557
|
+
line = 1
|
558
|
+
column = 1
|
559
|
+
line_content = "end of file"
|
560
|
+
else:
|
561
|
+
token = self._peek()
|
562
|
+
line = token.line
|
563
|
+
column = token.column
|
564
|
+
line_content = token.value
|
565
|
+
|
566
|
+
raise MDLParserError(
|
567
|
+
message=message,
|
568
|
+
file_path=self.source_file,
|
569
|
+
line=line,
|
570
|
+
column=column,
|
571
|
+
line_content=line_content,
|
572
|
+
suggestion=suggestion
|
573
|
+
)
|