Stream: ideas

Topic: assertUnique function.


view this post on Zulip Eli Dowling (Dec 07 2024 at 15:05):

I think it would be very handy to have, for debugging purposes an "assertUnique"/"expectUnique" function that allows you to get some performance guarantees and track down pesky references.

In my little toy parser I would like to guarantee for example that when I get the next bytes that the buffer is re-used if I do something that changes that I want to know.

I think with unique references being such a big part of performance in roc have more debugging tools and visibility into that would be a boon for folks trying to write high performance roc code.

view this post on Zulip Eli Dowling (Dec 07 2024 at 15:06):

Just to clarify, like any assert/ expect I'd want this to crash the program and provide some info.

view this post on Zulip Eli Dowling (Dec 07 2024 at 15:08):

Something like:
Oh no!, variable X is not a unique reference, it current has a reference count of y.

view this post on Zulip Eli Dowling (Dec 07 2024 at 15:11):

I guess actually thinking about it, for the purposes of tracking down bugs being able to check any ref count value in an assert could be helpful. As In, "Its not unique, but I expect it to be 2 here and 3 here and then 2 here and then unique. Let's see where I'm wrong"

view this post on Zulip Richard Feldman (Dec 07 2024 at 15:36):

yeah this has come up a number of times over the years. I think this is a case where the upside is obvious but the downsides are nonobvious and potentially serious

view this post on Zulip Richard Feldman (Dec 07 2024 at 15:38):

for example, suppose it existed but got used not just for debugging but in public facing APIs, and people start using it to say "you can only pass unique values to this" because who wouldn't want the best performance?

view this post on Zulip Richard Feldman (Dec 07 2024 at 15:39):

but now it's not part of the type system, so you start getting crashes and you have no way to tell when one might happen, so to fix that now there's demand for moving it to the type level, and before you know it Roc has moved halfway to Rust :sweat_smile:

view this post on Zulip Richard Feldman (Dec 07 2024 at 15:41):

I think a potentially interesting alternative might be like a cli flag that lets you say "whenever a function clones something because it couldn't do in-place, print a backtrace to stderr so I can see where it happened" and then accept a list of modules and/or functions to enable this for, so you aren't seeing spam from other places you don't care about

view this post on Zulip Richard Feldman (Dec 07 2024 at 15:44):

another potentially interesting idea might be a top-level expect variant where you can say "run this code and expect that at most this number of clones happen due to in-place ineligibility" - if it's only available at the top level, then none of the concerns above apply

view this post on Zulip Richard Feldman (Dec 07 2024 at 15:45):

that would be a way to automatically catch accidental perf regressions due to unintentionally making something shared in a place where it matters

view this post on Zulip Eli Dowling (Dec 07 2024 at 16:39):

Richard Feldman said:

I think a potentially interesting alternative might be like a cli flag that lets you say "whenever a function clones something because it couldn't do in-place, print a backtrace to stderr so I can see where it happened" and then accept a list of modules and/or functions to enable this for, so you aren't seeing spam from other places you don't care about

I like this! It would be super cool if you could just tag a function or even a single variable, again to reduce noise. Maybe we could add a special comment or keyword, like dbg that basically just tracks clones.

view this post on Zulip Richard Feldman (Dec 07 2024 at 16:42):

yeah that could also work!

view this post on Zulip Eli Dowling (Dec 07 2024 at 16:46):

Richard Feldman said:

for example, suppose it existed but got used not just for debugging but in public facing APIs, and people start using it to say "you can only pass unique values to this" because who wouldn't want the best performance?

So I thought the same thing. Two thoughts on that:

  1. We could just make it debug build only, and get stripped out of optimised builds
  2. I think it would be so obnoxious it'd be highly unlikely to be misused in the real world. As a package developer why would I ever add an arbitrary restriction that "prescribes performance" rather than just let the user do whatever they want.

I mean there are all sorts of things I could do that would make my package hard to use or annoying, like returning string errors everywhere or other crappy API design choices. But if I did that people would complain or pick another package.

view this post on Zulip Eli Dowling (Dec 07 2024 at 16:48):

One option I really like is to ignore it on external packages.

That way as a very perf conscious package developer for my own tests and test cases, to prevent regressions, I can fill my code with asserts to ensure maximum perf.
Then I can comfortably package it up and know that I won't annoy any of my users.

view this post on Zulip Eli Dowling (Dec 07 2024 at 16:55):

Richard Feldman said:

another potentially interesting idea might be a top-level expect variant where you can say "run this code and expect that at most this number of clones happen due to in-place ineligibility" - if it's only available at the top level, then none of the concerns above apply

I think this a good variant and I like the idea that it's simple and broad, "if the bad number increases anywhere in this code, fail".
I imagine this, coupled with a keyword that tracks the locations of the clones may be enough. Though I think it might still be best to have all three of these :)

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 17:22):

I really think this is something that we need a better profiler tool for in the long term

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 17:25):

Also, I'm not even sold on allowing the print idea. It will mostly lead to people wasting time trying to stop clones that don't even effect performance in a meaningful way. (Not saying it is bad to remove all clones period, it just isn't necessarily valuable).

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 17:27):

