Skip to content

Syntax Reference

Complete syntax reference for RedScript.

File Extension

RedScript files use the .mcrs extension.

Comments

rs
// Single-line comment

/*
   Multi-line
   comment
*/

Variables

rs
let name: type = value;       // mutable
const NAME: type = value;     // constant
let name = value;             // type inferred

Types

TypeDescriptionExample
intInteger (scoreboard, 32-bit signed)42
fixedFixed-point ×10000 (renamed from float in v2.5.0)10000 (= 1.0)
doubleIEEE 754 double, NBT-backed (new in v2.5.0)x as double
stringText"hello"
boolBooleantrue, false
int[]Array of int[1, 2, 3]
string[]Array of string["a", "b"]
selectorEntity selector@a, @e[type=zombie]
nbtNBT data{Health: 20f}

v2.5.0: float is deprecated and renamed to fixed. Use x as fixed / x as double for explicit numeric conversions — implicit coercion is no longer allowed. The compiler will warn on float arithmetic used without mulfix.

Functions

rs
fn name() {
    // body
}

fn name(param: type) {
    // body
}

fn name(param: type) -> return_type {
    return value;
}

fn name(param: type, optional: type = default) {
    // body
}

Decorators

rs
@decorator
fn name() { }

@decorator(key=value)
fn name() { }
DecoratorDescription
@loadRun on datapack load
@tickRun every game tick
@tick(rate=N)Run every N ticks
@on_trigger("name")Run when player activates trigger
@on_deathRun on entity death
@on_loginRun when player joins server
@on_advancement("id")Run when player earns advancement
@on_craft("item")Run when player crafts item
@on_join_team("team")Run when player joins a team
@on(EventType)Run on static event (PlayerDeath, PlayerJoin, BlockBreak, EntityKill, ItemUse)
@keepPrevent DCE from removing the function

Control Flow

if / else

rs
if (condition) {
    // body
}

if (condition) {
    // body
} else {
    // body
}

if (condition) {
    // body
} else if (condition) {
    // body
} else {
    // body
}

match

Match supports both enum patterns and integer range patterns:

rs
// Enum patterns
match value {
    Pattern::A => { },
    Pattern::B => { },
    _ => { },
}

