Language Reference
MDL (Minecraft Datapack Language) - Complete Language Reference
MDL is a simple, scope-aware language that compiles to Minecraft datapack .mcfunction
files. This document defines the complete language specification.
Core Language Design
Philosophy
- Explicit scoping: Variables support explicit
<scope>
; if omitted,@s
(current entity) is assumed - Clear reading vs writing: Use
$variable<scope>$
or$variable$
for reading, andvariable<scope>
orvariable
for writing - No scope inheritance: Each operation uses its own explicitly defined scope (or defaults to
@s
when omitted) - Default scope: When no scope specified, always use
@s
(current entity) - No return values: All functions are void - they execute commands and modify state
- No quotes needed: Use
$variable<scope>$
syntax directly instead of string literals - Function execution: Use
exec
keyword to execute all functions - Tag-based resources: Use tag syntax to reference datapack resources like recipes, loot tables, etc.
- User-friendly communication:
say
commands automatically convert totellraw
with proper JSON formatting - Real control flow: If/else if/else statements and while loops that actually work and generate proper Minecraft conditional logic
Basic Syntax
Pack Declaration
pack "pack_name" "description" pack_format;
Namespace Declaration
namespace "namespace_name";
Tag Declarations
// Recipe tags
tag recipe "RecipeName" "path/to/recipe.json";
tag recipe "diamond_sword" "recipes/diamond_sword.json";
// Loot table tags
tag loot_table "LootTableName" "path/to/loot_table.json";
tag loot_table "epic_loot" "loot_tables/epic_loot.json";
// Advancement tags
tag advancement "AdvancementName" "path/to/advancement.json";
tag advancement "first_spell" "advancements/first_spell.json";
// Item modifier tags
tag item_modifier "ItemModifierName" "path/to/item_modifier.json";
tag item_modifier "enchant_tool" "item_modifiers/enchant_tool.json";
// Predicate tags
tag predicate "PredicateName" "path/to/predicate.json";
tag predicate "has_mana" "predicates/has_mana.json";
// Structure tags
tag structure "StructureName" "path/to/structure.json";
tag structure "custom_house" "structures/custom_house.json";
Variable Declaration
// Declare variables (scope optional; defaults to @s)
var num player_score<@a> = 0; // Global scope - accessible by all players
var num player_health<@s> = 20; // Player-specific scope
var num player_health = 20; // Same as player_health<@s> = 20
var num team_score<@a[team=red]> = 0; // Team scope
var num entity_data<@e[type=armor_stand,tag=mdl_global,limit=1]> = 0; // Custom entity scope
Variable Assignment
// Scope optional; defaults to @s for both reads and writes when omitted
player_score<@s> = $player_score<@s>$ + 1; // Add 1 to current player's score
player_health<@a> = $player_health<@s>$; // Read from @s, write to @a
team_score<@a[team=red]> = 5; // Set red team score to 5
// Default scope is @s when not specified
player_score = 0; // Same as player_score<@s> = 0;
Variable Substitution
// Use $variable<scope>$ or $variable$ anywhere in the code
// $variable$ defaults to <@s>
tellraw @s {"text":"You have ","extra":[{"score":{"name":"@s","objective":"player_score"}}," points"]};
tellraw @s {"text":"You have ","extra":[{"score":{"name":"@s","objective":"player_score"}}," points"]}; // $player_score$
execute if score @s player_score matches 10.. run game:celebrate;
// In conditions
if $player_score$ > 10 {
player_score = 0; // defaults to <@s>
}
Say Commands (Auto-converted to tellraw)
// Simple say commands automatically convert to tellraw with JSON formatting
say "Welcome to the game!";
say "You have $player_score<@s>$ points!";
say "Team score: $team_score<@a[team=red]>$";
// These get converted to:
// tellraw @a {"text":"Welcome to the game!"};
// tellraw @a {"text":"You have ","extra":[{"score":{"name":"@s","objective":"player_score"}}," points!"]};
// tellraw @a {"text":"Team score: ","extra":[{"score":{"name":"@s","objective":"team_score"}}]};
Functions
Function Declaration
// Basic function
function game:start_game {
player_score<@s> = 0;
player_health<@s> = 20;
}
// Function declaration (no scope on definition)
function game:reset_player {
player_score<@s> = 0;
player_health<@s> = 20;
}
Function Calls
// Execute function with exec keyword (runs any function, with or without scope)
exec game:reset_player; // Execute function
exec game:start_game; // Execute any function
exec utils:calculator; // Execute from different namespace
exec game:reset_player<@s>; // Execute function with scope
exec game:reset_player<@a>; // Execute function with different scope
// Function Macros (Minecraft snapshot): pass macro arguments
// Inline JSON compound as a single-quoted string to minimize escapes
exec game:spawn_mob '{id:"minecraft:cow",name:"Betsy"}';
// With-clause to pull a compound from a data source
exec game:spawn_mob with storage mymod:ctx path.to.compound;
Exec and Scope Execution Rules
exec ns:name
runsfunction ns:name
in the current executor context.exec ns:name<selector>
compiles toexecute as <selector> run function ns:name
.- Macro args compile to
function ns:name {json}
form; with-clause compiles tofunction ns:name with <data source and path>
.
Control Structures
If Statements
if $player_score<@s>$ > 10 {
exec game:celebrate;
player_score<@s> = 0;
}
if $player_health<@s>$ < 5 {
exec game:heal;
} else {
exec game:check_health;
}
Else If Statements
if $player_score<@s>$ > 100 {
exec game:celebrate;
player_score<@s> = 0;
} else if $player_score<@s>$ > 50 {
exec game:reward;
player_score<@s> = $player_score<@s>$ + 10;
} else {
exec game:encourage;
player_score<@s>$ = $player_score<@s>$ + 5;
}
While Loops
while $counter<@s>$ > 0 {
counter<@s> = $counter<@s>$ - 1;
exec game:countdown;
}
Note: The standard while
loop uses recursive function calls internally. This is simple and fast but can hit Minecraft’s function call depth limit for very long-running loops.
Scheduled While Loops
Use scheduledwhile
to iterate via the scheduler instead of recursion. This avoids recursion limits by running one iteration per game tick and scheduling the next iteration only if the condition remains true.
scheduledwhile $counter<@s>$ > 0 {
counter<@s> = $counter<@s>$ - 1;
exec game:countdown;
}
Compilation strategy:
- Generates a helper function containing the loop body
- At the end of the helper, emits
execute if <condition> run schedule function <helper> 1t
- Entry point schedules the first iteration with
schedule function <helper> 1t
- Breakout occurs naturally when the condition becomes false (no re-schedule)
When to use:
- Prefer
while
for short/medium loops - Prefer
scheduledwhile
for long-running loops, per-tick processes, or when avoiding recursion depth limits
Hooks
on_load game:start_game; // Runs when datapack loads
on_tick game:update_timer; // Runs every tick
Note: Hooks use the same function reference syntax as regular function calls, but they are processed at datapack load time, not during execution.
Raw Blocks
// Raw blocks pass through unchanged - no MDL processing
$!raw
scoreboard players set @s player_timer_enabled 1
execute as @a run function game:increase_tick_per_player
say "Raw commands bypass MDL syntax checking"
raw!$
// Single-line raw commands
$!raw scoreboard players add @s player_tick_counter 1 raw!$
// Raw blocks can contain any Minecraft commands, including complex execute chains
$!raw
execute as @a[team=red] at @s run particle minecraft:explosion ~ ~ ~ 1 1 1 0 10
execute as @a[team=blue] at @s run playsound minecraft:entity.player.levelup player @s ~ ~ ~ 1 1
raw!$
Macro Lines
// Lines starting with $ are emitted as-is into the generated .mcfunction and
// can contain $(variable) placeholders that Minecraft will substitute when the
// function is called with a macro compound.
$summon minecraft:cow ~ ~ ~ {CustomName:'{"text":"$(name)"}'}
Important: Raw blocks are completely ignored by the MDL parser. They get copied directly to the output .mcfunction
files without any processing. This means you can use any valid Minecraft command syntax inside raw blocks.
Scope System
Core Scope Rules
- Variable Writing: Use
variable<scope>
for assignments and declarations;variable
defaults to<@s>
- Variable Reading: Use
$variable<scope>$
for reading values;$variable$
defaults to<@s>
- Function Execution: Use
exec
keyword to run any function (with or without scope) - No Inheritance: Functions do not inherit scope from their caller
- Default Scope: When no scope specified, always use
@s
(current entity) - No Memory: The system does not remember a variable’s declared scope for subsequent operations
Scope Usage Examples
// VARIABLES: Clear distinction between reading and writing
var num score<@a> = 0; // Declare with scope
score<@s> = 5; // Write with scope
if $score<@a>$ > 10 { ... } // Read with scope
// FUNCTIONS: Use exec keyword to run any function (with or without scope)
exec game:start; // Execute function
exec utils:helper; // Execute from different namespace
exec game:start<@a>; // Execute function with scope
Scope Examples
// Declare variable with global scope
var num global_counter<@a> = 0;
// Later operations - each specifies its own scope
global_counter<@s> = 5; // Set current player's counter to 5
global_counter<@a> = $global_counter<@a>$ + 1; // Increment global counter
global_counter = 10; // Same as global_counter<@s> = 10 (defaults to @s)
say "Player has $global_counter$ points"; // $global_counter$ defaults to <@s>
// Function calls
exec game:increment; // Execute function
exec game:increment<@s>; // Execute function with scope
exec utils:helper; // Execute from different namespace
Valid Scope Selectors
// Basic selectors
<@s> // Current player
<@a> // All players
<@p> // Nearest player
<@r> // Random player
// Complex selectors
<@a[team=red]> // Red team players
<@e[type=armor_stand,tag=mdl_global,limit=1]> // Specific entity
<@s[distance=..5]> // Current player within 5 blocks
// Global scope (special case)
<global> // Maps to @e[type=armor_stand,tag=mdl_global,limit=1]
// A single invisible armor stand with tag 'mdl_global' is ensured on load
Mathematical Expressions
Operators
// Arithmetic
+ (addition)
- (subtraction)
* (multiplication)
/ (division)
// Comparison
== (equal)
!= (not equal)
> (greater than)
< (less than)
>= (greater than or equal)
<= (less than or equal)
// Logical
&& (logical AND)
|| (logical OR)
! (logical NOT)
// Range (for matches)
.. (range operator)
Unary Operators and Precedence
- Unary minus:
-x
applies before multiplication/division and addition/subtraction. Literals are constant-folded; non-literals are compiled as0 - x
via a temp score. - Logical NOT:
!expr
negates a boolean expression. For comparisons like!$a$ > 0
, the comparison is compiled first, then inverted usingexecute unless
. - Precedence (lowest to highest):
1)
||
2)&&
3) Comparisons (>
,>=
,<
,<=
,==
,!=
) 4)+
,-
5)*
,/
6) Unary (!
, unary-
) 7) Parentheses(...)
Expression Examples
// Complex expressions with different scopes
player_score<@s> = $x<@a>$ + $y<@p>$ * $z<@r>$;
// Parentheses for precedence
player_score<@s> = ($x<@s>$ + $y<@s>$) * 2;
// Comparisons
if $score<@s>$ > 10 {
exec game:reward;
}
// Logical operators
if $a<@s>$ > 0 && $b<@s>$ > 0 {
say "Both are greater than 0";
}
if $a<@s>$ > 0 || $b<@s>$ > 0 {
say "At least one is greater than 0";
}
// NOT negates the entire comparison when used like: !$a$ > 0
if !$a$ > 0 {
say "a is not greater than 0";
}
// Complex logical expression with parentheses
if ($a$ > 0 && $b$ > 0) || $c$ > 0 {
say "Condition satisfied";
}
// Unary minus with literals and variables
var num t = -2;
if ($x$ + -($y$ * 3)) >= -5 { say "ok"; }
Reserved Names
Function Names to Avoid
load
- Conflicts with Minecraft’s built-in load functiontick
- Conflicts with Minecraft’s built-in tick function- Any other names that might conflict with Minecraft’s internal functions
Alternative Naming
// Instead of 'load', use:
function game:initialize { ... }
function game:setup { ... }
function game:start { ... }
// Instead of 'tick', use:
function game:update { ... }
function game:loop { ... }
function game:process { ... }
Complete Examples
Basic Counter with Tags
pack "counter" "Counter example" 82;
namespace "counter";
// Tag declarations
tag recipe "diamond_sword" "recipes/diamond_sword.json";
tag loot_table "sword_loot" "loot_tables/sword_loot.json";
tag advancement "first_sword" "advancements/first_sword.json";
var num global_counter<@a> = 0;
var num player_counter<@s> = 0;
function "increment" {
global_counter<@a> = $global_counter<@a>$ + 1;
player_counter<@s> = $player_counter<@s>$ + 1;
// Using tellraw for player-specific messages
tellraw @s {"text":"Global: ","extra":[{"score":{"name":"@s","objective":"global_counter"}}," Player: ",{"score":{"name":"@s","objective":"player_counter"}}]};
// Using say for broadcast messages (auto-converts to tellraw)
say "Player $player_counter<@s>$ just incremented the counter!";
}
function "reset_player" {
player_counter<@s> = 0;
tellraw @s {"text":"Counter reset!"};
}
on_load "counter:increment";
Team Game with Resources
pack "teamgame" "Team game example" 82;
namespace "teamgame";
// Tag declarations
tag recipe "team_banner" "recipes/team_banner.json";
tag loot_table "team_reward" "loot_tables/team_reward.json";
tag advancement "team_win" "advancements/team_win.json";
tag item_modifier "team_boost" "item_modifiers/team_boost.json";
var num red_score<@a[team=red]> = 0;
var num blue_score<@a[team=blue]> = 0;
var num player_score<@s> = 0;
function "award_points" {
player_score<@s> = $player_score<@s>$ + 10;
if $player_score<@s>$ > 100 {
red_score<@a[team=red]> = $red_score<@a[team=red]>$ + 10;
tellraw @s {"text":"High score bonus! Red team score: ","extra":[{"score":{"name":"@s","objective":"red_score"}}]};
} else if $player_score<@s>$ > 50 {
red_score<@a[team=red]> = $red_score<@a[team=red]>$ + 5;
tellraw @s {"text":"Medium score bonus! Red team score: ","extra":[{"score":{"name":"@s","objective":"red_score"}}]};
} else {
red_score<@a[team=red]> = $red_score<@a[team=red]>$ + 1;
tellraw @s {"text":"Standard bonus! Red team score: ","extra":[{"score":{"name":"@s","objective":"red_score"}}]};
}
tellraw @s {"text":"Your score: ","extra":[{"score":{"name":"@s","objective":"player_score"}}]};
}
function "show_leaderboard" {
tellraw @s {"text":"=== LEADERBOARD ==="};
tellraw @s {"text":"Red Team: ","extra":[{"score":{"name":"@s","objective":"red_score"}}]};
tellraw @s {"text":"Blue Team: ","extra":[{"score":{"name":"@s","objective":"blue_score"}}]};
tellraw @s {"text":"Your Score: ","extra":[{"score":{"name":"@s","objective":"player_score"}}]};
}
function "countdown_timer" {
var num timer<@s> = 10;
while $timer<@s>$ > 0 {
tellraw @s {"text":"Time remaining: ","extra":[{"score":{"name":"@s","objective":"timer"}}]};
timer<@s> = $timer<@s>$ - 1;
exec game:wait_one_second;
}
tellraw @s {"text":"Time's up!"};
}
Complex Game Logic
pack "game" "Complex game example" 82;
namespace "game";
// Tag declarations
tag recipe "magic_wand" "recipes/magic_wand.json";
tag loot_table "magic_loot" "loot_tables/magic_loot.json";
tag advancement "magic_master" "advancements/magic_master.json";
tag predicate "has_mana" "predicates/has_mana.json";
tag structure "magic_tower" "structures/magic_tower.json";
var num player_level<@s> = 1;
var num player_exp<@s> = 0;
var num global_high_score<@a> = 0;
var num game_timer<@a> = 0;
function "gain_experience" {
player_exp<@s> = $player_exp<@s>$ + 10;
if $player_exp<@s>$ >= 100 {
player_level<@s> = $player_level<@s>$ + 1;
player_exp<@s> = 0;
tellraw @s {"text":"Level up! New level: ","extra":[{"score":{"name":"@s","objective":"player_level"}}]};
if $player_level<@s>$ > $global_high_score<@a>$ {
global_high_score<@a> = $player_level<@s>$;
tellraw @a {"text":"New high level achieved: ","extra":[{"score":{"name":"@s","objective":"global_high_score"}}]};
}
}
}
function "update_timer" {
game_timer<@a> = $game_timer<@a>$ + 1;
if $game_timer<@a>$ >= 1200 {
game_timer<@a> = 0;
tellraw @s {"text":"Time's up! Final level: ","extra":[{"score":{"name":"@s","objective":"player_level"}}]};
}
}
on_tick "game:update_timer";
Compilation Rules
Variable Resolution
- Declaration: Variables declare their storage scope when defined
- Reading:
$variable<scope>$
gets converted to appropriate Minecraft scoreboard commands - Writing:
variable<scope>
specifies the target scope for assignments - Access: Variables can be accessed at any scope, regardless of where they were declared
Function Compilation
- Exec Calls:
exec function
becomesexecute as @s run function namespace:function
- Exec Calls with Scope:
exec function<@s>
becomesexecute as @s run function namespace:function
- No Return Values: Functions compile to a series of Minecraft commands
Control Structure Compilation
- If Statements: Comparisons compile to scoreboard comparisons.
!=
uses equality with inversion. Boolean expressions (&&
,||
,!
) compile via temporary boolean scores andexecute
chaining. - Else If Statements: Handled as nested
if
with separate generated helper functions; chains are preserved. - Else Blocks: Compiled using inverted conditions with
execute unless
to run the else helper function. - While Loops: Generate recursive function calls that continue while the condition is true.
- Scheduled While Loops: Generate a per-tick scheduled helper, tagging entities to iterate; schedule continues while the condition remains true.
- Nested Structures: Automatically handle complex nested if/else and while loop combinations.
Say Command Compilation
- Simple Text:
say "message"
becomestellraw @a {"text":"message"}
- With Variables:
say "Score: $score<@s>$"
orsay "Score: $score$"
compiles totellraw
with ascore
component;$var$
defaults to<@s>
. - Multiple Variables: Complex variable substitutions are automatically formatted into proper JSON structure.
- Default Target: All say commands target
@a
(all players) for maximum visibility.
Tag Compilation
- Recipe Tags:
tag recipe "name" "path"
generates appropriate tag files - Loot Table Tags:
tag loot_table "name" "path"
generates loot table tag files - Advancement Tags:
tag advancement "name" "path"
generates advancement tag files - Item Modifier Tags:
tag item_modifier "name" "path"
generates item modifier tag files - Predicate Tags:
tag predicate "name" "path"
generates predicate tag files - Structure Tags:
tag structure "name" "path"
generates structure tag files
Error Handling
- Undefined Variables: Compilation error if variable not declared
- Invalid Scopes: Compilation error if scope selector is malformed
- Missing Semicolons: Compilation error for incomplete statements
- Unterminated Blocks: Compilation error for missing braces
- Invalid Tag Paths: Compilation error if tag file path is malformed
Best Practices
- Always specify scopes explicitly - Makes code clear and prevents bugs
- Use consistent syntax -
$variable<scope>$
for reading,variable<scope>
for writing - Use meaningful variable names -
player_score<@s>
is clearer thanscore<@s>
- Group related variables - Keep variables with similar purposes together
- Comment complex scopes - Explain non-standard selectors
- Avoid reserved names - Don’t use
load
,tick
, or other Minecraft keywords - Use consistent naming - Pick a convention and stick to it
- Test scope combinations - Verify that your scope logic works as expected
- Organize tag declarations - Group related tags together at the top of files
- Use descriptive tag names - Make tag names clear and meaningful
Tokenization Specification
This section defines exactly how MDL source code is broken down into tokens. This specification is critical for maintaining consistency between the lexer, parser, and compiler.
Core Token Types
Keywords (Reserved Words)
pack, namespace, function, var, num, if, else, while, scheduledwhile, on_load, on_tick, exec, tag
Tag Types (Resource Categories)
recipe, loot_table, advancement, item_modifier, predicate, structure
Identifiers
[a-zA-Z_][a-zA-Z0-9_]*
Examples: player_score
, game
, start_game
, _internal_var
Numbers
[0-9]+(\.[0-9]+)?
Examples: 0
, 42
, 3.14
, 1000
Operators
// Arithmetic
+ (PLUS), - (MINUS), * (MULTIPLY), / (DIVIDE)
// Comparison
== (EQUAL), != (NOT_EQUAL), > (GREATER), < (LESS), >= (GREATER_EQUAL), <= (LESS_EQUAL)
// Logical
&& (AND), || (OR), ! (NOT)
// Assignment
= (ASSIGN)
// Range
.. (RANGE)
// Execution
exec (EXEC)
Delimiters
; (SEMICOLON) - Statement terminator
, (COMMA) - Parameter separator
: (COLON) - Namespace separator
Brackets and Braces
( (LPAREN), ) (RPAREN) - Parentheses for expressions and function calls
{ (LBRACE), } (RBRACE) - Braces for code blocks
[ (LBRACKET), ] (RBRACKET) - Brackets for selectors and arrays
< (LANGLE), > (RANGLE) - Angle brackets for scope syntax
Special Tokens
$ (DOLLAR) - Variable substitution delimiter; line-start $... as MACRO_LINE
! (EXCLAMATION) - Used in $!raw markers
RAW_CONTENT - Entire content of a raw block
" (QUOTE) - String literal delimiter (supports both " and ' in lexer)
Tag Declaration Tokenization
Basic Tag Declaration
tag recipe "RecipeName" "path/to/recipe.json";
Tokenized as:
TAG
(tag
)RECIPE
(recipe
)QUOTE
("
)IDENTIFIER
(RecipeName
)QUOTE
("
)QUOTE
("
)IDENTIFIER
(path/to/recipe.json
)QUOTE
("
)SEMICOLON
(;
)
Tag Declaration with Complex Path
tag loot_table "EpicLoot" "loot_tables/epic_loot.json";
Tokenized as:
TAG
(tag
)LOOT_TABLE
(loot_table
)QUOTE
("
)IDENTIFIER
(EpicLoot
)QUOTE
("
)QUOTE
("
)IDENTIFIER
(loot_tables/epic_loot.json
)QUOTE
("
)SEMICOLON
(;
) ```
Scope Selector Tokenization
Basic Selectors
@s, @a, @p, @r
These are tokenized as single IDENTIFIER
tokens.
Complex Selectors
@e[type=armor_stand,tag=mdl_global,limit=1]
This entire selector is tokenized as a single IDENTIFIER
token.
Scope Syntax
<@s>, <@a[team=red]>, <global>
These are tokenized as:
LANGLE
(<
)IDENTIFIER
(the selector content)RANGLE
(>
)
Variable Substitution Tokenization
Basic Substitution
$player_score<@s>$
Tokenized as:
DOLLAR
($
)IDENTIFIER
(player_score
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)DOLLAR
($
)
Shorthand (Default Scope)
$player_score$
Tokenized as:
DOLLAR
($
)IDENTIFIER
(player_score
)DOLLAR
($
)
Note: When the scope is omitted, the parser defaults it to <@s>
during AST construction.
Complex Substitution
$team_score<@a[team=red]>$
Tokenized as:
DOLLAR
($
)IDENTIFIER
(team_score
)LANGLE
(<
)IDENTIFIER
(@a[team=red]
)RANGLE
(>
)DOLLAR
($
)
Function Declaration Tokenization
Basic Function
function game:start_game {
Tokenized as:
FUNCTION
(function
)IDENTIFIER
(game
)COLON
(:
)IDENTIFIER
(start_game
)LBRACE
({
)
Function with Scope
function game:reset_player {
Tokenized as:
FUNCTION
(function
)IDENTIFIER
(game
)COLON
(:
)IDENTIFIER
(reset_player
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)LBRACE
({
)
Function Call Tokenization
Call with Scope
exec game:reset_player<@s>;
Tokenized as:
EXEC
(exec
)IDENTIFIER
(game
)COLON
(:
)IDENTIFIER
(reset_player
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)SEMICOLON
(;
)
Exec Call without Scope
exec game:reset_player;
Tokenized as:
EXEC
(exec
)IDENTIFIER
(game
)COLON
(:
)IDENTIFIER
(reset_player
)SEMICOLON
(;
) ```
Variable Declaration Tokenization
Basic Declaration
var num player_score<@s> = 0;
Tokenized as:
VAR
(var
)NUM
(num
)IDENTIFIER
(player_score
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)ASSIGN
(=
)NUMBER
(0
)SEMICOLON
(;
)
Variable Assignment Tokenization
Simple Assignment
player_score<@s> = 42;
Tokenized as:
IDENTIFIER
(player_score
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)ASSIGN
(=
)NUMBER
(42
)SEMICOLON
(;
)
Expression Assignment
player_score<@s> = $player_score<@s>$ + 1;
Tokenized as:
IDENTIFIER
(player_score
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)ASSIGN
(=
)DOLLAR
($
)IDENTIFIER
(player_score
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)DOLLAR
($
)PLUS
(+
)NUMBER
(1
)SEMICOLON
(;
)
Control Structure Tokenization
If Statement
if $player_score<@s>$ > 10 {
Tokenized as:
IF
(if
)DOLLAR
($
)IDENTIFIER
(player_score
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)DOLLAR
($
)GREATER
(>
)NUMBER
(10
)LBRACE
({
)
While Loop
while $counter<@s>$ > 0 {
Tokenized as:
WHILE
(while
)DOLLAR
($
)IDENTIFIER
(counter
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)DOLLAR
($
)GREATER
(>
)NUMBER
(0
)LBRACE
({
)
Raw Block Tokenization
Raw Block Start
$!raw
Tokenized as:
DOLLAR
($
)EXCLAMATION
(!
)IDENTIFIER
(raw
)
Raw Block End
raw!$
Tokenized as:
IDENTIFIER
(raw
)EXCLAMATION
(!
)DOLLAR
($
)
Whitespace and Comments
Whitespace
- Spaces, tabs, and newlines are ignored during tokenization
- They serve only to separate tokens
- Multiple consecutive whitespace characters are treated as a single separator
Comments
// Single line comment
/* Multi-line comment */
Comments are completely ignored during tokenization and do not generate any tokens.
Comment Rules:
- Single-line comments start with
//
and continue to the end of the line - Multi-line comments start with
/*
and end with*/
- Comments can appear anywhere in the code
- Comments are stripped out before processing - they don’t affect the generated
.mcfunction
files
Tokenization Rules
- Longest Match: Always consume the longest possible token (e.g.,
>=
not>
then=
) - No Ambiguity: Each character sequence maps to exactly one token type
- Scope Priority: Scope selectors are always tokenized as complete
IDENTIFIER
tokens - No Context: Tokenization is context-free - the same character sequence always produces the same tokens
- Error Handling: Invalid characters or unterminated sequences generate appropriate error tokens
- String Handling: Quoted strings are tokenized as complete units with their delimiters
Example Complete Tokenization
tag recipe "diamond_sword" "recipes/diamond_sword.json";
var num player_score<@s> = 0;
Tokens Generated:
TAG
(tag
)RECIPE
(recipe
)QUOTE
("
)IDENTIFIER
(diamond_sword
)QUOTE
("
)QUOTE
("
)IDENTIFIER
(recipes/diamond_sword.json
)QUOTE
("
)SEMICOLON
(;
)VAR
(var
)NUM
(num
)IDENTIFIER
(player_score
)LANGLE
(<
)IDENTIFIER
(@s
)RANGLE
(>
)ASSIGN
(=
)NUMBER
(0
)SEMICOLON
(;
)EOF
This tokenization specification ensures that the lexer, parser, and compiler all work with the same understanding of how MDL source code is structured.
Edge Cases and Error Handling
Common Error Scenarios
Unterminated Scope Selectors
// ❌ Error: Missing closing >
var num score<@s = 0;
// ✅ Correct
var num score<@s> = 0;
Invalid Scope Selectors
// ❌ Error: Invalid selector syntax
var num score<@invalid[type=armor_stand]> = 0;
// ✅ Correct
var num score<@e[type=armor_stand,tag=mdl_global,limit=1]> = 0;
Missing Semicolons
// ❌ Error: Missing semicolon
var num score<@s> = 0
player_score<@s> = 5
// ✅ Correct
var num score<@s> = 0;
player_score<@s> = 5;
Unterminated Blocks
// ❌ Error: Missing closing brace
function game:test {
player_score<@s> = 0;
// Missing }
// ✅ Correct
function game:test {
player_score<@s> = 0;
}
Invalid Variable References
// ❌ Error: Variable not declared
player_score<@s> = 0;
score<@s> = 5; // 'score' was never declared
// ✅ Correct
var num score<@s> = 0;
player_score<@s> = 0;
score<@s> = 5;
Invalid Tag Declarations
// ❌ Error: Missing quotes
tag recipe RecipeName "path/to/recipe.json";
// ❌ Error: Missing semicolon
tag recipe "RecipeName" "path/to/recipe.json"
// ✅ Correct
tag recipe "RecipeName" "path/to/recipe.json";
Complex Edge Cases
Nested Scope Selectors in Raw Blocks
// This is valid - raw blocks pass through unchanged
$!raw
execute if score @s player_score<@s> matches 10.. run function game:celebrate
raw!$
Scope Selectors with Special Characters
// Valid - selector with complex parameters
var num data<@e[type=armor_stand,tag=mdl_global,limit=1,nbt={CustomName:'{"text":"Server"}'}]> = 0;
Variable Names with Underscores
// Valid - underscores are allowed in variable names
var num player_score_red_team<@a[team=red]> = 0;
var num _internal_counter<@s> = 0;
Function Names with Numbers
// Valid - numbers are allowed in function names
function game:level_1_complete {
player_score<@s> = player_score<@s> + 100;
}
Tag Paths with Special Characters
// Valid - paths can contain various characters
tag recipe "complex_recipe" "recipes/complex/recipe_v1.2.json";
tag loot_table "special_loot" "loot_tables/special/loot_#1.json";
Error Recovery
The MDL compiler attempts to provide helpful error messages:
- Line and Column Information - Shows exactly where the error occurred
- Context - Displays the problematic line with surrounding context
- Suggestions - Provides specific guidance on how to fix the error
- Error Categories - Groups errors by type (syntax, scope, undefined variables, invalid tags, etc.)
Performance Considerations
- Large Selectors: Very long scope selectors may impact compilation time
- Deep Nesting: Excessive nesting of control structures may affect parsing performance
- Raw Block Size: Large raw blocks are processed efficiently as they’re copied without parsing
- Tag Processing: Tag declarations are processed efficiently as they’re simple string operations
Abstract Syntax Tree (AST) Implementation
The MDL language is implemented using a comprehensive Abstract Syntax Tree (AST) system that represents the parsed code structure. This section explains how the AST works and how it represents all language constructs.
AST Node Hierarchy
The AST is built using a hierarchy of node classes, each representing a specific language construct:
Root Node: Program
class Program:
pack: Optional[PackDeclaration] # Pack metadata
namespace: Optional[NamespaceDeclaration] # Default namespace
tags: List[TagDeclaration] # Resource tag declarations
variables: List[VariableDeclaration] # Variable declarations
functions: List[FunctionDeclaration] # Function definitions
hooks: List[HookDeclaration] # Event hooks (on_load, on_tick)
statements: List[ASTNode] # Top-level statements
The Program
node serves as the root of the AST, containing all top-level declarations and statements. This structure allows the compiler to:
- Generate the
pack.mcmeta
file from pack declarations - Create the proper directory structure based on namespace
- Process all tag references for resource generation
- Manage variable scoping across the entire program
- Generate function files with proper namespacing
- Set up event hooks for automatic execution
Declaration Nodes
PackDeclaration
class PackDeclaration:
name: str # Pack name (e.g., "MyGame")
description: str # Pack description
pack_format: int # Minecraft pack format version
NamespaceDeclaration
class NamespaceDeclaration:
name: str # Namespace name (e.g., "game")
TagDeclaration
class TagDeclaration:
tag_type: str # Resource type (recipe, loot_table, etc.)
name: str # Tag name
file_path: str # Path to the JSON file
VariableDeclaration
class VariableDeclaration:
var_type: str # Variable type (currently only "num")
name: str # Variable name
scope: str # Scope selector (e.g., "<@s>", "<@a[team=red]>")
initial_value: Any # Initial value expression
FunctionDeclaration
class FunctionDeclaration:
namespace: str # Function namespace
name: str # Function name
scope: Optional[str] # Optional scope for function execution
body: List[ASTNode] # Function body statements
HookDeclaration
class HookDeclaration:
hook_type: str # Hook type ("on_load" or "on_tick")
namespace: str # Function namespace to call
name: str # Function name to call
scope: Optional[str] # Optional scope for hook execution
Statement Nodes
VariableAssignment
class VariableAssignment:
name: str # Variable name
scope: str # Scope selector
value: Any # Value expression
VariableSubstitution
class VariableSubstitution:
name: str # Variable name
scope: str # Scope selector
FunctionCall
class FunctionCall:
namespace: str # Function namespace
name: str # Function name
scope: Optional[str] # Optional scope
Control Structures
class IfStatement:
condition: Any # Condition expression
then_body: List[ASTNode] # Then block statements
else_body: Optional[List[ASTNode]] # Optional else block
class WhileLoop:
condition: Any # Loop condition
body: List[ASTNode] # Loop body statements
Commands
class SayCommand:
message: str # Message text with variable placeholders
variables: List[VariableSubstitution] # Extracted variables
class RawBlock:
content: str # Raw content (passed through unchanged)
Expression Nodes
BinaryExpression
class BinaryExpression:
left: Any # Left operand
operator: str # Operator (+, -, *, /, >, <, >=, <=, ==, !=)
right: Any # Right operand
LiteralExpression
class LiteralExpression:
value: Any # Literal value
type: str # Value type ("number", "string", "identifier")
ParenthesizedExpression
class ParenthesizedExpression:
expression: Any # Expression inside parentheses
AST Construction Process
The AST is constructed through a multi-stage process:
- Lexical Analysis: Source code is converted to tokens
- Parsing: Tokens are parsed into AST nodes
- Validation: AST structure is validated for correctness
- Compilation: AST is traversed to generate output
Lexical Analysis (Tokenization)
The lexer converts source code into a stream of tokens:
# Source: var num score<@s> = 0;
# Tokens: [VAR, NUM, IDENTIFIER('score'), LESS, IDENTIFIER('@s'),
# GREATER, ASSIGN, NUMBER('0'), SEMICOLON]
Token Types
- Keywords:
PACK
,NAMESPACE
,FUNCTION
,VAR
,IF
,WHILE
, etc. - Operators:
PLUS
,MINUS
,MULTIPLY
,DIVIDE
,ASSIGN
,GREATER
,LESS
, etc. - Delimiters:
SEMICOLON
,COMMA
,COLON
,LPAREN
,RPAREN
, etc. - Literals:
IDENTIFIER
,NUMBER
,QUOTE
- Special:
DOLLAR
,EXCLAMATION
,RAW_CONTENT
Parsing Strategy
The parser uses a recursive descent approach with operator precedence:
def _parse_expression(self) -> Any:
"""Parse expressions with proper operator precedence."""
return self._parse_comparison()
def _parse_comparison(self) -> Any:
"""Parse comparison expressions (>, <, >=, <=, ==, !=)."""
expr = self._parse_term()
while self._peek().type in [GREATER, LESS, GREATER_EQUAL, LESS_EQUAL, EQUAL, NOT_EQUAL]:
operator = self._advance().type
right = self._parse_term()
expr = BinaryExpression(left=expr, operator=operator, right=right)
return expr
Operator Precedence (highest to lowest):
- Primary: Variables, literals, parenthesized expressions
- Factors: Multiplication, division
- Terms: Addition, subtraction
- Comparisons: Greater, less, equal, not equal
Logical Operators - Compilation Notes
- Logical expressions compile into temporary boolean scoreboard values (1 true, 0 false) checked via
execute if/unless
. &&
is compiled as a chain ofexecute if
conditions;||
sets the result true if either operand is true.!
negates the entire operand. For comparisons like!$a<@s>$ > 0
, the comparison is evaluated first, then negated.!=
is compiled using equality with inversion (unless score ... = ...
) because Minecraft lacks a direct not-equal comparator.
Integer-Only Arithmetic and Literal Handling
- Scoreboard arithmetic is integer-only. MDL normalizes integer-like literals:
2.0
->2
. Non-integer literals (e.g.,2.5
) cause a compile-time error when used in scoreboard math. - Literal addition/subtraction uses
scoreboard players add/remove
; elides+0
/-0
. - Literal multiplication/division uses temporary constants with
scoreboard players operation
(e.g., set a temp score to the constant, then*=
//=
).*1
//1
are elided;*0
sets the temp to zero. Division by zero is a compile-time error. - Mixed expressions are lowered via temporary scores to preserve precedence. Score-to-score operations use
scoreboard players operation
.
AST Traversal and Code Generation
The AST is designed to support efficient code generation:
Visitor Pattern Support
class ASTVisitor:
def visit_program(self, node: Program): pass
def visit_variable_declaration(self, node: VariableDeclaration): pass
def visit_function_declaration(self, node: FunctionDeclaration): pass
# ... other visit methods
Code Generation Strategy
- Pack Generation: Create
pack.mcmeta
from pack declarations - Namespace Setup: Establish directory structure
- Tag Processing: Generate resource references
- Variable Management: Set up scoreboard objectives
- Function Generation: Create
.mcfunction
files - Hook Integration: Set up automatic execution
Error Handling and Recovery
The AST system provides comprehensive error handling:
Parser Error Context
class MDLParserError:
message: str # Error description
file_path: str # Source file path
line: int # Error line number
column: int # Error column number
line_content: str # Problematic line content
suggestion: str # How to fix the error
Error Recovery Strategies
- Graceful Degradation: Continue parsing when possible
- Context Preservation: Maintain line/column information
- Helpful Messages: Provide specific fix suggestions
- Error Aggregation: Collect multiple errors when possible
Extensibility Features
The AST system is designed for easy extension:
Adding New Node Types
class NewNode(ASTNode):
def __init__(self, new_field: str):
self.new_field = new_field
Adding New Parsers
def _parse_new_construct(self) -> NewNode:
# Parse new language construct
pass
Adding New Token Types
class TokenType:
# ... existing types ...
NEW_TYPE = "NEW_TYPE"
Parsing System Implementation
The MDL parser implements a robust, extensible parsing system that handles all language constructs defined in the specification. This section explains how the parsing works and how it processes the language.
Parser Architecture
The parser uses a recursive descent approach with lookahead capabilities:
class MDLParser:
def __init__(self, source_file: str = None):
self.source_file = source_file
self.tokens: List[Token] = []
self.current = 0
self.current_namespace = "mdl"
Core Parsing Methods
Program Parsing
def _parse_program(self) -> Program:
"""Parse the complete program structure."""
pack = None
namespace = None
tags = []
variables = []
functions = []
hooks = []
statements = []
while not self._is_at_end():
# Parse top-level constructs based on token type
if self._peek().type == TokenType.PACK:
pack = self._parse_pack_declaration()
elif self._peek().type == TokenType.NAMESPACE:
namespace = self._parse_namespace_declaration()
# ... continue with other constructs
Declaration Parsing
def _parse_pack_declaration(self) -> PackDeclaration:
"""Parse: pack "name" "description" format;"""
self._expect(TokenType.PACK, "Expected 'pack' keyword")
self._expect(TokenType.QUOTE, "Expected opening quote for pack name")
name = self._expect_identifier("Expected pack name")
# ... continue parsing
Statement Parsing
def _parse_if_statement(self) -> IfStatement:
"""Parse: if condition { then_body } else { else_body }"""
self._expect(TokenType.IF, "Expected 'if' keyword")
condition = self._parse_expression()
self._expect(TokenType.LBRACE, "Expected '{' to start if body")
then_body = self._parse_block()
# ... handle optional else clause
Expression Parsing with Operator Precedence
The parser implements a Pratt parser approach for expressions:
def _parse_expression(self) -> Any:
"""Entry point for expression parsing."""
return self._parse_comparison()
def _parse_comparison(self) -> Any:
"""Parse comparison expressions with left associativity."""
expr = self._parse_term()
while not self._is_at_end() and self._peek().type in [
TokenType.GREATER, TokenType.LESS, TokenType.GREATER_EQUAL,
TokenType.LESS_EQUAL, TokenType.EQUAL, TokenType.NOT_EQUAL
]:
operator = self._peek().type
self._advance()
right = self._parse_term()
expr = BinaryExpression(left=expr, operator=operator, right=right)
return expr
Precedence Levels:
- Primary: Variables, literals, parentheses
- Factors:
*
,/
- Terms:
+
,-
- Comparisons:
>
,<
,>=
,<=
,==
,!=
Scope Selector Parsing
Scope selectors are parsed differently based on context:
def _parse_scope_selector(self) -> str:
"""Parse scope selector: <@s>, <@a[team=red]>, etc."""
self._expect(TokenType.LESS, "Expected '<' for scope selector")
selector_content = ""
while not self._is_at_end() and self._peek().type != TokenType.GREATER:
selector_content += self._peek().value
self._advance()
self._expect(TokenType.GREATER, "Expected '>' to close scope selector")
return f"<{selector_content}>"
Context-Sensitive Parsing:
- Variable Declarations: Use
LESS
/GREATER
tokens - Variable Substitutions: Use
LANGLE
/RANGLE
tokens - Function Parameters: Use
LESS
/GREATER
tokens
Raw Block Processing
Raw blocks are handled specially to preserve exact content:
def _parse_raw_block(self) -> RawBlock:
"""Parse: $!raw ... raw!$"""
# Consume $!raw
self._expect(TokenType.DOLLAR, "Expected '$' to start raw block")
self._expect(TokenType.EXCLAMATION, "Expected '!' after '$' in raw block")
self._expect(TokenType.IDENTIFIER, "Expected 'raw' keyword")
# Look for RAW_CONTENT token (generated by lexer)
if self._peek().type == TokenType.RAW_CONTENT:
content = self._peek().value
self._advance()
else:
content = ""
# Consume raw!$ end marker
self._expect(TokenType.IDENTIFIER, "Expected 'raw' to end raw block")
self._expect(TokenType.EXCLAMATION, "Expected '!' to end raw block")
self._expect(TokenType.DOLLAR, "Expected '$' to end raw block")
return RawBlock(content=content)
Variable Substitution in Strings
Variable substitutions within strings are handled through regex extraction:
def _parse_say_command(self) -> SayCommand:
"""Parse: say "message with $variable<scope>$";"""
self._expect(TokenType.IDENTIFIER, "Expected 'say' keyword")
self._expect(TokenType.QUOTE, "Expected opening quote for say message")
# Get string content (includes variable substitutions)
if self._peek().type == TokenType.IDENTIFIER:
message = self._peek().value
self._advance()
else:
message = ""
# Extract variables using regex pattern
variables = []
import re
var_pattern = r'\$([a-zA-Z_][a-zA-Z0-9_]*<[^>]+>)\$'
matches = re.findall(var_pattern, message)
for match in matches:
if '<' in match and '>' in match:
name = match[:match.index('<')]
scope = match[match.index('<'):match.index('>')+1]
variables.append(VariableSubstitution(name=name, scope=scope))
return SayCommand(message=message, variables=variables)
Error Recovery and Context
The parser provides detailed error information:
def _error(self, message: str, suggestion: str):
"""Raise a parser error with full context."""
if self._is_at_end():
line = 1
column = 1
line_content = "end of file"
else:
token = self._peek()
line = token.line
column = token.column
line_content = token.value
raise MDLParserError(
message=message,
file_path=self.source_file,
line=line,
column=column,
line_content=line_content,
suggestion=suggestion
)
Parser Extensibility
The parser is designed for easy extension:
Adding New Constructs
def _parse_new_construct(self) -> NewNode:
"""Parse new language construct."""
# Implementation here
pass
# Add to _parse_program method:
elif self._peek().type == TokenType.NEW_KEYWORD:
statements.append(self._parse_new_construct())
Adding New Expression Types
def _parse_primary(self) -> Any:
"""Parse primary expressions."""
if self._peek().type == TokenType.NEW_TYPE:
return self._parse_new_expression()
# ... existing cases
Performance Optimizations
The parser includes several performance optimizations:
- Token Lookahead: Efficient
_peek()
method for lookahead - Early Exit: Quick checks for common token types
- Memory Efficiency: Minimal object creation during parsing
- Error Recovery: Fast error detection and reporting
Integration with Lexer
The parser works seamlessly with the lexer:
def parse(self, source: str) -> Program:
"""Parse MDL source code into an AST."""
# Lex the source into tokens
lexer = MDLLexer(self.source_file)
self.tokens = lexer.lex(source)
self.current = 0
# Parse the program
return self._parse_program()
This architecture ensures that:
- Lexical errors are caught early with detailed context
- Parsing errors provide helpful recovery suggestions
- AST construction is robust and handles edge cases
- Error reporting is consistent across the entire pipeline
The parsing system provides a solid foundation for the MDL compiler, ensuring that all language constructs are properly understood and can be translated into Minecraft datapack commands.