Stream: ideas

Topic: incremental/resumable builds


view this post on Zulip Kevin Gillette (Apr 03 2022 at 22:36):

Brendan Hansknecht said:

I assume that would be a runtime check in dev and compile time check in release?

In many places within community chat, I've seen what, afaict, is assumption of a Rust-style tradeoff between dev and release compilations, specifically dev builds minimizing time to first app instruction, while prod provides extra compile-time safety and performance optimizations.

Is that truly a binary tradeoff though? I see extra compiler analysis checks as shortening the dev-test loop (since those extra compile checks are, at least, a kind of "test"); any correctness test early in the dev loop is something I ultimately view as making a better use of developer time compared to unexpectedly finding out about those correctness issues later on ("okay, dev build succeeds, unit tests run, even dev build deployed to the staging environment passes unit tests, so I'm ready to release, right?").

Please forgive the probably inaccurate use of terminology, but can we have a modular, incremental build cache, such that a dev build can quickly produce a binary for app-level testing, yet a separate process can continue (or resume) compilation past the "dev" stage towards release quality (at least any further correctness analysis)?

If I change nothing about the source code but start another "build" after the background/extended compilation has been built and cached, the compiler can deliver those results to me for free, in the form of new errors. Likewise, if I use a module that has a cached release build, at least those non-inlineable but otherwise optimized code-paths from the cache should be able to make it into my dev build.

If byte-for-byte reproducible builds are desirable, that can be a guarantee of release builds, but not a guarantee of dev builds, since dev builds would require a minimum level of processing, but could opportunistically make use of more than that minimum level if available, or if it fits within a time budget ("if the non-linking part of the build takes less than 500ms, then use up to 500ms to see if anything more valuable comes out of it").

view this post on Zulip Kesanov (Apr 04 2022 at 06:49):

prod provides extra compile-time safety

It's the opposite in rust AFAIK. Dev builds provide extra safety guarantees. Like error on numeric overflow.

view this post on Zulip Brendan Hansknecht (Apr 04 2022 at 07:01):

unoptimized and optimized builds both provide the same safety guarantees. They will both panic in the same situations ( i.e numeric overflow ). In current roc, there should be no correctness difference between unoptimized and optimized builds.

With optimized builds, we run morphic which lets us know which pieces of data are always unique. This can be used for removing some refcounting instructions and things of that nature. This does not effect correctness but does effect performance. We don't run it on dev builds because we want dev builds to be blazing fast.

view this post on Zulip Brendan Hansknecht (Apr 04 2022 at 07:05):

If we had some sort of expect unique and it was a runtime check, I think it would mostly defeat the purpose of it. If it is a runtime check, it is no different then the code that is currently generated except it will panic when you get it wrong instead of copying the data. The goal of this uniqueness check is to guarantee removal of those branches and the copying in general. Also removal of any related refcount updates.

This should almost certainly be a compile time error in general. The question is how it would be implemented such that we can still compile fast and make it a compile time error. If it depends on morphic, it would likely be "too costly" for unoptimized builds. That is why I made the comment about unoptimized vs optimized builds

view this post on Zulip Brendan Hansknecht (Apr 04 2022 at 07:08):

specifically dev builds minimizing time to first app instruction

Just a general note, one of the goals is for Roc to essentially be usable as a scripting language. Compile fast, run with ok speed, compile code with errors and hope for the best. Also, another part of fast compilation is around a smooth editor experience where we may want to attempt to incrementally compile and hot code reload an app at 60 fps as someone is using a slider in the editor.

view this post on Zulip Brendan Hansknecht (Apr 04 2022 at 07:11):

Is that truly a binary tradeoff though? I see extra compiler analysis checks as shortening the dev-test loop

It definitely isn't binary though deciding what and how to expose the configuration is an open question. For example, roc check will block programs that will build with roc build. Something like this may help give each developer the experience they care most about.

view this post on Zulip Brendan Hansknecht (Apr 04 2022 at 07:19):

Please forgive the probably inaccurate use of terminology, but can we have a modular, incremental build cache, such that a dev build can quickly produce a binary for app-level testing, yet a separate process can continue (or resume) compilation past the "dev" stage towards release quality (at least any further correctness analysis)?

This is an interesting idea. Might be complex to implement, but definitely worth thinking more about. Rather than just incremental unoptimized or optimized builds, have incremental builds that upgrade from unoptimized to optimized, sharing work (parsing and canonicalization at least).

view this post on Zulip Richard Feldman (Apr 04 2022 at 12:24):

so expect runs in dev builds as well as during tests, so if we had an "expect unique" check which did a runtime check, you could verify it via roc test - which isn't compile time, but it is build time

view this post on Zulip Richard Feldman (Apr 04 2022 at 12:27):

and doing the runtime verification would also catch situations where the optimization actually does trigger at runtime only (because a refcount is 2+ at some point but drops to 1 before hitting that code path, so it's unique at the point of the check at runtime in practice) which would mean having the test rely on morphic could lead to false negatives

view this post on Zulip Richard Feldman (Apr 04 2022 at 12:29):

on the other hand, maybe there's some case where we'd want to be able to assert "this has been verified to be unique in all possible code paths, not just the ones I've covered in tests" - but I'm not sure how important that distinction would be in practice!

view this post on Zulip Kevin Gillette (Apr 04 2022 at 13:19):

@Richard Feldman unique for all possible code paths could be valuable to a module developer, even if such exhaustiveness is more expensive to prove and warrants a separate build flag or subcommand. I agree that for an app build, there really isn't a meaningful distinction between "possible code paths" and "reachable code paths."

Such a module level proof would imply a subtlety to the kind and scope of such possible uniqueness checks though, ordered least to most strict:

  1. This function does not leak additional references to the caller, but may return the same number, e.g. 5 references in, 5 references out, but those could be different references: useful for proving constant memory bounding if the caller doesn't retain the original references during the call.
  2. This function yields the same references, thus does not result in more memory used after the call completes (though if it temporarily allocates during the call, it may worsen page fragmentation depending on the allocator).
  3. Like #2, but this function may yield fewer of the same references, for example, a list-wise min of strings.
  4. Neither this function, nor its callees, heap allocate at all, if given a unique reference.
  5. Neither this function nor its callees heap allocate at all regardless of input.

view this post on Zulip Kevin Gillette (Apr 04 2022 at 13:22):

If such assertions are valuable (they might not be), it implies the need for a distinction between "point sampled" (context-unaware) assertions and span sampled ("from here to there") or directional assertions (coming in from up-stack vs from here all the way down-stack)

view this post on Zulip Brendan Hansknecht (Apr 06 2022 at 04:22):

  1. Neither this function, nor its callees, heap allocate at all, if given a unique reference.

This is kinda what I am interested in, but is much more strict. I just want the guarantee that a specific variable will never be cloned in a specific function or its callees. It might still heap allocate, because a value might get appended to it.

view this post on Zulip Brendan Hansknecht (Apr 06 2022 at 04:24):

I guess I would also like to add. I think it would be good to also guarantee that the function is always called with unique variables. If the value would need to be cloned, I would want that to to be explicit and in the caller.


Last updated: Jun 16 2026 at 16:19 UTC