Macro miscompilation? Type error when expanding macro

⚓ Rust    📅 2025-11-22    👤 surdeus    👁️ 15      

surdeus

You can skip to the Macro Implementation below.
If you're really busy, just look at /****** LOOK HERE ******/ part in the macro_rules! __race_job and skip to the last section - The Problem.

Background information

I wanted to make a race<T>(job: Vec<RaceJob<T>>) -> (usize, T) function that does the following:

  1. Takes vector of closures
  2. Run all closures at once
  3. Wait until any closure ends, then take the result from the fastest closure
  4. Stop other closures
  5. Return the fastest closure's index and its result.

To stop other closures, RaceJob<T> is defined as &AtomicBool -> Option<T> function.
RaceJob is responsible for checking AtomicBool's value.

  1. If the AtomicBool's value is true, RaceJob should stop and return None.
  2. If the AtomicBool remained false until the job ends, RaceJob should set the AtomicBool's value to true to indicate the job is finished. After that, RaceJob returns Some(T). (This is done by compare_exchange which uses CAS operation.)

Now race can just create a AtomicBool value, spawn threads using AtomicBool and RaceJob, check if thread returned Some(T) (only the fastest thread can return it), then unwrap its value.

(Some traits are snipped for simplicity, exact definitions are type RaceJob<T> = Box<dyn FnOnce(&AtomicBool) -> Option<T> + Send + 'static> and fn race<T: Send + 'static>(jobs: Vec<RaceJob<T>>) -> (usize, T).)

Macro Implementation

To make it easier for users to use the race<T>(job: Vec<RaceJob<T>>) -> (usize, T) function, I've implemented the race! and __race_job! macro.
Ideally, instead of manually creating vector, boxes, and closures, user can just write list of closure bodies.

For starters, we have a race! macro.
race! macro takes body of closures, then create vector and boxes that wraps closures.

macro_rules! race {
    ( $( { $($body:tt)* } ),+ $(,)? ) => {{
        $crate::race(vec![
            $(
                Box::new({
                    use std::sync::atomic::{ AtomicBool, Ordering };

                    move |__is_finished: &AtomicBool| {
                        $crate::__race_job![ __is_finished; $($body)* ]
                    }
                })
            ),+
        ])
    }};
}

Specifically, race![ { code11; code12; code13 }, { code21; code22 } ] will turn into:

{race(vec![
    Box::new({
        use std::sync::atomic::{ AtomicBool, Ordering };

        move |__is_finished: &AtomicBool| {
            __race_job![ __is_finished;
                code11; code12; code13 // original code
            ]
        }
    }),
    Box::new({
        use std::sync::atomic::{ AtomicBool, Ordering };

        move |__is_finished: &AtomicBool| {
            __race_job![ __is_finished;
                code21; code22 // original code
            ]
        }
    })
])}

I know that the outmost bracket is unnecessary, but I added just in case.

We also have a __race_job! macro.
__race_job! macro will insert code that handles AtomicBool between statements.

macro_rules! __race_job {
    // recursion base case; if ends without expr, return ()
    ( $flag:ident; ) => {
        if $flag
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .is_ok()
        {
            Some(())
        } else {
            None
        }
    };

    // another recursion base case; if ends with expr, return its value
    ( $flag:ident; $last:expr ) => {{
        if $flag.load(Ordering::Acquire) {
            return None;
        }
        let __check_result = $last;

        if $flag
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .is_ok()
        {
            Some(__check_result)
        } else {
            None
        }
    }};

    // if statement exists, insert checking code in the middle
    ( $flag:ident; $head:stmt; $($tail:tt)* ) => {{
        if $flag.load(Ordering::Acquire) {
            return None;
        }

        /****** LOOK HERE ******/
        $head;
        /****** LOOK HERE ******/

        $crate::__race_job![ $flag; $($tail)* ]
    }};
} 

Specifically, __race_job![ __is_finished; code11; code12; code13 ] will turn into:

