Stream: beginners

Topic: `Task I32 {}` returning nonsense on zig platform


view this post on Zulip Jared Cone (Oct 06 2024 at 15:36):

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 };
        }
    };
}

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 15:49):

RocResultI32Void is probably just I32

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 15:50):

But let me dump the llvm and double check

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 15:50):

Or actually, that wouldn't make sense cause Err {} can be initialized and must be representable.

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 15:53):

Oh, I see the bug.

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 15:54):

I32 in roc, not i32. i32 is a type variable. In this case, it is getting initialized to the default number type... I64.

view this post on Zulip Jared Cone (Oct 06 2024 at 16:09):

ooooh sneaky sneaky, good eye. That's working now!

view this post on Zulip Jared Cone (Oct 06 2024 at 16:09):

I get those i32's mixed up switching between roc and zig and rust

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 16:25):

same

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 16:26):

then I go to c++ and cry when I need to write uint64_t

view this post on Zulip Jared Cone (Oct 06 2024 at 17:28):

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?

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 19:16):

This repro now is hit with your mini platform?

view this post on Zulip Jared Cone (Oct 06 2024 at 20:02):

Yes

view this post on Zulip Jared Cone (Oct 06 2024 at 20:02):

Should be latest on that GitHub link

view this post on Zulip Luke Boswell (Oct 06 2024 at 21:21):

Wait... isn't this a typo then? Task i32 {}

view this post on Zulip Luke Boswell (Oct 06 2024 at 21:22):

It should be Task I32 {} .. and then the correct i32 would be generated?

view this post on Zulip Jared Cone (Oct 06 2024 at 21:35):

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

view this post on Zulip Jared Cone (Oct 06 2024 at 21:50):

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

view this post on Zulip Jared Cone (Oct 06 2024 at 21:53):

RocResult in zig is using extern union and extern struct which is supposed to ensure C memory layout

view this post on Zulip Jared Cone (Oct 06 2024 at 21:59):

Apparently extern structs in zig don't zero the memory. Checking if that's the issue...

view this post on Zulip Jared Cone (Oct 06 2024 at 22:11):

nope not that

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 22:32):

I'm pretty sure this is #compiler development > tags and c abi

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 22:32):

Essentially, we have a bug in our c abi handling

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 22:34):

Let me properly file that issue

view this post on Zulip Jared Cone (Oct 06 2024 at 22:34):

ah yes that'l do it

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 22:40):

Changing RocResultI32Void to RocResult(i64, void) just on the zig side should fix the issue

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 22:40):

Cause roc is failing to pack into a single register it ends up passing the value in the wrong layout.

view this post on Zulip Brendan Hansknecht (Oct 06 2024 at 22:41):

General tracking issue: https://github.com/roc-lang/roc/issues/7142

view this post on Zulip Jared Cone (Oct 06 2024 at 22:44):

yep that works!


Last updated: Jul 06 2025 at 12:14 UTC