Stream: compiler development

Topic: remove expect in can


view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 00:29):

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?

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:31):

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.

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:32):

As discussed in the top level ? try topic, we want expect to allow top-level try usage.

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:32):

This only needs to be for the inline expects

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:33):

Guess I missed clarifying that

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:33):

Currently top level expects are properly elided (though through a different mechanism)

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:33):

Okay, good thing I didn't type everything in one big message.

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:33):

Saved us both some time

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:34):

We'll still want to give proper type errors when inline expect is used incorrectly

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:35):

Let's say we desugar inline expect to something like:

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:35):

{} = if condition then {} else dbg (condition, "condition failed")

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:36):

Not sure what to put for the else branch, it doesn't matter as much

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:36):

If condition is not a Bool, then we want to be able to say "inline expects only work with Bool values"

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:37):

I might be misunderstanding something, let me re-read your message again

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:39):

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

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:39):

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

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:40):

I understand now

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:41):

Sometimes llvm optimizes that away. A lot of the time we touch memory or call a zig builtin and llvm gives up.

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:41):

Not surprising

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:42):

I'm looking through the roc_can to see what we currently represent expect as

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:43):

I think it is just a statement at that level

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:44):

But it seems like we'd just want to pass an ignore_expects flag to mono and delete inline expects in with_hole?

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:44):

By mono, the condition is a separate statement and expect just holds a symbol to load

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:45):

That's surprising, but I believe you

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:45):

I could be wrong, but that is what it looked like to me

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:45):

I'm checking

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:46):

https://github.com/roc-lang/roc/blob/7495495800eb18c5fbd66e41850b3f5fa5a42fcf/crates/compiler/mono/src/ir.rs#L7112

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:47):

So on this line, when we see an expect, it's represented as:

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:48):

This match branch is where the statements are created

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:48):

So this is the last place to cut everything off in one place instead of having split statements

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:48):

Yep

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:48):

But I'm not sure where to cut it off before this.

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:49):

In the pipeline, we've got

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:49):

We need to type check, and then right after that this is what's called

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:49):

So it seems like the right move is to chop it off here

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:49):

I just think it should be before codegen. Cause backends should not need to decide to elide expects

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:50):

So right at conversion from can to mono. That sounds fine

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:50):

Just need to pipeline the right info here

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:50):

I agree that monomorphization doesn't mean "specialize AND ALSO delete expects if this flag was passed"

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:51):

Would you be happy with adding a 4th field to the Expr::Expect that's called ignored?

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:51):

I feel like this should be part of the environment or similar. It will be one value for all expects

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:52):

Which feels like we're still having mono do the work of deleting dead code, but then it's not reading from the environment

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:52):

What you just suggested would be my actual vote

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:52):

My most recent suggestion is a way to avoid having mono need to be "directly" compiler flag-aware

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:54):

Brendan Hansknecht said:

So right at conversion from can to mono. That sounds fine

Oh, I see that we're in agreement

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:54):

I'll make an issue?

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:55):

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.

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:56):

We'd need some other compiler phase to do that

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:56):

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)

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:56):

oh, is type check on mono?

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:57):

I really don't know these parts of the compiler

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 05:57):

I thought mono was after unification and was just about generating many specializations, but that is probably wrong?

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:58):

To my understanding:

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:59):

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)

view this post on Zulip Sam Mohr (Dec 12 2024 at 05:59):

We can't remove anything during typechecking

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:00):

So this is literally the first and last place to do it

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:00):

That said, we will eventually need something that does inlining and constant evaluation

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:01):

That compiler phase could also decide to remove dbg's and expect's

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 06:01):

Theoretically, you could remove it all the way at the top of canonicalization.

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 06:01):

inlining and constant evaluation

I think that is expected to run on mono right before the backends run

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:02):

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?

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 06:02):

Actually, as I am thinking about this more, I think we should remove it before typechecking and allow the type to change

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:02):

A better name would be IgnoredExpect or something like that

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 06:03):

I think the type will almost never change, but if it does, it should be a compiler optimization due to removing an unnecessary constraint.

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 06:04):

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

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 06:05):

I guess another option is to put a pass after mono that deletes expects along with the node that generates their conditionals.

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 06:06):

but that just feels like extra hassle

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:06):

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

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:07):

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

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:08):

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

view this post on Zulip Sam Mohr (Dec 12 2024 at 06:25):

https://github.com/roc-lang/roc/issues/7348

view this post on Zulip Richard Feldman (Dec 12 2024 at 06:53):

as it happens, @Agus Zubiaga and I have been working on just such a pass for lambda sets! :big_smile:

view this post on Zulip Richard Feldman (Dec 12 2024 at 06:53):

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

view this post on Zulip Ayaz Hafiz (Dec 12 2024 at 07:35):

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