if __is_finished.load(Ordering::Acquire) { return None; }
code11; // original code
{
    if __is_finished.load(Ordering::Acquire) { return None; }
    code12; // original code
    {
        if __is_finished.load(Ordering::Acquire) { return None; }
        let __check_result = code13; // original code
        if __is_finished.compare_exchange(false, true,Ordering::AcqRel, Ordering::Acquire).is_ok() {
            Some(__check_result)
        } else {
            None
        }
    }
}

The Problem

From the definition, the following code:

let (id, winner) = race![
    {
        let mut x = 3; // stmt
        x += 5; // stmt

        if x == 3 {
            String::from("Add failed")
        } else {
            String::from("Add succeeded")
        } // expr
    },
    {
        let mut guess = String::new(); // stmt

        /****** LOOK HERE ******/
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line"); // stmt
        /****** LOOK HERE ******/

        guess //expr
    },
    {
        fn prepend_hello(name: String) -> String {
            format!("Hello, {name}!")
        }; // stmt

        let my_name = String::from("MyName"); // stmt
        prepend_hello(my_name) // expr
    }
];

should be expanded into:

let (id, winner) = {race(vec![
    Box::new({
        use std::sync::atomic::{AtomicBool, Ordering};

        move |__is_finished: &AtomicBool| {
            if __is_finished.load(Ordering::Acquire) { return None; }
            let mut x = 3;
            {
                if __is_finished.load(Ordering::Acquire) { return None; }
                x += 5;
                {
                    if __is_finished.load(Ordering::Acquire) { return None; }
                    let __check_result = if x == 3 {
                        String::from("Add failed")
                    } else {
                        String::from("Add succeeded")
                    };
                    if __is_finished.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire).is_ok() {
                        Some(__check_result)
                    } else {
                        None
                    }
                }
            }
        }
    }),
    Box::new({
        use std::sync::atomic::{AtomicBool, Ordering};

        move |__is_finished: &AtomicBool| {
            if __is_finished.load(Ordering::Acquire) { return None; }
            let mut guess = String::new();
            {
                if __is_finished.load(Ordering::Acquire) { return None; }

                /****** LOOK HERE ******/
                io::stdin().read_line(&mut guess).expect("Failed to read line");
                /****** LOOK HERE ******/

                {
                    if __is_finished.load(Ordering::Acquire) { return None; }
                    let __check_result = guess;
                    if __is_finished.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire).is_ok() {
                        Some(__check_result)
                    } else {
                        None
                    }
                }
            }
        }
    }),
    Box::new({
        use std::sync::atomic::{AtomicBool, Ordering};

        move |__is_finished: &AtomicBool| {
            if __is_finished.load(Ordering::Acquire) { return None; }
            fn prepend_hello(name: String) -> String {
                format!("Hello, {name}!")
            };
            {
                if __is_finished.load(Ordering::Acquire) { return None; }
                let my_name = String::from("MyName");
                {
                    if __is_finished.load(Ordering::Acquire) { return None; }
                    let __check_result = prepend_hello(my_name);
                    if __is_finished.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire).is_ok() {
                        Some(__check_result)
                    } else {
                        None
                    }
                }
            }
        }
    }),
])};

In fact, this is the output of the cargo-expand crate, which expands every macro used in the Rust code.

From the definition of the __race_job! macro, we add a semicolon after the $head like this: $head;
Therefore, expanded code should also have io::stdin().read_line(&mut guess).expect("Failed to read line"); with a semicolon.

However, this code emits the following compile error:

error[E0308]: mismatched types
   --> src/main.rs:101:13
    |
101 | /             io::stdin()
102 | |                 .read_line(&mut guess)
103 | |                 .expect("Failed to read line"); // stmt
    | |                                              ^- help: consider using a semicolon here
    | |______________________________________________|
    |                                                expected `()`, found `usize`

For more information about this error, try `rustc --explain E0308`.

Since I'm already using a semicolon, I don't even understand why this error occurred.
I've tried running the code expanded by cargo-expand, and it works fine without any type errors.

Any idea how to convince the compiler that a semicolon should be added when expanding our macro?
You can see the full code in the playground.

2 posts - 2 participants

Read full topic

🏷️ Rust_feed