Yuzhe's Blog

yuzhes

Deep Dive into the RedScript Type System

RedScript’s type system has a very clear characteristic: it is not abstracted for abstraction’s sake, but built to safely press higher-level language features into Minecraft datapack primitives such as scoreboards, NBT storage, and execute semantics. So understanding RedScript types requires looking at what they become in runtime, not only at syntax terms.

Primitive types: int, double, fixed, string, bool

int remains the central runtime value; most control flow, timers, enum tags, and scoreboard arithmetic are built around it. fixed and double reflect RedScript’s split between numeric semantics: fixed is fixed-point, suitable for controlled arithmetic inside the scoreboard system, while double uses a heavier runtime path for scenarios that require true floating-point-like behavior. The type checker already treats them as distinct numeric families; int -> fixed and int -> double are not implicit conversions.

string is a normal string type; format_string still exists in the AST/type checker as a legacy annotation retained for compatibility with older implementations. Rich text builtin argument checks now accept string or format_string, which shows they are converging. bool is intentionally simple, mainly for branching and state bits.

Algebraic data types: Option, Result, payload-bearing enums

The AST already has option type nodes, Some(expr), None, and if let Some(...), so Option<T> is a truly built-in null-safety model in RedScript. The compiler documentation also notes that values like Option<int> are lowered into tag plus payload slot, which is textbook tagged-union behavior.

Result needs two layers of reading. Conceptually, it serves as a payload-bearing enum in a “success value / error code” role; in current implementation, however, stdlib’s Result is still a concrete enum enum Result { Ok(value: int), Err(code: int) }, not a generic Result<T>. So from a type-system perspective, RedScript supports “enum with payload,” but the standard library Result is not yet fully genericized.

Payload enums are one of the most critical parts of the system. Whether Option or Result, everything relies on enumName -> variant -> fields metadata entering the type checker, and during match payloads are bound to local variables through that mapping. In other words, pattern matching in RedScript is not string replacement; it is based on real variant layout.

Composite types: tuple, struct, interface

‘tupleis now one of the most mature composite types.(int, int), (a, b), and let (x, y) = foo()` all flow through to code generation. At runtime, a tuple is not an array; it is multiple fixed return slots or temporary values, which makes it very suitable as a multi-return mechanism.

struct is more like a runtime record with named fields. Field information is collected by the type checker into structs: Map<string, Map<string, TypeNode>>, and later member access and assignments are validated against this table. At runtime, structs usually map to scoreboard/NBT combinations rather than contiguous memory blocks.

The scope of interface needs to be precise. The AST and parser already support interface declarations, and impl Trait for Type can be parsed into HIR; but I did not find complete interface consistency checks and trait dispatch. In other words, interface is currently closer to “syntax and IR scaffolding” than a mature type feature.

How type checking works

src/typechecker/index.ts basically reflects RedScript’s current strategy. The first pass collects functions, globals, structs, enums, consts, and impl methods, including payload field lists for enum variants, not just variant indices. Then when entering function-body checks, local scope advances statement by statement, while expressions are recursively processed by inferType and a set of specialized checks.

There are several key points. First, RedScript type checking is not Hindley-Milner inference; it is a more direct, engineering-style “declaration table + local inference” approach. Second, match branches inject bound variables with new scope based on PatEnum payloads, which is why match Result::Ok(value) lets value have an exact type. Third, Minecraft-specific concepts such as rich text builtins, selectors, event parameters, @watch, and @singleton also enter the type checker instead of being left to backend error handling.

Compile-time versus runtime

Many RedScript types exist only at compile time; runtime does not retain high-level object shells. Tuples are split into multiple return slots; enum-with-payload becomes tag plus payload; structs become scoreboard/NBT fields; even @config injects values into output directly at compile time.

But some type boundaries extend into runtime. For example, double versus fixed is not only a static difference—their storage and arithmetic paths differ on the backend as well. string matches compile into storage rs:strings; event parameter Player type influences @s context and selector validity.

So a more accurate claim is: RedScript is not “a type layer wrapped around everything that then gets fully erased.” It is “keep semantics as much as possible at compile time, and keep only what the target platform must know at runtime.” This is the core reason it can realize advanced features like Option, tuples, and pattern matching inside datapack.

Conclusion

What makes RedScript’s type system truly strong is not the number of vocabulary terms. It is that each type corresponds to a concrete lowering strategy. int/fixed/double map to different numeric backends, Option and payload enums map to tag+payload, tuples map to multi-slot returns, structs map to field tables and storage layout, and interface is still in its first phase. Once those mappings are understood, the type checker rules no longer feel like imitating traditional languages; they are building reliable static guardrails for an uncooperative platform like Minecraft.