Currently when we have an expect, it isn't removed until all the way after mono. This has the annoying issue that the expect and conditional are separate code by that point. As such, the conditional still runs no matter what. This is clearly a waste of perf and unintended behaviour. On top of that, we eventually want to have the dev backend and wasm backend generating expects. As such, addition or removal of expects should be higher up in compilation process instead of in each individual backend.
My gut feeling as that it should get removed in can around the time we desugar (maybe as part of desaguring). Anyone have other thoughts/opinions? Also, what is the best way to map a single boolean of information from the cli all the way down into can. Feels like this should be easy to do, but I think we may have multiple levels of indirection that make this quite inconvenient to do. Any tips or folks that want to take this on?
I don't think we'll be able to remove at least top-level expect
until after type constraining because expect
will probably be allowed to have bodies with different rules than any other expression.
As discussed in the top level ? try topic, we want expect to allow top-level try
usage.
This only needs to be for the inline expects
Guess I missed clarifying that
Currently top level expects are properly elided (though through a different mechanism)
Okay, good thing I didn't type everything in one big message.
Saved us both some time
We'll still want to give proper type errors when inline expect is used incorrectly
Let's say we desugar inline expect to something like:
{} = if condition then {} else dbg (condition, "condition failed")
Not sure what to put for the else
branch, it doesn't matter as much
If condition
is not a Bool
, then we want to be able to say "inline expects only work with Bool values"
I might be misunderstanding something, let me re-read your message again
As such, addition or removal of expects should be higher up in compilation process instead of in each individual backend.
This seems like your real goal, to not run the expect's condition if you're trying to optimize the expect away
So we essentially desugar to that. The issue is that it ends up becoming:
condition = # some expensive computation
{} = if condition then {} else dbg (condition, "condition failed")
We then skip emitting the expect and emit this:
condition = # some expensive computation
I understand now
Sometimes llvm optimizes that away. A lot of the time we touch memory or call a zig builtin and llvm gives up.
Not surprising
I'm looking through the roc_can
to see what we currently represent expect
as
I think it is just a statement at that level
But it seems like we'd just want to pass an ignore_expects
flag to mono and delete inline expect
s in with_hole?
By mono, the condition is a separate statement and expect just holds a symbol to load
That's surprising, but I believe you
I could be wrong, but that is what it looked like to me
I'm checking
So on this line, when we see an expect
, it's represented as:
roc_can
expressionroc_can
expressionThis match branch is where the statements are created
So this is the last place to cut everything off in one place instead of having split statements
Yep
But I'm not sure where to cut it off before this.
In the pipeline, we've got
We need to type check, and then right after that this is what's called
So it seems like the right move is to chop it off here
I just think it should be before codegen. Cause backends should not need to decide to elide expects
So right at conversion from can to mono. That sounds fine
Just need to pipeline the right info here
I agree that monomorphization
doesn't mean "specialize AND ALSO delete expects if this flag was passed"
Would you be happy with adding a 4th field to the Expr::Expect
that's called ignored
?
I feel like this should be part of the environment or similar. It will be one value for all expects
Which feels like we're still having mono do the work of deleting dead code, but then it's not reading from the environment
What you just suggested would be my actual vote
My most recent suggestion is a way to avoid having mono need to be "directly" compiler flag-aware
Brendan Hansknecht said:
So right at conversion from can to mono. That sounds fine
Oh, I see that we're in agreement
I'll make an issue?
Also, just to clarify, I'm just being pragmatic. It needs to be somewhere before mono. Probably should be after type checking so that types won't change between debug and release due to removing expects.
We'd need some other compiler phase to do that
And I just think we need to get a bool there somehow (may want to make the bool a two element enum, but same idea)
oh, is type check on mono?
I really don't know these parts of the compiler
I thought mono was after unification and was just about generating many specializations, but that is probably wrong?
To my understanding:
If we do it in canonicalization, we can't do type-checking (unless we added a UselessExpect
ast node, type checked it just like inline expects, and then ignored in in mono)
We can't remove anything during typechecking
So this is literally the first and last place to do it
That said, we will eventually need something that does inlining and constant evaluation
That compiler phase could also decide to remove dbg's and expect's
Theoretically, you could remove it all the way at the top of canonicalization.
inlining and constant evaluation
I think that is expected to run on mono right before the backends run
Sam Mohr said:
If we do it in canonicalization, we can't do type-checking (unless we added a
UselessExpect
ast node, type checked it just like inline expects, and then ignored in in mono)
Sounds like you'd like something like this?
Actually, as I am thinking about this more, I think we should remove it before typechecking and allow the type to change
A better name would be IgnoredExpect
or something like that
I think the type will almost never change, but if it does, it should be a compiler optimization due to removing an unnecessary constraint.
eh, but that mean if you always build with --optimize
and then remove that flag at some point, you code my suddenly have a type error... so nvm to the above idea
I guess another option is to put a pass after mono that deletes expects along with the node that generates their conditionals.
but that just feels like extra hassle
The long term with canonicalization is to cache some artifact that is parsed, canonicalized, and partially typecheck in a way that doesn't ever need to be recalculated unless there are code changes or you use a different Roc version
I see the more-than-single responsibility aspect of mono
as a performance optimization. We don't to read over the entire AST 40 times, this seems like an unfortunate side effect of that
But we can add a TODO or something that says if we add such a compiler pass, we can move this conditional deletion of inline expects to that pass
https://github.com/roc-lang/roc/issues/7348
as it happens, @Agus Zubiaga and I have been working on just such a pass for lambda sets! :big_smile:
so yeah we can make sure to drop inline expect
(when it's an optimized build) when we convert from canonical IR to the new IR that we're working on that will later get turned into our current mono
IR
i wonder if there’s an easier method to this - for example, what if you desugared the entire expect call to a closure and called that at the expect site? for example
expect foo == bar
becomes
#call_expect expect_1 {..closure data}
expect_1 = \{..locals} -> expect foo == bar
Now it is trivial to remove the #call_expect calls in prod build and the rest is DCEd
Last updated: Jul 06 2025 at 12:14 UTC