IMPORTANT: To view this page as Markdown, append `.md` to the URL (e.g. /docs/manual/basics.md). For the complete Mojo documentation index, see llms.txt.
Skip to main content
Version: Nightly
For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).

Mojo compilation feature toggles

Mojo provides several mechanisms for compile-time feature gating and configuration:

  • Compile-time defines (-D and sys.defines): pass values from the command line into Mojo code
  • Compile-time conditionals and platform detection (comptime if, comptime assert, sys.info): branch or halt compilation based on compile-time conditions
  • Debug and optimization gating (debug_assert()): control debug-only behavior and runtime checks

Compile-time conditionals

Using comptime assert to establish preconditions

comptime assert halts compilation when its condition evaluates to False. Unlike a runtime assertion, it executes during compilation and produces a compiler error with your message.

Use comptime assert to declare compile-time preconditions on parameters or compilation targets. For example, say you call a GPU-specific function from a CPU build:

from std.sys import is_gpu

def gpu_kernel():
# Called from a CPU build
comptime assert is_gpu(), "this function requires a GPU target"
# ... GPU-specific code

When you call gpu_kernel(), the compiler prints a constraint failed: note with your message, pointing at the assert:

note: constraint failed: this function requires a GPU target
comptime assert is_gpu(), "this function requires a GPU target"
^

Feature gating with comptime if

Use comptime if to select code paths at compile time. The condition must be parameter-evaluable, that is, the compiler must reason about it and it can depend on comptime values and parameter expressions:

Call:

mojo run -Dmode=release hello.mojo

Code:

from std.sys import get_defined_string

def main():
comptime mode = get_defined_string["mode", "debug"]()

comptime if mode == "release":
print("optimized path")
else:
print("debug path with extra checks")

Compile-time defines

The -D flag passes key-value pairs from the command line into Mojo code. The std.sys.defines module reads them.

Call:

mojo run -Dmode=release -Dverbose -Dmax_threads=8 hello.mojo

# or

mojo run -D mode=release -D verbose -D max_threads=8 hello.mojo

You can write either -Dkey=value or -D key=value. Keys and values must be joined with =.

Supported forms include:

  • -D KEY=VALUE: the value is parsed as a string, integer, or boolean, depending on which get_defined_*[]() function reads it
  • -D KEY: defines a flag with no value. is_defined[]() returns True. Use is_defined[]() for presence checks
  • -D KEY=42: numeric values can be read with get_defined_int[]()

The sys.defines module exposes several functions for reading compile-time defines. All define names are compile-time StaticString parameters, not runtime strings.

is_defined[name]()

is_defined[name]() returns True when -D name was passed, regardless of its value. It never errors.

Call:

mojo -Dverbose hello.mojo

Code:

from std.sys import is_defined

def main():
comptime if is_defined["verbose"]():
print("verbose mode enabled")

is_defined[name]() is similar to C's #ifdef. It checks only whether a define exists. The value is ignored.

Use it when any value enables the feature, or when you only care that the flag was passed. Use other get_defined_*[]() functions to read the value itself.

get_defined_bool[name, default=False]()

get_defined_bool[name, default=False]() returns a Bool. It distinguishes between "defined" and "truthy".

The following values are treated as True:

  • 1
  • true, True, TRUE
  • on, On, ON

Any other assigned string value returns False.

This function errors when the define does not provide a value.

CommandCompiler viewResult
mojo -D verbose app.mojoverbose defined, no valueError
mojo -D verbose=on app.mojoverbose="on"True
mojo -D verbose=yes app.mojoverbose="yes"False (yes is not a recognized truthy value)
mojo app.mojodefine missingdefaultFalse
from std.sys import get_defined_bool

def main():
comptime verbose = get_defined_bool["verbose"]()
comptime if verbose:
print("verbose mode enabled")

Avoid default=True. It reverses the meaning in a confusing way: missing values become True, while present-but-non-truthy values such as -D verbose=banana or -D verbose=0 become False.

If you need default=True, consider using is_defined[]() instead. It expresses intent more clearly.

get_defined_int[name]() and get_defined_int[name, default]()

get_defined_int[name]() returns an Int. If the define is missing or the value is not a valid integer, compilation fails.

Use this only when the define is required.

Call:

mojo -D max_threads=8 app.mojo

Code:

