Module System
RedScript's module system lets you import a whole module, a single symbol, or every exported symbol from a library module. This page documents the module-oriented form used in newer builds.
Library Modules
A file becomes importable as a module when it starts with a library header:
module library
fn heal(player: entity) {
effect(player, "instant_health", 1, 1)
}
fn heal_all() {
foreach (player in @a) {
heal(player)
}
}The module name is taken from the file name. For example, player_utils.mcrs is imported as player_utils.
Use module library for shared code that is meant to be imported by other files instead of compiled only as a standalone entry file.
Importing a Whole Module
Use a plain module import when you want the module namespace but do not want every symbol injected into the local scope.
import player_utils
fn start_round() {
player_utils::heal_all()
}Compile-time behaviour:
- resolves the target library module
- records its exported public symbols
- binds the module name so you can qualify calls with
module_name::symbol
This is the safest style for large projects because it keeps call sites explicit.
Importing a Single Symbol
Use a symbol import when you want one specific name in local scope:
import math::sin
fn wave(angle: int) -> int {
return sin(angle)
}Compile-time behaviour:
- imports only the requested public symbol
- leaves the rest of the module unbound locally
- reports an error if the symbol is missing or not exported
Prefer this form when a module is large but you only need one or two helpers.
Wildcard Imports
Use a wildcard import when you want every exported public symbol:
import math::*
fn circle_x(cx: int, radius: int, angle: int) -> int {
return cx + cos(angle) * radius
}Compile-time behaviour:
- expands to the module's exported public symbols
- injects those names into the current scope
- can produce ambiguity errors if multiple imports provide the same name
Wildcard imports are convenient in small files, but they scale poorly in large codebases. If a file grows, switch to whole-module or symbol imports.
Export Surface
The module system follows the same public/private convention used by DCE:
- names not starting with
_are public and importable - names starting with
_are private implementation details
module library
fn normalize(v: int) -> int {
return _clamp(v)
}
fn _clamp(v: int) -> int {
if (v < 0) {
return 0
}
return v
}Other files can import normalize, but not _clamp.
Circular Dependencies
The compiler handles module cycles in two phases:
- scan library headers and exported symbol tables
- resolve bodies after the global module graph is known
That means mutually recursive function references can be resolved as long as the imported symbols exist and their signatures are valid.
// a.mcrs
module library
import b::pong
fn ping() {
pong()
}
// b.mcrs
module library
import a::ping
fn pong() {
ping()
}Practical guidance:
- cycles between function declarations are acceptable
- avoid circular top-level initialization with real runtime state
- move shared types and helpers into a third module when two modules start depending on each other too heavily
If a cycle cannot be resolved cleanly, split shared contracts into a neutral library module and let both sides import that instead.
Choosing an Import Style
Use this rule of thumb:
import player_utilswhen explicit qualification improves clarityimport math::sinwhen you only need one symbolimport math::*when the file is small and tightly focused
For large datapacks, whole-module imports tend to age the best.