Idea: comptime var

Proposal: Compile-Time Mutable Variables (comptime var)

Summary

Introduce comptime var to allow mutable state during the compilation phase. This feature, inspired by Zig, enables advanced metaprogramming patterns—such as compile-time counters, type registries, and iterative code generation—that are currently difficult or impossible with immutable alias definitions.

Motivation

Currently, Mojo’s compile-time metaprogramming relies on alias (or param), which are effectively immutable constants. While safe, this creates friction for advanced use cases:

  • Global Counters: Impossible to generate unique, sequential IDs for types or functions at compile time.
  • Accumulation: Difficult to “collect” items (like plugin registrations or test cases) into a list across different scopes.
  • Complex Logic: Algorithms requiring state (like backtracking during macro expansion) are cumbersome to implement using only recursion and immutable parameters.

Proposed Syntax & Semantics

We propose the comptime var declaration. Unlike alias, which binds a name to a static value, comptime var binds a name to a value stored in the compiler’s memory context.

Basic Usage

# Define a mutable variable available only at compile time
comptime var build_counter: Int = 0

fn generate_unique_id() -> Int:
    # Modify the state
    build_counter += 1
    return build_counter

fn main():
    # These resolve at compile-time to 1 and 2 respectively
    alias id_a = generate_unique_id()
    alias id_b = generate_unique_id()

Scope

  • Block Scope: comptime var inside a function works like a standard variable but is evaluated and discarded during compilation.
  • Global Scope: Allows for cross-function state accumulation (e.g., registering types).

Comparison: The Zig Pattern

In Zig, comptime var is essential for iterating over types or creating compile-time logic that mimics runtime logic.

Zig Style:

comptime var i = 0;
inline while (i < 10) : (i += 1) { ... }

Mojo Equivalent:

Currently, Mojo requires recursive unrolling or @unroll. With comptime var:

comptime var i = 0
# A theoretical compile-time loop structure
while i < 10:
    generate_code(i)
    i += 1

Implementation Challenges

Implementing this is non-trivial due to Compiler Determinism.

  • Order of Evaluation: To ensure builds are reproducible, the compiler must guarantee a strict order of operations. If generate_unique_id() is called in two different modules, the resulting IDs must be deterministic based on the import graph.
  • Parallel Compilation: comptime var introduces data dependencies that may inhibit parallel compilation of source files.
  • Lifecycle: Clear rules must be defined for when the variable is initialized and destroyed (e.g., per module vs. global compilation context).

Conclusion

While implementing comptime var requires solving complex resolution ordering constraints, the payoff is significant. It unblocks powerful metaprogramming capabilities, allowing libraries to implement zero-cost abstractions that currently require external source-generation tools.

Global Counters: Impossible to generate unique, sequential IDs for types or functions at compile time.

Sequential integers are probably something that can be built into the compiler without too much difficulty. Making it much more than that may be difficult.

Accumulation: Difficult to “collect” items (like plugin registrations or test cases) into a list across different scopes.

We do have a few mechanisms like that, namely kernel collection in Mojo and the way that tests are collected. Having a slightly more generalized version of that would be nice.

Complex Logic: Algorithms requiring state (like backtracking during macro expansion) are cumbersome to implement using only recursion and immutable parameters.

You should be able to just run the algorithm at comptime using a normal function.

Implementation Challenges

Order of Evaluation: To ensure builds are reproducible, the compiler must guarantee a strict order of operations. If generate_unique_id() is called in two different modules, the resulting IDs must be deterministic based on the import graph.

The compiler is multithreaded, you’d have to serialize it to make this happen. Making reproducible builds much slower is something that would need to be discussed more in depth.

Overall

I don’t think that comptime var strictly like that is a good idea. Zig does some really weird stuff to make inline for and friends work, and Mojo can do similar with a comptime for, but I think that having generic mutable data structures floating around parameter space is going to be hazardous for compile time at best. I agree that having a sequential integer counter would be helpful (implemented as a named atomic variable which starts at 0), but I think that needs to be a compiler builtin. The ability to collect sets, lists or maps of values at compile time would also be nice, since that could both reduce the “magic” of @register, and it would also be very nice for HTTP servers or servers for other path-based protocols and more advanced testing frameworks.

@denis any compiler team thoughts on whether the main idea or my ideas are even reasonable to implement?

Unique IDs have some very interesting uses in combination with linear types that I’d like to explore.

The main thing I’d be concerned about is how to make a function that wants the value to be “fully initialized” before some function runs at compile time, or how to insure that a materialized version of the value is the “final” one. For instance, if you wanted to do perfect hashing for max operations to make lookups faster, the name -> op map would need to be complete, similar with if you wanted to build a regex to do HTTP path validation at compile time so you can optimize it more heavily. At runtime, simply wanting to make sure that all ops/routes are present would require materialize to work correctly. Initially, I think not allowing the use of other counters and “compiler aided collections” in these “finalizers” would be fine, but I think people would want it quickly, which would force the construction of a graph, with no loops of course, to figure out a safe evaluation order. I’m not sure how doable that is.

Perhaps the feature is too good to be true given Mojo’s focus on moderate compile times.

The fact that parameter for is supposed to be replaced by comptime gives me hope that this feature is on the radar. I don’t have in-depth knowledge of the Mojo C++ codebase or the risk of compiler bugs, but I assume this feature would require a major architectural overhaul.