Proposal: replace @parameter with comptime statement modifier

Hey all,

one more proposed syntax change: deprecate @parameter decorator on if and for statements and use the comptime keyword instead. Here is the proposal document, please take a look and share your thoughts.

– Denis

10 Likes

I like the new syntax and it’s overall consistency compared to the status quo and discussed alternatives.

Some small typos:

  • „appied“ → „applied“
  • var x = comptime(foo()()var x = comptime(foo())

I think it would be helpful to exactly explain in the Mojo doc how comptime if and comptime for are handled at comptime.

The change per se is a good one as it makes clearer what is happening and why.

The examples (the proposal in general?) seem to focus on performing numerical calculations at compile time and ignore the potential applications for metaprogramming and conditional codegen. Consider e.g. something like:

fn foo[dtype: DType](x: Scalar[dtype]) -> Scalar[dtype]:
    comptime if dtype.is_integer():
        # do the calculation in a way that's optimized for ints
    elif dtype.is_floating_point():
        # do the calculation in a way that's optimized for floats

It’s worth noting that these comptime expressions have equivalents in other languages, such as the static if and static foreach formulations in the D programming language:

I would recommend Mojo devs examine D’s use of this feature in detail, as it predates C++’s constexpr, and has a wide range of application in delivering highly ergonomic compile time metaprogramming.

One thing I think could be interesting about the if comptime expr route is that, provided the compiler guarantees that if True and if False get folded, you can have an if chain that mixes compile-time and run-time decisions. ex:

if comptime (simd_width_of[Float32]() == 1): ...
elif comptime (simd_width_of[Float32]() >= 8): ...
elif parallelism_level() >= 128: ...
else: ... # fallback

Without that capability, it becomes a bit messy to express this behavior. My hope is that this would simplify the implementation a lot while providing similar benefits as C++'s consteval for sub-expressions, since I would expect any compiler to compile-time fold if True/if False regardless of whether or not the user asks for it. I see the point about if comptime(tensor.size()) > dyn_size being slightly confusing, but I also don’t see a way to not have that if we enable comptime for arbitrary subexpressions, which I feel is desirable.

I agree that adding new keywords is a bad idea, since I think that road leads to mpi_distributed_heterogeneous_multi_device_parallel_for. However, I think that comptime for is really a different concept than for i in comptime(...) so it deserves its own keyword to reduce confusion but I have no idea what that would be. comptime assert also does something very different than assert comptime(...) so I think it also deserves some form of unique signifier.

2 Likes

This already works. The comptime works as a function, so the first and the second conditions are evaluated at comptime, and as you said, the optimizer folds the IFs away.

However, if one did this (under the current proposal)

The last condition would error out.

I’m of two minds about this. It can be a powerful thing to mix runtime and comptime in one chain, but it also increases the chances of accidental errors, and the workaround is reasonably easy.

(Re-edited to restore the original content. I’ll post what was intended to be a response to a later post below.)

Yes, but surely it would be trivial to modify to:

comptime if (simd_width_of[Float32]() == 1):
    ...
elif (simd_width_of[Float32]() >= 8):
    ...
else:
    if parallelism_level() >= 128:  # should work, and produce the desired effect, no?
        ...

I’m not a huge fan of nesting one “logical” chain,

comptime if ...:
elif ...:
else:
    if ...:
    elif ...:

However, having issues because of leaving out a comptime is probably worse.

Damn, did I just accidentally edit my previous post instead of creating a new reply? :frowning:

As a point of detail … shouldn’t the compiler be able to optimize away that first pair of if/elif statements at compile time anyway? I’d expect it to be able to take advantage of the simd_width_of answer being knowable at compile time regardless of whether the comptime keyword is used.

It does feel like this is a case where Pythonic syntax is maybe a little less elegant than it could be, with the elif in particular kind of getting in the way of explicit clarity.

In D this would read something like:

static if (...) {          // compile-time 
    ...
} else static if (...) {   // compile-time
    ...
} else if (...) {          // runtime
    ...
} else if (...) {          // runtime
    ...
}

… but that emerges from the C-like semantics. The the separation between compile-time and runtime clauses above is maybe clearer to understand once you grasp it’s equivalent to:

static if (...) {          // compile-time
    ...
} else static if (...) {   // compile-time
    ...
} else {                   // compile-time
    if (...) {             // runtime
        ...
    } else if (...) {      // runtime
        ...
    }
}

Pythonic syntax forces you into writing something like the latter in Mojo. That may not be an entirely bad thing: at least it makes explicit by indentation level the distinction between which clauses are comptime and which aren’t.

Curious question: what scope rules does Mojo apply inside control flow statements? I ask because in D an important distinction between runtime and compile-time control flow statements is that the runtime statements create a new scope, whereas the compile-time ones don’t (and there are good reasons for that).

Mojo is closer to Python in that sense. Each indentation level is a scope, so both comptime and runtime ifs define a new scope. One cannot “conditionally create a variable” using comptime-if.

To add to the big picture, Mojo can be understood roughly as 3-phase compile. (1) parsing and type checking, (2) generics instantiation and comptime IF folding, and (3) code generation and code optimization.

Thanks for clarifying. If you have the time (as I’m sure you’re very busy!), could you help me understand why not? And given that it’s not … (a) is it on the roadmap and (b) what’s the current motivation for comptime if? (The feature, not the change of keyword.)

comptime for I can see a motivation for, as it allows the programmer to explicitly force unrolling of the loop. But if the condition of an if statement is resolvable at compile time, I would expect the compiler to optimize it away regardless of whether a comptime keyword is present or not, so I’m not understanding the intent here.

Apologies if I’m missing something obvious. I’m just very conditioned by my own programming experience to see something like comptime if as overwhelmingly a tool for compile time metaprogramming.

Mojo is built on the idea that a generic function can be analyzed for correctness (“type-checked”) without substituting specific parameter values. Syntax and semantic analysis happens before the comptime IFs are converted to True/False.

Now consider an example like:

fn foo[v: Int](): 
  comptime if v = 2:
    z = 1
  # more code here
  comptime if v > 0:
    use(z)

Mojo compiler is going to complain that the variable z is not assigned before use(z)

Why? Because it does the semantic analysis before it sees call sites of foo(), before it knows what values v is going to have. So the “conditionally create a variable” request can’t work – the condition is not known at the time when the compiler checks the program for correctness.

My limited understanding of D language is that it takes the opposite approach: semantic analysis (is the variable available at the time of use) is performed during template instantiation. Thus, variables/fields etc can be created conditionally, based on template parameter values.

The upside of D’s approach is that it is more powerful and expressive. The upside of Mojo is that each function can be analyzed independently, in isolation, without expensive whole-program template instantiation phase. This makes the mental model simpler, and also enables near-instant compiler feedback in the IDE (which in practice is very much hindered by our not-yet-debugged LSP server).

This is a pretty deep topic, and I’m sure others on the forum can explain it better than I do.

– Denis

3 Likes

Thanks, that really helps my understanding.

FWIW I think I’d consider D code like your example, where a generic function might try to use a parameter that hasn’t been instantiated, to be an implementation bug – either it should explicitly limit the permitted values to ones that will work, or it should ensure the function call only gets made in circumstances where z will have been instantiated. But I get the rationale for why Mojo doesn’t allow it, even in cases where the comptime paths wouldn’t cause this issue, e.g.:

fn foo[v: Int]():
    comptime if v == 2:
        z = 1
    elif v > 0:
        z = 3

    comptime if v > 0:
        print(z)   # still rejected, even though z will always exist in this scope

… and I appreciate the tradeoffs involved in those design decisions.

Heads up, the changes were implemented, and in the Mojo nightly, comptime if, comptime for and comptime assert all exist.

Eventually Mojo will issue warnings (with fix-its) about @parameter decorator. I’m not sure how fast to move on this, should we make the warning pop up right away, or wait for some time. Please share your thoughts.

5 Likes

A warning that points to the how to use the fixit should be fine as soon as you or the team can make it happen. In my opinion, warnings that come with automated fixes aren’t super disruptive.

3 Likes

The rename is complete, next nightly will have deprecation warning for @parameter for and @parameter if

2 Likes

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.