> For the complete Mojo documentation index, see [llms.txt](/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:

```mojo
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:

```text
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:

```sh
mojo run -Dmode=release hello.mojo
```

Code:

```mojo
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:

```sh
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:

```sh
mojo -Dverbose hello.mojo
```

Code:

```mojo
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.

| Command                        | Compiler view               | Result                                           |
|--------------------------------|-----------------------------|--------------------------------------------------|
| `mojo -D verbose app.mojo`     | `verbose` defined, no value | Error                                            |
| `mojo -D verbose=on app.mojo`  | `verbose="on"`              | `True`                                           |
| `mojo -D verbose=yes app.mojo` | `verbose="yes"`             | `False` (`yes` is not a recognized truthy value) |
| `mojo app.mojo`                | define missing              | `default` → `False`                              |

```mojo
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:

```sh
mojo -D max_threads=8 app.mojo
```

Code:

```mojo
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:

```sh
mojo -D max_threads=8 app.mojo
```

Code:

```mojo
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:

```sh
mojo -D mode=release app.mojo
```

Code:

```mojo
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:

```sh
mojo -D mode=release app.mojo # release

# or

mojo app.mojo # debug (default)
```

Code:

```mojo
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:

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

Code:

```mojo
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](https://mojolang.org/docs/tools/a, b, c.md)  # 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:

```mojo
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](/docs/tools/compilation)
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:

| Define                 | Flag                            | Type     | Values                                              |
|------------------------|---------------------------------|----------|-----------------------------------------------------|
| `__OPTIMIZATION_LEVEL` | `-O` / `--optimization-level`   | `Int`    | `0`, `1`, `2`, `3` (default `3`)                    |
| `__DEBUG_LEVEL`        | `-g` / `--debug-level`          | `String` | `"line-tables"`, `"full"`                           |
| `__SANITIZE_ADDRESS`   | `--sanitize=address`            | `Int`    | `0` (off, default), `1` (on)                        |

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

`--sanitize` also accepts `thread` (ThreadSanitizer), but only
`--sanitize=address` injects a compile-time define.

Read these values through `sys.compile`, which exposes them as the
compile-time values `OptimizationLevel.level` (an `Int`), `DebugLevel.level`
(a `String`), and `SanitizeAddress` (a `Bool`).

For example:

```mojo
from std.sys.compile import OptimizationLevel

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

Mojo's `ASSERT` flag controls `debug_assert()` behavior:

| Define   | Flag                | Type     | Values                                  |
|----------|---------------------|----------|-----------------------------------------|
| `ASSERT` | `-D ASSERT=<value>` | `String` | `none`, `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:

```sh
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:

```mojo
debug_assert[assert_mode="safe"](https://mojolang.org/docs/tools/n >= 0,
    "nth: n must be non-negative",.md)
```

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:

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

:::caution Apple GPU
`debug_assert()` is silently disabled on Apple GPU targets.
:::

## 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**:

| Goal                                            | Flag                                  | Effect                                  |
|-------------------------------------------------|---------------------------------------|-----------------------------------------|
| Emit full debug info (LLDB symbols)             | `-g` or `--debug-level=full`          | Sets `__DEBUG_LEVEL="full"`             |
| Emit line tables only                           | `-g1` or `--debug-level=line-tables`  | Sets `__DEBUG_LEVEL="line-tables"`      |
| Disable optimization                            | `-O0` or `--no-optimization`          | Sets `__OPTIMIZATION_LEVEL=0`           |
| Enable AddressSanitizer                         | `--sanitize=address`                  | Sets `__SANITIZE_ADDRESS=1`             |

**Changes the defines seen by Mojo code**:

| Goal                                            | Flag                                  | Effect                                  |
|-------------------------------------------------|---------------------------------------|-----------------------------------------|
| Enable all `debug_assert()` checks              | `-D ASSERT=all`                       | Independent of `-g` and `-O`            |

A typical debug configuration combines several of these flags:

```sh
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:

```sh
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"`.

```mojo
from std.sys.compile import DebugLevel

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

:::note
Don't confuse Mojo assert levels with MAX runtime configuration.

The MAX runtime defines its own assertion system through the
`MODULAR_DEBUG=assert-level=...` environment variable, with levels such as
`none`, `warn`, `safe`, and `all`.

These settings control MAX inference runtime assertions. `-D ASSERT`
controls Mojo `debug_assert()` behavior.

The two systems are independent.
:::
