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.
Files changed (27) hide show
  1. minecraft_datapack_language/__init__.py +17 -2
  2. minecraft_datapack_language/_version.py +2 -2
  3. minecraft_datapack_language/ast_nodes.py +87 -59
  4. minecraft_datapack_language/mdl_compiler.py +470 -0
  5. minecraft_datapack_language/mdl_errors.py +14 -0
  6. minecraft_datapack_language/mdl_lexer.py +624 -0
  7. minecraft_datapack_language/mdl_parser.py +573 -0
  8. minecraft_datapack_language-15.4.29.dist-info/METADATA +266 -0
  9. minecraft_datapack_language-15.4.29.dist-info/RECORD +16 -0
  10. minecraft_datapack_language/cli.py +0 -159
  11. minecraft_datapack_language/cli_build.py +0 -1292
  12. minecraft_datapack_language/cli_check.py +0 -155
  13. minecraft_datapack_language/cli_colors.py +0 -264
  14. minecraft_datapack_language/cli_help.py +0 -508
  15. minecraft_datapack_language/cli_new.py +0 -300
  16. minecraft_datapack_language/cli_utils.py +0 -276
  17. minecraft_datapack_language/expression_processor.py +0 -352
  18. minecraft_datapack_language/linter.py +0 -409
  19. minecraft_datapack_language/mdl_lexer_js.py +0 -754
  20. minecraft_datapack_language/mdl_parser_js.py +0 -1049
  21. minecraft_datapack_language/pack.py +0 -758
  22. minecraft_datapack_language-15.4.28.dist-info/METADATA +0 -1274
  23. minecraft_datapack_language-15.4.28.dist-info/RECORD +0 -25
  24. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/WHEEL +0 -0
  25. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/entry_points.txt +0 -0
  26. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/licenses/LICENSE +0 -0
  27. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.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
+ )