Hi! I am poking at lower-level stuff for the first time, so apologies if this is obvious.
I'm working with a Zig-based nightly 2026-04-10 on a platform experiment (Rust host) that wraps a data-processing engine over an extern "C" ABI. The host does the heavy lifting - running processing loops over streams of records - and I'd like user Roc code to define the per-record transformations (let's ignore batching for now). So the natural API I reached for was a platform method that takes a lambda, something like:
Builder := [].{
map_records! : Stream, (Row -> Row) => Stream
}
and then in an app you'd write Builder.map_records!(stream, |row| { ...transform... }), and my host would call that lambda once per record.
When I tried it, the lambda seems to just... disappear. The host method gets called fine, but it only receives the non-lambda arguments - the (Row -> Row) never shows up on the host side, and the lambda body doesn't seem to run at all. I even swapped |row| row for |row| transform(row) and the compiled output was byte-for-byte identical, so it looks like the lambda argument is dropped entirely at the boundary.
I also tried to pass a top-level function, but it seemingly got elided, too. As a control, passing a simple string works as expected, so this seems to affect any function-like object.
So my questions:
run_transform! : U64, Row => Row
run_transform! = |id, row|
when id is
0 -> ...first transform...
1 -> ...second transform...
_ -> crash "unknown id"
and my host stores an id per operator, then calls back into run_transform! with that id whenever it needs to transform a record. The user-facing lambdas would get rewritten into branches of this one function under the hood.
Basically: can I pass lambdas or functions into the platform, or should I flip it around so the app provides a callback the host invokes? And if the second is the way, is there anything I should know about doing it efficiently, since this runs on a hot path (potentially hundreds of thousands of records per second)?
Thanks!
welcome! You're really diving straight into the deep end on this one...I like it! :smile:
Is passing a lambda or a function directly to a platform function a supported thing? If yes, is there a particular way I need to declare it on the platform side (some special type, or a host_* form) so the host actually receives something callable?
yeah, they have to be boxed right now - as in, Box(Row -> Row) instead of (Row -> Row)
some notes on this:
Box is the way you opt into that.Box has a stable size and alignment regardless of what it captures, because the capture is heap-allocated (which is the downside and why we make it opt-in to box closures)Box to send the lambda to/from the hostas an aside, we should definitely give a compile-time error when you try to use something unsupported (like unboxed lambdas) in the host boundary, but we don't have any validation in place for that yet - sorry about that!
as far as your high-level question about approach, I think you have the right idea! in terms of performance, I'd expect boxed lambdas to be totally fine for the use case you just described - like the app says "here is my lambda to transform this data" and then the platform hands that off to the host, which runs it a gazillion times
another thing to note: until this PR lands (I expect it to land in the next few days), LLVM optimizations are currently disabled, so roc --optimize won't work and you won't get the performance that you'll get once that lands.
anyway thanks for the detailed write-up @Karakatiza and lmk if you have any other questions!
My plan to test out the recently landed Boxed lambdas was to start poking at https://github.com/lukewilliamboswell/roc-ray again ... but it's currently on the back burner as I've been focussed on Zig 16 upgrade and also helping Richard with things.
So I don't think anyone has a platform currently which demonstrates that functionality we can point at.
Thank you for such a detailed reply! I had to compile Roc from the latest main, seems to work so far! Syntax Box((Str -> Str)) worked for me, single parentheses failed to compile. I assume the heap allocation is not too much of an overhead because of CPU caching, but we'll see. I'll share if I reach any meaningful result =)
As a part of this experiment I'm building a largish Roc platform + library - there are ~500 small helper functions plus a ~70-method "builder" pattern type. Working through different ways to structure it (Claude tried a lot of options), I kept hitting what looks like symbol count limits, and I'd love a sanity check on whether these are expected / known before I consider a proper bug report:
- A single := [].{} block seems to stop "seeing" its own imports once it has ~50+ functions — every FfiOption.x reference reports "no module FfiOption imported", even though import FfiOption is right there at the top.
- A platform seems to cap around ~350 total .{} methods across its modules before roc check panics ("reached unreachable code").
- An app seems to cap around ~200 methods pulled in (not referenced directly) across all imports before the same panic.
For now it seems I can declare all ~500 functions in a single library across multiple modules (couldn't simply within the platform), but I can only import a subset of the modules in the lib when writing the Roc app before the program stops compiling.
Are these known limits or bugs of symbol count per block / module / platform / app?
You're really diving straight into the deep end on this one
For me it's like a warzone - you fly in before you can walk :sweat_smile:
Are these known limits or bugs of symbol count per block / module / platform / app?
I think they are all bugs
for sure! @Karakatiza any chance you could give a .roc file (or a few of them) which reproduce that?
I want to get further along in my prototype to get to some working baseline, after that it should be easy to trigger these issues I encountered again, which I'd then share.
cool, thanks!
If you (or anyone) finds issues like this and makes a minimal repro and GH issue we usually can fix the bugs pretty fast.
Last updated: Jun 16 2026 at 16:19 UTC