I just wanted to ask, given Luau’s stack-based VM architecture with its specific bytecode instruction set, how does the garbage collector’s tri-color marking algorithm interact with the VM’s register allocation strategy when handling deeply nested coroutine resumption chains that contain upvalue captures across multiple lexical scopes, and what are the precise memory barriers and write barrier optimizations that prevent collection of live objects during concurrent execution of multiple coroutines that share metamethod-driven proxy tables with __index and __newindex chains that recursively invoke native C API functions through Luau’s host application boundary while maintaining proper stack unwinding semantics during error propagation in Roblox’s sandboxed environment?
Not going to lie I understood like half of your question, but I can provide some info relative to your question:
(if anyone wants to fact check me please do so, I’m going off memory / some light research)
The garbage collector (as far as I’m aware) is reference based, meaning that as long as a value is referenced anywhere, it won’t be garbage collected (this can best be seen in weak tables containing instances referenced by variables anywhere else on the VM not being garbage collected). As for how memory is allocated, you’d have to ask a Roblox staff member as that’s more under-the-hood stuff and isn’t well documented (the only real information we have about this is that tables are always stored on the heap, and tuples are typically stored on the stack).
As for how metatable proxy chains are regulated, Roblox just has a maximum recursion limit present on most (if not all) metamethods. If this becomes an issue for you, consider using rawget and rawset.
Also coroutines don’t tend to cause traditional multithreading related issues as coroutines don’t actually run in direct parallel (see parallel lua for this). Instead, each coroutine’s runtime is scheduled individually by the task scheduler as needed.
The more complex stuff you’re asking about should be forwarded to a Roblox staff member, or you can take a look at the public Luau runtime repo on GitHub here.
anyway. First things first - upvalues are references, they are not values in themselves.
Coroutines are all threads running in serial. This is very important as it removes race conditions because tasks run interweaved with one another.
Now - the tri-colour garbage collector. Imagine you have 3 colours:
Black - Marks values that are definitely alive and have been fully scanned. Grey - Marks values that are alive, but are not yet scanned - so it is unknown what they reference. White - Values that haven’t been visited this GC cycle, effectively thought as unreferenced.
Garbage collector cycle begins:
All values are marked as white
Definite values, such as globals and definite stack frames, are marked as grey (definitely alive, but not scanned)
GC scans all of these grey values recursively, marking each one at black, until it finds one with no more references.
so you effectively end up with:
All referenced values are marked as black
All unreferenced values are marked as white
but now if a write operation comes into play, e.g. someTable["hi"] = someTableThatWasAboutToBeCollected
a write barrier detects that a value about to be collected, i.e. someTableThatWasAboutToBeCollected, has just been referenced. So, it marks it as grey, meaning it will not be collected that cycle.
Finally, the cycle ends with all values marked as white being collected.
The cycle then repeats, and as a result that value marked as grey is re-scanned and marked as black, meaning it won’t be GC’d while it’s alive.
This is the same for metamethods and proxies - the GC doesn’t care why it’s alive, only that it is. So, if the metatable is reachable, the metamethods won’t be GC’d because there’s still a valid path open to reach them.
Given that Luau coroutines run serially as you mentioned, how does the garbage collector handle the specific case where a coroutine yields in the middle of a metamethod call that’s modifying a weak table, and that weak table’s __mode is set to ‘k’ (weak keys), while simultaneously another coroutine resumes and triggers a different metamethod on the same weak table that creates a new key reference - what’s the exact ordering of write barrier invocations and weak reference processing during the GC cycle, and how does Luau’s incremental GC step timing interact with the coroutine scheduler to prevent the weak key from being collected between the yield and resume points?
Write barriers will only be invoked for the table itself, because the keys are marked as weak, meaning the GC doesn’t care about preserving the values through the table - it’s told to not treat them as strong references.
As for coroutines, the key will persist between yielding and resumption provided that the key is within the stack frame - the stack frame is not cleared at yielding, so the GC will still scan it and mark values with their appropriate value.
But, if you mark it as weak and only access the key/value through the table, it won’t persist because it’s the only reference to the value and it’s a weak one, the same way it would not persist with this setup in a standard function.