Implementing a switch-like statement with macros

hi there,

I am still learning Rust and as a self-assigned exercize to better understand macros, I am trying to implement a switch-like statement with macros.

Basically, I'd like to replicate the switch from Go, and translate this:

fn foo(cut: f64) {
    let v = 42.0;
    switch! {
        v > cut      => println!("passing cut"),
        v < -cut     => println!("case 2"),
        v == cut/2.0 => println!("special case 3"),
        v == 42.0    => println!("answer to the universe"),
        _            => println!("default case"),
    }
}

to a chain of if-else-ifs:

fn foo(cut: f64) {
    let v = 42.0;
    if v > cut {
        println!("passing cut");
    } else if v < -cut {
        println!("case 2");
    } else if v == cut / 2.0 {
        println!("special case 3");
    } else if v == 42.0 {
        println!("answer to the universe");
    } else {
        println!("default case");
    }
}

as a first stab, I tried this:

macro_rules! switch {
    ($($a:expr => $b:expr $(,)?)*) => {
        $(if $a {
            return $b;
        })*
    };
    (_ => $e:expr $(,)?) => {
        $(if true {
            return $e;
        })*
    };
}

but of course that doesn't work (I guess I am not using the correct pattern matcher to match for _):

error: in expressions, `_` can only be used on the left-hand side of an assignment
  --> main.rs:88:9
   |
88 |         _ => println!("default case"),
   |         ^ `_` not allowed here

I then tried:

macro_rules! switch {
    ($($a:expr => $b:expr,)* _ => $e:expr $(,)?) => {
        $(if $a {
            return $b;
        })*
        if true {
            return $e;
        }
    };
}

and got:

error: local ambiguity when calling macro `switch`: multiple parsing options: built-in NTs expr ('a') or 1 other option.
  --> main.rs:88:9
   |
88 |         _ => println!("default case"),
   |         ^

→ I guess macro_rules doesn't cut it for what I'd like to achieve and I would need to resort to TokenStreams ?
(we need to keep some state to remember whether we are considering the first switch-case, the last/default one or any other case. And it seems to macro_rules doesn't provide this kind of information).

→ but perhaps the more fundamental question is: can this (a switch-like statement) be achieved at all ?

thanks for your time and input.
-s

You can also write that if chain like this:

fn foo(cut: f64) {
    let v = 42.0;
    match () {
        _ if v > cut      => println!("passing cut"),
        _ if v < -cut     => println!("case 2"),
        _ if v == cut/2.0 => println!("special case 3"),
        _ if v == 42.0    => println!("answer to the universe"),
        _                 => println!("default case"),
    }
}
6 Likes

Matching unit is blasphemy! :smile:
scnr

Also v should probably be a const.

Try to put _ in the first branch. macro_rules doesn't backtrack is _ is a perfectly valid expr .

yes, I also tried that "divide and conquer" strategy with:

macro_rules! switch {
    (_ => $e:expr $(,)?) => {
        if true {
            return $e;
        }
    };
    ( $($a:expr => $b:expr,)* ) => {
        $(if $a {
            return $b;
        })*
    };
}

but got that in response:

error: in expressions, `_` can only be used on the left-hand side of an assignment
   --> main.rs:115:9
    |
115 |         _            => println!("default case"),
    |         ^ `_` not allowed here

if it's acceptable to modify the syntax a little bit (use true instead of _ for the "catch-all" guard), there's a trivial implementation:

macro_rules! switch_simplified {
	($($a:expr => $b:expr),* $(,)?) => {
		if false { unreachable!() }
		$(else if $a { $b })*
		else { unreachable!() }
	};
}

fn foo(cut: f64) {
    let v = 42.0;
    switch_simplified! {
        v > cut      => println!("passing cut"),
        v < -cut     => println!("case 2"),
        v == cut/2.0 => println!("special case 3"),
        v == 42.0    => println!("answer to the universe"),
        true         => println!("default case"),
    }
}

note, this version has a caveat: if none of the guard conditions are true, and you don't have the "catch-all" clause, you will get a panic.

if you want to keep the _, you can still use this trick, but it's a bit more involved.

UPDATE:

I was going to do a incremental muncher, then I realized I don't need to produce a "linear" chain of else if, a "nested" chain of else { if ... } is the same. so here's a recursive version of this idea:

macro_rules! switch {
	// when there's no catch-all clause, put a `unreachable!()` in the inner `else` to please the type checker
	{@recursion} => { unreachable!() };
	// the catch-all guard, must not be followed by more clauses, its value is put directly in `else`
	{@recursion _ => $b:expr, } => { $b };
	// regular case, a boolean expression followed 
	{@recursion $a:expr => $b:expr, $($rest:tt)*} => {
		if $a {
			$b
		} else {
			switch!{@recursion $($rest)*}
		}
	};
	// entry point must be the last matcher, since we use `tt`
	($($tt:tt)*) => {
		switch!{@recursion $($tt)*}
	}
}

here's a playground link, use the "Expand Macros" command in the Tools menu to see the result:

3 Likes

There are two problems with your approach:

  1. You expect that macro_rules would magically use both rules in one match.
  2. More problematic: after _ is consumed by expr it wouldn't match _ anymore!

Consider:

macro_rules! expr_or_underscore {
    (_) => {
        "underscore"
    };
    ($e:expr) => {
        "expr"
    }
}

macro_rules! expr_or_underscore2 {
    ($e:expr) => {
        expr_or_underscore!($e)
    }
}

pub fn main() {
    println!("{}", expr_or_underscore!(_));
    println!("{}", expr_or_underscore2!(_));
}

Output:

underscore
expr

You may cheat:

macro_rules! switch {
    ($($a:expr_2021 => $b:expr,)*
     _ => $f:expr,) => {
        $(if $a {
            return $b;
        })*
        if true {
            return $f;
        }
    };
}

This may accept your original example, but you would very quickly run into trouble trying to imitate lisp with macro_rules.

Instead of expr_2021 you can use some manually-written parser that would accept token trees instead (underscore/expr example works just fine if expr_or_underscore2 would use $e:tt instead of $e:expr), but this very-very quickly devolves into “don't try that in home” tricks.

If you want to do something tricky then proc_macro and syn are your friends…

1 Like

thanks all.

I think I have a better grasp of macro_rules' set of capabilities.
I'll have a look at syn as well.