// Integer range patterns
let score: int = scoreboard_get(@s, #points);
match score {
    90..100 => { say("A grade"); },
    80..89  => { say("B grade"); },
    70..79  => { say("C grade"); },
    _       => { say("Below C"); },
}

Range patterns use min..max (inclusive on both ends).

repeat

rs
repeat(count) {
    // body runs count times
}

for i in range

Iterate over an integer range. The upper bound can be a literal or a variable:

rs
for i in 0..10 {
    say("${i}");   // 0 through 9
}

let count: int = get_score(@s, #rounds);
for i in 0..count {
    // runs 'count' times
}

The range is exclusive on the upper end (0..n → 0, 1, …, n-1).

for x in array

Iterate over every element of an array. The loop variable takes the value of each element in order:

rs
let names: string[] = ["Alice", "Bob", "Carol"];
for name in names {
    tell(@a, "Hello, ${name}!");
}

let scores: int[] = [10, 20, 30];
for s in scores {
    // s is 10, then 20, then 30
}

Works with any element type (int[], string[], struct arrays, etc.). Use for i in 0..arr.len when you also need the index.

break / continue

break exits the innermost loop early. continue skips to the next iteration:

rs
while (true) {
    if (score(@s, #lives) <= 0) {
        break;
    }
    // ...
}

foreach (player in @a) {
    if (score(player, #skip) == 1) {
        continue;
    }
    give(player, "diamond", 1);
}

Both break and continue work in while, foreach, and for i in range loops.

execute

The execute statement maps directly to Minecraft's execute command with a typed body block:

rs
execute as @a at @s run {
    setblock ~ ~-1 ~ "stone";
}

execute if block ~ ~-1 ~ "grass_block" run {
    say("Standing on grass!");
}

execute positioned 0 64 0 run {
    particle("heart", ~0, ~1, ~0);
}

execute store result score @s #points run {
    // commands that produce a result
}

Supported subcommands:

SubcommandDescription
as <selector>Change executor
at <selector>Change position/rotation to entity
positioned <x> <y> <z>Set execution position
positioned as <selector>Set position to entity
rotated <yaw> <pitch>Override rotation
rotated as <selector>Copy entity rotation
facing <x> <y> <z>Face a position
facing entity <selector>Face an entity
anchored eyes|feetSet coordinate anchor
align <axes>Align to block grid
in <dimension>Change dimension
on <relation>Navigate entity relations
if block <x> <y> <z> <block>Condition on block type
if score <target> <obj> matches <range>Condition on score
unless block ...Negated block condition
unless score ...Negated score condition
store result score <target> <obj>Store result in score
store success score <target> <obj>Store success flag

Operators

Arithmetic

OperatorDescription
+Addition
-Subtraction
*Multiplication
/Division
%Modulo

Comparison

OperatorDescription
==Equal
!=Not equal
<Less than
>Greater than
<=Less or equal
>=Greater or equal

Logical

OperatorDescription
&&And
||Or
!Not

Strings

rs
let s: string = "hello";
let interpolated: string = "Hello, ${name}!";

F-Strings (Interpolated Strings)

RedScript supports f-string interpolation using ${expr} inside any string literal. Any expression can appear inside the braces.

rs
let player: string = "Steve";
let score: int = 42;

// Simple variable interpolation
let msg: string = "Hello, ${player}!";

// Expression interpolation
let info: string = "Score: ${score * 2} points";

// Nested computation
let desc: string = "Lives: ${max_lives - used_lives}";

v2.6.0: Using "string" + var for concatenation is now a compile error. Use f-strings instead:

rs
// ❌ compile error
let bad: string = "Hello " + name;

// ✅ correct
let good: string = "Hello ${name}";

F-Strings in Chat Commands

When f-strings are used inside tell, title, subtitle, actionbar, or announce, the compiler emits proper Minecraft JSON text components so dynamic values render correctly in-game:

rs
tell(@a, "Your score is ${score(@s, #points)}!");
title(@s, "Round ${round} of ${max_rounds}");
actionbar(@a, "HP: ${score(@s, #hp)} / ${score(@s, #max_hp)}");

These compile to MC JSON text like ["", "Your score is ", {"score": {"name": "@s", "objective": "points"}}, "!"].

Arrays

rs
let arr: int[] = [1, 2, 3];
let first: int = arr[0];

Structs

rs
struct Name {
    field: type,
    field2: type,
}

let instance = Name {
    field: value,
    field2: value,
};

instance.field;

Enums

rs
enum Name {
    Variant1,
    Variant2,
    Variant3,
}

let val: Name = Name::Variant1;

Impl Blocks

impl blocks attach methods to a struct. Methods receive an implicit self parameter and are called with dot notation.

rs
struct Vec2 {
    x: int,
    y: int,
}

impl Vec2 {
    fn length_sq(self) -> int {
        return self.x * self.x + self.y * self.y;
    }

    fn scale(self, factor: int) -> Vec2 {
        return Vec2 { x: self.x * factor, y: self.y * factor };
    }

    fn add(self, other: Vec2) -> Vec2 {
        return Vec2 { x: self.x + other.x, y: self.y + other.y };
    }
}

let v = Vec2 { x: 3, y: 4 };
let len_sq: int = v.length_sq();   // 25
let scaled: Vec2 = v.scale(2);     // Vec2 { x: 6, y: 8 }

Methods defined in impl blocks follow the same visibility rules as top-level functions: names starting with _ are private and subject to dead-code elimination.

Option<T>

Option<T> represents a value that may or may not be present. It has two variants: Some(value) and None.

rs
// Declare an optional value
let maybe: Option<int> = Some(42)
let empty: Option<int> = None

// Unwrap with if let (the only supported unwrap syntax)
if let Some(v) = maybe {
    say("Got ${v}")
}

Returning Option from functions

rs
fn find_score(target: selector) -> Option<int> {
    let s: int = score(target, #points)
    if (s < 0) {
        return None
    }
    return Some(s)
}

let result: Option<int> = find_score(@p)
if let Some(pts) = result {
    tell(@s, "Points: ${pts}")
}

Note: RedScript currently only supports if let Some(x) = opt { ... } for unwrapping Option. match, .unwrap(), .is_some(), .is_none(), and .unwrap_or() are not yet implemented.


## Generics

Functions and structs can be parameterized with one or more type variables. The type variable is written in angle brackets after the name and can be used anywhere a type is expected in that definition.

```rs
// Generic function – returns the first element of any typed array
fn first<T>(arr: T[]): T {
    return arr[0];
}

let n: int = first<int>([10, 20, 30]);      // 10
let s: string = first<string>(["a", "b"]); // "a"

// Multiple type parameters
fn zip<A, B>(a: A, b: B): string {
    return "${a} / ${b}";
}

// Generic struct
struct Pair<T> {
    left: T,
    right: T,
}

let p: Pair<int> = Pair { left: 1, right: 2 };

Type inference is supported for function calls when the compiler can determine the type from the arguments. Explicit type parameters (first<int>(...)) are always accepted.

Lambdas

rs
let f = (x: int) => x * 2;
let g = (x: int) => {
    // multi-line body
    return x * 2;
};

Selectors

rs
@a                          // all players
@e                          // all entities
@p                          // nearest player
@s                          // self
@r                          // random player
@a[tag=x, distance=..10]   // with arguments

NBT Literals

rs
1b          // byte
100s        // short
1000L       // long
1.5f        // float
3.14d       // double
42          // int

{key: value}                    // compound
[1, 2, 3]                      // list
[B; 1b, 0b]                    // byte array
[I; 1, 2, 3]                   // int array
[L; 100L, 200L]                // long array

foreach

rs
// Run code as each entity matching the selector
foreach (z in @e[type=zombie]) {
    kill(z);  // z becomes @s in the compiled function
}

foreach (player in @a) {
    give(player, "minecraft:diamond", 1);
}

Compiles to:

mcfunction
execute as @e[type=minecraft:zombie] run function ns:fn/foreach_0

Execute Context Modifiers

You can attach execute context modifiers directly to a foreach loop to control where and how the body runs. Modifiers are appended after the selector, before the block, and can be stacked in any order.

rs
// Execute at each player's position
foreach (p in @a) at @s {
    // body runs at each player's coordinates
}

// Execute 2 blocks above each zombie
foreach (z in @e[type=zombie]) at @s positioned ~ ~2 ~ {
    // body runs 2 blocks above each zombie
}

// Execute at each player's position, facing the nearest zombie
foreach (p in @a) at @s rotated ~ 0 facing entity @e[type=zombie,limit=1,sort=nearest] {
    // body runs at player position, rotated to face nearest zombie
}

Supported Modifiers

ModifierDescription
at @sExecute at the iterated entity's position and rotation
positioned <x> <y> <z>Offset the execution position (supports relative ~ and local ^ coordinates)
rotated <yaw> <pitch>Override the execution rotation (degrees; ~ keeps current axis)
facing entity <selector>Rotate execution to face the matched entity
facing <x> <y> <z>Rotate execution to face a fixed position
anchored eyes|feetSet the anchor point for ^-relative coordinates
align <axes>Align position to the block grid on the specified axes (e.g., xyz, xz)

Modifiers compile directly to execute sub-commands in the generated .mcfunction file. For example:

mcfunction
# foreach (p in @a) at @s positioned ~ ~2 ~
execute as @a at @s positioned ~ ~2 ~ run function ns:fn/foreach_1

Dead Code Elimination (DCE)

RedScript's optimizer automatically removes unreachable functions from the compiled output.

Visibility rules:

  • Functions not starting with _ are public — always emitted (callable via /function namespace:name)
  • Functions starting with _ are private — only kept if called from reachable code
  • Decorated functions (@tick, @load, @on_*, @on, @keep) are always kept
rs
fn public_fn() { }       // public, always emitted

fn _helper() { }         // private, removed if not called

@keep
fn _kept_helper() { }    // private name, but @keep forces retention

This means you can write private utility functions freely — if they're never called, they won't bloat your datapack.

Released under the MIT License.