Stream: compiler development

Topic: Str -> Effect Str bug


view this post on Zulip Richard Feldman (Jul 20 2023 at 02:08):

I ran into this issue at work today, and haven't figured out a workaround; can anyone think of an idea for a workaround?

view this post on Zulip Ayaz Hafiz (Jul 20 2023 at 02:16):

can you dump the llvm ir for both rust and the roc program? it's likely an incompatibility there

view this post on Zulip Richard Feldman (Jul 20 2023 at 23:54):

roc_fx_envVar on the roc side:

declare void @roc_fx_envVar(%str.RocStr* sret(%str.RocStr), { i8*, i64, i64 }*)

on the rust side:

; Function Attrs: uwtable
define void @roc_fx_envVar(ptr sret(%"roc_std::roc_str::RocStr") %0, ptr align 8 %1) unnamed_addr #1 !dbg !10953 {
start:
  %_18 = alloca [1 x { ptr, ptr }], align 8
  %_11 = alloca %"core::fmt::Arguments<'_>", align 8
  %_3 = alloca %"core::fmt::Arguments<'_>", align 8
  %roc_str = alloca ptr, align 8
  store ptr %1, ptr %roc_str, align 8
  call void @llvm.dbg.declare(metadata ptr %roc_str, metadata !10957, metadata !DIExpression()), !dbg !10958
; call core::fmt::Arguments::new_v1
  call void @_ZN4core3fmt9Arguments6new_v117hc9cd7c67f24d5224E(ptr sret(%"core::fmt::Arguments<'_>") %_3, ptr align 8 @all
oc1112, i64 1, ptr align 8 @alloc1114, i64 0), !dbg !10959
; call std::io::stdio::_print
  call void @_ZN3std2io5stdio6_print17h8769d5b8d07b47a7E(ptr %_3), !dbg !10959
; call core::fmt::ArgumentV1::new_debug
  %2 = call { ptr, ptr } @_ZN4core3fmt10ArgumentV19new_debug17h70af07452f17459eE(ptr align 8 %roc_str), !dbg !10960
  %_19.0 = extractvalue { ptr, ptr } %2, 0, !dbg !10960
  %_19.1 = extractvalue { ptr, ptr } %2, 1, !dbg !10960
  %3 = getelementptr inbounds [1 x { ptr, ptr }], ptr %_18, i64 0, i64 0, !dbg !10960
  %4 = getelementptr inbounds { ptr, ptr }, ptr %3, i32 0, i32 0, !dbg !10960
  store ptr %_19.0, ptr %4, align 8, !dbg !10960
  %5 = getelementptr inbounds { ptr, ptr }, ptr %3, i32 0, i32 1, !dbg !10960
  store ptr %_19.1, ptr %5, align 8, !dbg !10960
; call core::fmt::Arguments::new_v1
  call void @_ZN4core3fmt9Arguments6new_v117hc9cd7c67f24d5224E(ptr sret(%"core::fmt::Arguments<'_>") %_11, ptr align 8 @alloc1117, i64 2, ptr align 8 %_18, i64 1), !dbg !10960
; call std::io::stdio::_print
  call void @_ZN3std2io5stdio6_print17h8769d5b8d07b47a7E(ptr %_11), !dbg !10960
; call <roc_std::roc_str::RocStr as core::default::Default>::default
  call void @"_ZN67_$LT$roc_std..roc_str..RocStr$u20$as$u20$core..default..Default$GT$7default17h4cb6e5af3316de95E"(ptr sret(%"roc_std::roc_str::RocStr") %0), !dbg !10961
  ret void, !dbg !10962
}

view this post on Zulip Richard Feldman (Jul 20 2023 at 23:58):

so they're both void and have the same name, so the differences look like:

first argument: (pointer to returned RocStr)
Rust: ptr sret(%"roc_std::roc_str::RocStr") %0
Roc: %str.RocStr* sret(%str.RocStr)

second argument: (RocStr passed by reference)
Rust: ptr align 8 %1
Roc: { i8*, i64, i64 }*

aside from the notation differences I think those are saying the same thing, so doesn't look like a type disagreement unless I'm missing something

view this post on Zulip Richard Feldman (Jul 21 2023 at 00:07):

and it ends in what certainly appears to be calling RocStr::default(), writing it into the sret pointer (so, "returning" it), and then actually returning void

; call <roc_std::roc_str::RocStr as core::default::Default>::default
  call void @"_ZN67_$LT$roc_std..roc_str..RocStr$u20$as$u20$core..default..Default$GT$7default17h4cb6e5af3316de95E"(ptr sret(%"roc_std::roc_str::RocStr") %0), !dbg !10961
  ret void, !dbg !10962

view this post on Zulip Richard Feldman (Jul 21 2023 at 00:10):

hm, this might be something:

define internal fastcc void @roc_fx_envVar_fastcc_wrapper(%str.RocStr* %arg, %str.RocStr* %arg1) {
entry:
  %to_cc_type_ptr = bitcast %str.RocStr* %arg to { i8*, i64, i64 }*
  call void @roc_fx_envVar(%str.RocStr* %arg1, { i8*, i64, i64 }* %to_cc_type_ptr)
  ret void
}

