I have a couple of advent of code solutions which have problems running with roc run --optimize
.
Day 10 returns 1 less than the correct answer about 1/3 of the time. I can only hit it with the full input, not the example. The non-optimised build is always correct.
Day 12 hangs in optimised mode. This also happens with the example data.
Using the latest nightly release
Would be great if you can make any sort of more minimal repros, but this is nice too. Must mean we have incorrectly defined behaviour that llvm is optimizing away. That or something with morphic and uniquness being off.
There is no difference in the IR for day 10 between the times the bug occurs and the times it doesn't
I did cut it down a fair bit: https://github.com/alexnuttall/aoc2024-roc/blob/main/repro/10/main.roc
There is no difference in the IR for day 10 between the times the bug occurs and the times it doesn't
There is no difference between the optimized and non-optimized llvm ir?
(deleted)
I assume the optimized memory is accessing memory it shouldn't and making a decision based on that. 1/3 of the time, it gets lucky
woah, the same executable is random (not even recompilations)...that's really cool. I've never seen that before in roc... too bad it's a bug.
Looks to be a dictionary bug.
In optimized build we are failing to find an item in the dict
Not sure what the root bug is though
Interesting... debug printing the hash fixes the bug. So some sort of optimization right around hashing is breaking things.
woah...there seem to be bad dictionary seeds that break the dictionary in roc.......
Though maybe it is just seeds that reveal a bug in the dict or hash
still hashing to the same value, so probably revealing a bug in dict
Doesn't seem like a dict bug either per say. I think this is a refcounting or morphic uniqueness bug. I think that we are mutating inplace part of the dictionary despite it being needed later
seems to happend when Dict.keepIf
is called
The dictionary metadata is incorrectly mutated in place.
Ok, this is definitely a morphic bug (or I guess bug with the info we give morphic). It is using replace_in_place
incorrectly. If I force always using replace
, the bug goes away
This is the same issue being hit by #beginners > forcing a new string allocation/copy
I wonder if this is a bug in borrow inference
cc @Folkert de Vries in case you have any ideas to help with debugging.
Also looks to be the root cause of an older dict bug: #6936
I know a workaround, but hopefully we can root cause and fix
Actually, I guess everything is owned here. So wouldn't be a bug in borrow inference. Would just be a bug in morphic or what we feed it. I wonder if there is some sort of issue with uniqueness calculation and structures. Cause the dict should always be owned.
I guess I need to dive into how morphic decides it can call the inplace version of a function cause it is getting that wrong.
Ok, I think I see something interesting in mono related to this.
The Dict related functions assume ownership of the dict (cause all functions assume ownership of their inputs excepts for raw lists and strings, which can be borrowed). All functions within dict use the parts of it in a linear fashion. This means if you pass in a unique dict, it is correct to mutate in place. As such, the List.set
calls within the dict module are set to be inplace. This is all correct assuming you always pass a unique dict into the APIs. We do not pass a unique dict into the apis. Before calling the dict function we run inc `#UserApp.map`;
. This does not give us a unique dict. It explicitly gives us a dict with 2 references. I think we have a bug with owned functions arguments + inplace mutations.
Specifically, owned != unique. Owned seems to mean that the function has it's own reference to the passed in data. So when morphic uses owned to mean unique, this leads to bugs.
I'm not sure the correct way to go about fixing this, but it feels like a pretty fundmental mis-mapping between our stack and morphic. If I am correct, all uses of InPlace today are probably not trustable. Cause they might get passed something with a refcount > 1 due to owned function args not meaning unique.
I'm hoping that only something minor is off here (and I just don't understand how the pieces fit together), but definitely worried it is a bigger issue.
I found a really solid minimal repro. It seems that specifically recursion confuses morphic here:
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br",
}
import pf.Stdout
main =
list = [123]
x = updateListBroken list 0 |> List.len
# x = updateListOk list 0 |> List.len
Stdout.line "$(Inspect.toStr list) $(Num.toStr x)"
updateListBroken = \list, i ->
if i < List.len list then
next = List.set list i 456
updateListBroken next (i + 1)
else
list
updateListOk = \list, i ->
if i < List.len list then
List.set list i 789
else
list
This leads to inplace mutation of the list and printing out 456
EDIT: just made a minor correction to make the repro actually happen.
Hoping someone with more morphic knowledge can take a look.
cc @J.Teeuwissen
I have little to no morphic knowledge, but what you stated on recounting seems to be correct. Functions that (might) modify their inputs likely have them passed as owned. To see if they can be modified in place, only the reference count should have to be checked. How is inPlace currently defined?
Thanks for the deep debug @Brendan Hansknecht :heart:
J.Teeuwissen said:
I have little to no morphic knowledge, but what you stated on recounting seems to be correct.
Sorry about the wrong ping then.... I guess your drop specialization and recounting work made me assume you had context on morphic...
To see if they can be modified in place, only the reference count should have to be checked. How is inPlace currently defined?
I'm really not sure how all of this works. I thought the owned and borrowed info was passed to morphic, but that doesn't actually look to be the case. All I know is that morphic does a complex whole program analysis to attempt to find statically known unique values such that they can be updated in place without any refcount checks.
output from alias analysis debug flag. Likely holds the bug (though bug might be completely on the morphic side if this looks correct: https://gist.github.com/bhansconnect/c1e98d5ff04c567fda5dd17ff4174a13
Attempted to change the model of replace as a remove followed by an insert instead of a get followed by an update, but no luck with that.
Which I guess makes senses, cause I think the bug is with joinpoint loops, not with replace itself (though a small part of replace looks wrong too)
Sadly, this looks to be a general bug not specific to List.replace
. I can reproduce it with List.swap
as well. I assume this means it is a bug with morphic analysis for recursive code, but it still could be something we are doing wrong with passing data to/getting data from morphic. Either way, probably a harder fix.
Also, if any one else ends up looking into this, you can get a slightly more minimal repro (in terms of ir generated) by switching the code above to one the the platform switching example platforms.
cc: @Ayaz Hafiz in case you have any ideas or tips for debugging morphic.
sadly morphic is not easy to debug
easily the hardest out of any of our deps to debug
if it was easy to pull out i would suggest removing it but it's not
something needs to be done about it though because it's not maintained and it has internal bugs. It might be worth removing that and specialization inference if it makes things more stable rn.
Is morphic only for the inplace update mode currently or does it do more?
Also, what is specialization inference?
Morphic creates separate specializations of functions based on their borrow/ownership demands
so e.g. creates a separate List.map if one call site needs to borrow, and one site owns
Ah, didn't realize that, but makes sense
Also, if we just want stability for now, I could use the trivial analysis is optimized builds or just ignore update mode in place completely for now. I don't think there would be an easy way for me to just turn it off within loops for now.
Then I can just log the minimal repro in an issue for now.
Side question, do we have any "best" benchmark for me to see the perf cost of this?
Oh, random thought as to what might be going on. I wonder if morphic expects the first iteration of the loop to call the reference count checking version of the function. Then for every loop after, it would call the inplace version.
filed #7367 to track this
For now, setting morphic analysis to trivial too workaround this bug:
Brendan Hansknecht said:
limit morphic to trivial solving only to avoid the inplace mutation correctness bugs: https://github.com/roc-lang/roc/pull/7370
Surprisingly, this seems to increase performance in a number of cases by a few percent. My only guess is that the accidental mutation is leading to extra looping that shouldn't be happening. All changes to perf are within 5% and most are slightly positive.
Last updated: Jul 06 2025 at 12:14 UTC