from std.sys import get_defined_int

def main():
comptime threads = get_defined_int["max_threads"]()
print(t"Up to {threads} threads")

The parser accepts only base-10 integers. For example, -D N=10 works. -D N=0x10, -D N=0o10, and -D N=1_000 all fail at the get_defined_int[]() call site. The values are stored as strings and Mojo doesn't recognize those formats as integers.

Non-integer values such as -D max_threads=eight fail the same way. If you encounter these errors, check command-line spelling and format.

The defaulted version returns the provided value instead of erroring when the define is missing.

Call:

mojo -D max_threads=8 app.mojo

Code:

from std.sys import get_defined_int

def main():
comptime threads = get_defined_int["max_threads", 4]()
print("using", threads, "threads")

The default handles only missing defines. If the define exists but its value is not a valid integer, compilation still fails.

get_defined_string[name]() and get_defined_string[name, default]()

get_defined_string[name]() returns a StaticString. Compilation fails if the define is missing.

Use this when the define is required.

Call:

mojo -D mode=release app.mojo

Code:

from std.sys import get_defined_string

def main():
comptime mode = get_defined_string["mode"]()
comptime if mode == "release":
print("release build")

The defaulted version returns default instead of erroring when the define is entirely missing.

Call:

mojo -D mode=release app.mojo # release

# or

mojo app.mojo # debug (default)

Code:

from std.sys import get_defined_string

def main():
comptime mode = get_defined_string["mode", "debug"]()
comptime if mode == "release":
print("release build")

get_defined_dtype[name, default]()

get_defined_dtype[name, default]() returns a DType. A default value is required.

Use this to parameterize numeric code with a user-selected type.

Call:

mojo -D dtype=float8_e4m3fn -D ctype=bfloat16 app.mojo

Code:

from std.sys import get_defined_dtype

def main() raises:
# ... setup for a typical matmul call

comptime a_type = get_defined_dtype["dtype", DType.bfloat16]()
comptime c_type = get_defined_dtype["ctype", DType.bfloat16]()
matmul[a_type, c_type](a, b, c) # specialized at compile time for this dtype pair

# ... continuing code

Unlike C-style -D flags, which produce preprocessor strings, Mojo treats -D values as first-class compile-time parameters in the type system.

This allows the compiler to specialize code such as matmul[]() for every DType combination passed on the command line, without runtime branching.

Values are parsed by the standard library's internal DType parser, which expects canonical names such as float16, bfloat16, and float8_e4m3fn.

Misspelled or aliased names such as fp16 and bf16 don't necessarily produce compile-time errors at the get_defined_dtype[]() call site. They are parsed as an invalid dtype. An error appears later if that value is used in a context that rejects it.

If you encounter these errors, check the exact spelling used on the command line.

Platform and architecture detection

The sys.info module provides compile-time, parameter-evaluable functions for branching on compilation targets.

OS detection

Detect the target operating system with:

  • CompilationTarget.is_linux()
  • CompilationTarget.is_macos()

Mojo does not currently support Windows targets natively, so there is no Windows detection API.

CPU detection

Detect the target CPU architecture or specific Apple Silicon generation with:

  • CompilationTarget.is_x86()
  • CompilationTarget.is_apple_silicon()
  • CompilationTarget.is_apple_m1() through CompilationTarget.is_apple_m5()

Instruction set detection

Detect target instruction set extensions with APIs such as:

  • CompilationTarget.has_avx512f()
  • CompilationTarget.has_neon()

GPU and accelerator detection

Check whether code is compiling for a specific accelerator target with:

  • is_nvidia_gpu()
  • is_amd_gpu()
  • is_apple_gpu()
  • is_gpu()

Check whether the host system has a detected accelerator with:

  • has_accelerator()
  • has_nvidia_gpu_accelerator()
  • has_amd_gpu_accelerator()
  • has_apple_gpu_accelerator()

The distinction matters:

  • is_nvidia_gpu() asks: "am I compiling for an NVIDIA GPU?"
  • has_nvidia_gpu_accelerator() asks whether NVIDIA GPU acceleration is available. This can also be true when the current compilation target is NVIDIA GPU.

For example:

from std.sys import CompilationTarget

def compute():
comptime if CompilationTarget.has_avx512f():
print("AVX-512 path")
elif CompilationTarget.is_apple_silicon():
print("Apple Silicon path")
else:
print("generic path")