One thing that would make this all way easier is simply having good debug info

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 17:28):

If all llvm ir mapped to source line number, we would be able to run profiling tools and see the root line of code. That would make sources of clones a lot more obvious.

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 17:30):

With profiling clones are already quite obvious. They are always memcopies.

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 17:34):

Oh, also, if we do this, let's make it like dbg and not like a backtrack. The clones should log the source line of code, not a backtrace. If we are just printing bracktraces, there is no gain over telling people to just use a profiler. It will give them the backtraces that consume the most time.

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 17:39):

Random related thought:
I wonder if instead we could essentially hijack something like the code coverage infra, but make it for bytes copied per line instead of times a line was taken

view this post on Zulip Richard Feldman (Dec 07 2024 at 18:00):

that's super interesting! I don't know anything about code coverage implementations in practice

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 18:11):

I only know a little bit. Since I has so many locations to have counters for, it makes them super low bit width. So we would need to deviate there. Code coverage instrumentation is basically an array of counters and inserted code to increment the correct counter. Then each counter is mapped to debug info.

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 18:11):

That is a least my rough understanding

view this post on Zulip Brendan Hansknecht (Dec 07 2024 at 18:14):

One other thought. I wonder if we could just intercept all calls to roc_memcpy with some sort of instrumentation. That would log all large data movement in general rather than specifically from clones.

view this post on Zulip Eli Dowling (Dec 08 2024 at 02:10):

I really don't know anything about how profiling info works, but how feasible would it be to inject the variable names into roc copy info?
Then you just search the profiler output for your variable, if we had that plus source code I think we'd be in a good place for debugging.

I do still think the other things we've discussed would be super good for testing and catching regressions though.

view this post on Zulip Eli Dowling (Dec 08 2024 at 02:14):

I guess the last piece of this puzzle is:
For the majority of high performance cases we would probably know at compile time whether a reference is unique or not right?

Well if we had tooling that showed that it would be very handy.

Specifically I'm thinking we could abuse inlay hints from the language server to inlay the compiler's knowledge of the ref count into each variable.
It'd be super cool to press a key and have the reference count guarantees all pop up

view this post on Zulip Eli Dowling (Dec 08 2024 at 02:17):

Brendan Hansknecht said:

Also, I'm not even sold on allowing the print idea. It will mostly lead to people wasting time trying to stop clones that don't even effect performance in a meaningful way.

This is a very good point, maybe if we do have a more general logging feature we could restrain it by function or module, but also by size and frequency.
But then we've basically just recreates the profiler output.... Soooo we are back to "why don't we just have better debug info in profiler output"

I think most paths are leading to a single answer here :sweat_smile:

view this post on Zulip Richard Feldman (Dec 08 2024 at 02:29):

yeah we can definitely do that - the new IR that @Agus Zubiaga and I are working on has a concept of debuginfo so we can pass it along from canonicalization, but it's not doing anything yet

view this post on Zulip Eli Dowling (Dec 08 2024 at 02:40):

Cool! I'm keen!

view this post on Zulip Kasper Møller Andersen (Dec 08 2024 at 07:39):

I genuinely love the @tailrec annotation in Scala, which allows me to specify that some function must be tail recursive, otherwise it’s a compiler error. Elm doesn’t have annotations, but we can emulate it with elm-review, which is such a huge peace of mind.

So even if Roc doesn’t have annotations, so you could write an equivalent @no_clone annotation, maybe an eventual roc-review linter could have access to this level of type information? Then you could enforce it on some function/code block statically. I think that would be pretty huge if you know which parts of your code are hot, and you can protect against regressions that way.

view this post on Zulip Eli Dowling (Dec 08 2024 at 07:45):

I actually had a fork that put that info in the language server, I could revive it. But I think most of us using roc right now are pretty experienced in FP so it's not super useful to the present day users of roc. But in future it'd be nice.

Unfortunately lsp is a bit limited in it's ability to request annotations in code so it wasn't quite as useful as I would have hoped without making it specific to vscode.
I use helix and am experienced with FP so I quite quickly realised I didn't really care that much :sweat_smile:.

(if you search zulip you can probably find the conversation)

view this post on Zulip Sam Mohr (Dec 08 2024 at 07:47):

https://github.com/roc-lang/roc/pull/6466

view this post on Zulip Kasper Møller Andersen (Dec 08 2024 at 07:48):

Yeah, I think I’ve requested it before too, but the new part is allowing an external linting tool to drive the analysis :blush:

view this post on Zulip Luke Boswell (Dec 08 2024 at 08:29):

Wow, having that in the LSP would be awesome :heart_eyes:

view this post on Zulip Brendan Hansknecht (Dec 08 2024 at 08:30):

I wonder what percent of the time we statically know uniqueness at compile time

view this post on Zulip Eli Dowling (Dec 08 2024 at 08:37):

Exactly, see I would think In most performance sensitive cases you would know at compile time. Or if you don't know at compile time that in itself would be a big clue for what has gone wrong.

I'm guessing most of the issues are going to be, I'm accidentally logging this thing after it's used, or, I'm accidentally using the old buffer instead of the new one. Type problems. Which we would know about statically


Last updated: Jun 16 2026 at 16:19 UTC