view this post on Zulip Richard Feldman (Jul 21 2023 at 00:11):

the fastcc wrapper reverses the argument order; it takes the return pointer last rather than first

view this post on Zulip Richard Feldman (Jul 21 2023 at 00:13):

but then again, the fastcc wrapper is only called here:

define internal fastcc void @Effect_effect_closure_envVar_669c1355a3e727bb53dd458f2e96e48571aa45dfabcfb4b7de1689484f11({}
%"13", %str.RocStr* %closure_arg_envVar_0, %str.RocStr* %arg) {
entry:
  %result_value = alloca %str.RocStr, align 8
  call fastcc void @roc_fx_envVar_fastcc_wrapper(%str.RocStr* %closure_arg_envVar_0, %str.RocStr* nonnull %result_value)
  call fastcc void @"#Attr_#dec_1"(%str.RocStr* %closure_arg_envVar_0)
  %i = bitcast %str.RocStr* %arg to i8*
  %i1 = bitcast %str.RocStr* %result_value to i8*
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* noundef nonnull align 8 dereferenceable(24) %i, i8* noundef nonnull align 8 der
eferenceable(24) %i1, i64 24, i1 false)
  ret void
}

view this post on Zulip Richard Feldman (Jul 21 2023 at 00:13):

which looks to be correctly passing the return pointer last

view this post on Zulip Richard Feldman (Jul 21 2023 at 00:15):

(the full dumps, for reference:)

echo.ll
host.ll

view this post on Zulip Ayaz Hafiz (Jul 21 2023 at 00:35):

yeah our fastcc puts return-by-pointer arguments last. But C ABI wants in the first position

view this post on Zulip Ayaz Hafiz (Jul 21 2023 at 00:35):

what is the alignment of RocStr, { i8*, i64, i64 }*?

view this post on Zulip Folkert de Vries (Jul 21 2023 at 07:48):

depends on your platform, right?

view this post on Zulip Folkert de Vries (Jul 21 2023 at 07:48):

but I'd say 8 because of the i64

view this post on Zulip Richard Feldman (Jul 21 2023 at 11:37):

I'd assume 8 because it's a pointer and this is a 64-bit system

view this post on Zulip Richard Feldman (Jul 22 2023 at 18:16):

noice, after some dialogue with chatGPT, found a fix! The Rust host code was wrong; this is the corrected implementation:

#[no_mangle]
pub extern "C" fn rust_main() {
    let size = unsafe { roc_main_size() } as usize;
    let layout = Layout::array::<u8>(size).unwrap();

    unsafe {
        // TODO allocate on the stack if it's under a certain size
        let buffer = std::alloc::alloc(layout);

        roc_main(buffer);

        let closure_output = call_the_closure(buffer);
        let answer: *mut RocStr = closure_output.cast();

        println!("Answer was: {:?}", &*answer);

        std::alloc::dealloc(buffer, layout);
        std::alloc::dealloc(closure_output, layout);
    }
}

unsafe fn call_the_closure(closure_data_ptr: *const u8) -> *mut u8 {
    // return type changed
    let size = size_Fx_result() as usize;
    let layout = Layout::array::<u8>(size).unwrap();
    let buffer = std::alloc::alloc(layout) as *mut u8;

    call_Fx(
        // This flags pointer will never get dereferenced
        MaybeUninit::uninit().as_ptr(),
        closure_data_ptr as *const u8,
        buffer as *mut u8,
    );

    // Don't deallocate the buffer here
    // std::alloc::dealloc(buffer, layout);

    buffer // return the buffer
}

view this post on Zulip Richard Feldman (Jul 22 2023 at 18:16):

so I need to update glue accordingly

view this post on Zulip Richard Feldman (Jul 23 2023 at 00:10):

for my own future reference, the glue we want to generate:

pub extern "C" fn rust_main() {
    let answer = mainForHost().force_thunk();

    println!("Answer was: {:?}", unsafe { answer });
}

#[repr(C)]
#[derive(Debug, Clone)]
pub struct RocFunction_71 {
    closure_data: roc_std::RocList<u8>,
}

impl RocFunction_71 {
    pub fn force_thunk(self) -> roc_std::RocStr {
        extern "C" {
            fn roc__mainForHost_0_caller(
                arg0: *const (),
                closure_data: *mut u8,
                output: *mut roc_std::RocStr,
            );

            fn roc__mainForHost_0_result_size() -> i64;
        }

        let mut output = MaybeUninit::uninit();
        let closure_ptr =
            (&mut core::mem::ManuallyDrop::new(self.closure_data)) as *mut _ as *mut u8;

        unsafe {
            roc__mainForHost_0_caller(&(), closure_ptr, output.as_mut_ptr());

            output.assume_init()
        }
    }
}

pub fn mainForHost() -> RocFunction_71 {
    extern "C" {
        fn roc__mainForHost_1_exposed_generic(_: *mut RocFunction_71);
    }

    let mut ret = core::mem::MaybeUninit::uninit();

    unsafe {
        roc__mainForHost_1_exposed_generic(ret.as_mut_ptr());

        ret.assume_init()
    }
}

Last updated: Jul 06 2025 at 12:14 UTC