I'm working in a Zig platform and I'm having an issue where a hosted function that returns Task I32 {}
is returning a nonsense value. I've got a barebones repro up at https://github.com/jared-cone/roc-zig (run build.sh
)
The hosted function: test : {} -> Task i32 {}
The zig function:
const RocResultI32Void = RocResult(i32, void);
export fn roc_fx_test() callconv(.C) RocResultI32Void {
var result: i32 = 500;
return RocResultI32Void.ok(result);
}
The app:
main : Task {} {}
main =
_ = Stdout.line! "Getting result..."
result = Native.test! {}
_ = Stdout.line! "Result=$(Num.toStr result)"
_ = Stdout.line! "Done"
Task.ok {}
and the output:
Launching app...
Getting result...
Result=139642271695348
Done
My only guess is something not right with the zig RocResult? Here's the implementation:
const RocResultTag = enum(u8) {
RocErr = 0,
RocOk = 1,
};
fn RocResultPayload(comptime T: type, comptime E: type) type {
return extern union {
ok: T,
err: E,
};
}
pub fn RocResult(comptime T: type, comptime E: type) type {
return extern struct {
payload: RocResultPayload(T, E),
tag: RocResultTag,
pub fn ok(value: T) @This() {
return .{ .payload = .{ .ok = value }, .tag = .RocOk };
}
pub fn err(value: E) @This() {
return .{ .payload = .{ .err = value }, .tag = .RocErr };
}
};
}
RocResultI32Void
is probably just I32
But let me dump the llvm and double check
Or actually, that wouldn't make sense cause Err {}
can be initialized and must be representable.
Oh, I see the bug.
I32
in roc, not i32
. i32
is a type variable. In this case, it is getting initialized to the default number type... I64
.
ooooh sneaky sneaky, good eye. That's working now!
I get those i32's mixed up switching between roc and zig and rust
same
then I go to c++ and cry when I need to write uint64_t
K so that may have been a red herring, it didn't fix the issue I was seeing in my non-test zig platform. I've added some more debugging to the test platform, and what I'm seeing is Roc thinks a task is failing (when it shouldn't be able to fail), unless I execute another task before it?
When everything is working:
main =
_ = Native.printNum! 1
result = Native.returnI32! 2
_ = Native.printNum! result
_ = Native.printNum! 3
Task.ok {}
the output:
(host) Launching app...
Number=1
Number=2
Number=3
(host) Exiting app... code=0
However if you comment out that first line (_ = Native.printNum! 1
):
(host) Launching app...
(host) Exiting app... code=1
That code=1
means main is returning a failed task.
So it's like... a task that returns a value will fail, unless a task that returns nothing is executed first?
This repro now is hit with your mini platform?
Yes
Should be latest on that GitHub link
Wait... isn't this a typo then? Task i32 {}
It should be Task I32 {}
.. and then the correct i32
would be generated?
The latest has capital I32
. Turns out the lowercase i32
is why the barebones test platform was printing garbage, but my other platform exiting unexpectedly is some other issue, which I was then able to repro in the barebones test platform even after changing to I32
I think maybe zig and roc disagree on how large the payload for RocResult should be. I can trick roc into not returning an error by changing the zig function to return a RocResult of i64, and a payload of -1 (all 1's so roc reads the result tag as Ok):
zig:
const RocResultTest = RocResult(i64, void);
export fn roc_fx_returnI32(_: i32) callconv(.C) RocResultTest {
return RocResultTest.ok(-1);
}
roc:
main =
# _ = Native.printNum! 1
result = Native.returnI32! 2
_ = Native.printNum! result
_ = Native.printNum! 3
Task.ok {}
output:
(host) Launching app...
Number=-1
Number=3
(host) Exiting app... code=0
RocResult
in zig is using extern union
and extern struct
which is supposed to ensure C memory layout
Apparently extern structs in zig don't zero the memory. Checking if that's the issue...
nope not that
I'm pretty sure this is #compiler development > tags and c abi
Essentially, we have a bug in our c abi handling
Let me properly file that issue
ah yes that'l do it
Changing RocResultI32Void
to RocResult(i64, void)
just on the zig side should fix the issue
Cause roc is failing to pack into a single register it ends up passing the value in the wrong layout.
General tracking issue: https://github.com/roc-lang/roc/issues/7142
yep that works!
Last updated: Jul 06 2025 at 12:14 UTC