These functions report what the compiler is building for. To target a different platform, set the architecture, CPU, feature set, or accelerator from the command line. See Compilation targets for the available flags and how to query what your toolchain supports.

Built-in defines

Mojo provides several built-in defines for controlling compilation and runtime behavior. Other than ASSERT, each is populated by the compiler from a driver flag. Use the driver flag rather than -D so the define matches the compiler's behavior. The flags are shown in the following table:

DefineFlagTypeValues
__OPTIMIZATION_LEVEL-O / --optimization-levelInt0, 1, 2, 3 (default 3)
__DEBUG_LEVEL-g / --debug-levelString"line-tables", "full"
__SANITIZE_ADDRESS--sanitize=addressInt0 (off, default), 1 (on)

If -g is omitted, __DEBUG_LEVEL is not injected; DebugLevel.level returns "none" as a library fallback.

Read these values through sys.compile, which exposes them as the compile-time values: OptimizationLevel, DebugLevel, and SanitizeAddress.

For example:

from std.sys.compile import OptimizationLevel

def main():
comptime if OptimizationLevel.level == 0:
print("unoptimized build")

Mojo's ASSERT flag controls debug_assert() behavior:

DefineFlagTypeValues
ASSERT-D ASSERT=<value>Stringnone, safe (default), all, warn

debug_assert() reads this value directly. Supported assertion levels are:

  • none: disable all assertions
  • safe (default in non-debug builds): only run assertions tagged assert_mode="safe"
  • all: run every debug_assert() call
  • warn: run every assertion, but emit warnings instead of aborting

For example:

mojo run -D ASSERT=all hello.mojo

Using debug_assert() with -D ASSERT

debug_assert() is a runtime assertion controlled by the -D ASSERT flag, which defaults to safe. Other debug-related settings such as -g and -O don't affect debug_assert() behavior.

The default safe mode runs only assertions explicitly tagged as low-overhead:

debug_assert[assert_mode="safe"](
n >= 0,
"nth: n must be non-negative",
)

Untagged assertions written as debug_assert(...) run under -D ASSERT=warn and -D ASSERT=all. Conventionally:

  • Tag constant-time checks such as bounds tests and integer comparisons with assert_mode="safe"
  • Leave traversals, allocations, and more expensive invariant checks untagged

The plain Bool form always evaluates the condition, even when assertions are disabled:

# Bool form: always evaluates the condition.
debug_assert(len(data) > 0, "data must not be empty")

Debug and optimization

Debug and optimization features are controlled independently; enabling one does not automatically enable the others. "Debug build" can mean several different things in Mojo.

Changes how the compiler emits output:

GoalFlagEffect
Emit full debug info (LLDB symbols)-g or --debug-level=fullSets __DEBUG_LEVEL="full"
Emit line tables only-g1 or --debug-level=line-tablesSets __DEBUG_LEVEL="line-tables"
Disable optimization-O0 or --no-optimizationSets __OPTIMIZATION_LEVEL=0
Enable AddressSanitizer--sanitize=addressSets __SANITIZE_ADDRESS=1

Changes the defines seen by Mojo code:

GoalFlagEffect
Enable all debug_assert() checks-D ASSERT=allIndependent of -g and -O

A typical debug configuration combines several of these flags:

mojo -g -O0 -D ASSERT=all app.mojo

The -g and -O driver flags don't affect debug_assert(). If you want these behaviors together, pass each flag explicitly.

A typical release build requires no special flags. Running:

mojo app.mojo

uses -O3, emits no debug info and disables sanitizers.

One exception is debug_assert(). Its default mode is safe, so assertions tagged as always-on still execute. Pass -D ASSERT=none to disable them.

Reading optimization and debug levels

The compiler-defined values __OPTIMIZATION_LEVEL and __DEBUG_LEVEL are exposed through sys.compile as the compile-time values OptimizationLevel and DebugLevel.

Use these when you need fine control:

  • OptimizationLevel.level stores an integer from 0 to 3.
  • DebugLevel.level stores one of the strings "none", "line-tables", or "full".
from std.sys.compile import DebugLevel

def main():
comptime if DebugLevel.level == "full":
print(
"full debug info emitted: enabling source-aware logging"
)