Macro identifier, macro_rules!

Consider:

lisp! { (let [x 2] (+ x _y)) }

I want to define macro where

let => _symbol(stringify!(let))
x => _symbol(stringify!(x))
_y => y

So in particular, I want the "_y" to become the rust variable "y".

Questions:

  1. Is this possible to do with macro_rules!

  2. If not, is there a way, via changing the "_y" syntax (perhaps $y, ${y}) to make this work?

No, macro_rules! macros can't disambiguate / inspect the actual shape of an identifier (except when matching against hard-coded ones).

It is quite trivial to do this with a procedural macro, though:

let ref var_name = parse_macro_input!(input as Ident).to_string();
TokenStream::From(if var_name.starts_with('_') {
    let actual_var_name = format_ident!("{}", &var_name[1 ..]);
    quote! {
        #actual_var_name
    }
} else {
    quote! {
        _symbol(#var_name)
    }
})

Yes, $y would work for a macro_rules! macro.


In all cases, you'd need to implement a visitor to iterate over all tokens (or $ ident sequences of such).

2 Likes

It’ll be a lot easier if you pick some Rust operator to serve as your Lisp quote instead of putting it inside the identifier; @ is a reasonable choice because it’s unlikely to produce “local ambiguity” parse errors.

2 Likes

I am familiar with lisp style defmacro but am still grokking https://danielkeep.github.io/tlborm/book/mbe-min-captures-and-expansion-redux.html

Can you expand more on how this "@" solution would work? It sounds like you have a solution, but I am not yet familiar enough with macro_rules! to put it together.

  1. Thanks for the procedural macro solution. I appreciate it.

  2. If possible, I want to see far we can get with macro_rules! (easier to debug / pattern match), even if it means modifying the 'syntax' a bit.

  3. Can you expand on the solution you have in mind?

Not sure if this is useful. This is what I have so far:

#[derive(Clone, Debug)]
pub struct LispExprIf {
    test: Rc<LispExpr>,
    body_true: Rc<LispExpr>,
    body_false: Rc<LispExpr>,
}

#[derive(Clone, Debug)]
pub struct LispExprLet {
    bindings: Vec<(LispExpr, LispExpr)>,
    body: Vec<LispExpr>,
}

#[derive(Clone, Debug)]
pub enum LispExpr {
    I64(i64),
    F32(f32),
    Bool(bool),
    String(String),
    Symbol(String),
    If(LispExprIf),
    Let(LispExprLet),
}

impl From<i32> for LispExpr {
    fn from(x: i32) -> Self {
        LispExpr::I64(x as i64)
    }
}

impl From<&str> for LispExpr {
    fn from(x: &str) -> Self {
        LispExpr::String(String::from(x))
    }
}

macro_rules! lisp {
    () => {};
    ($e:literal) => {
        LispExpr::from($e)
    };
}

#[test]
fn test_00() {
    let t = lisp! {
      23
    };

    let t2 = lisp! {
      "asdf"
    };
}

Small diff:

pub trait LispObjectT {}

#[derive(Clone, Debug)]
pub enum LispExpr {
    I64(i64),
    F32(f32),
    Bool(bool),
    String(String),
    Symbol(String),
    If(LispExprIf),
    Let(LispExprLet),
    Object(Rc<dyn LispObjectT>),
}

This is where the "inject Rust object" part comes in.

A macro_rules-based traditional Lisp parser would look something like this:

enum LispValue {
    Nil,
    Pair(Box<(LispValue, LispValue)>),
    Atom(String),
}

macro_rules! term {
    (@ $x:ident) => {LispValue::from($x)};
    (($($tt:tt)*)) => {sexpr!{ $($tt)* }};
    ($tt:tt) => {LispValue::Atom(String::from(stringify!($tt)))};
}

macro_rules! sexpr {
    () => { LispValue::Nil };
    ( @ $x:ident $($rest:tt)* ) => { LispValue::Pair(Box::new((term!(@$x), sexpr!{$($rest)*}))) };
    ( $x:tt $($rest:tt)* ) => { LispValue::Pair(Box::new((term!($x), sexpr!{$($rest)* }))) };
}
2 Likes

@2e71828 : Thanks!

One more clarification:
Consider (quasiquote (+ (unquote x) (unquote y))) vs (quasiquote (+ ~x ~y))

unquote would be a "regular" macro, whereas "~" would be, I believe, a "reader macro"

This "@" behaves like "~" rather than "unquote" right? (Initially, I was surprised by the separate rules).

That sounds about right. This @ breaks out of the lisp parser temporarily and gets a rust value, which is pasted in as a syntax element. If you want to prevent lisp attempting to evaluate it, you’ll need to use (quote @rust_var) or the equivalent in your language. The macro wouldn’t be too hard to modify so that the value is always wrapped like this, if that’s the behavior you want.

Similarly, you could add a rule for @ $code:block $($rest:tt)* to allow arbitrary inline rust code instead of just a variable reference.

2 Likes

(I think) everything works now. Thanks for all your help. This is a very clever solution, the type of thing that is obvious-in-retrospect, but also not-obvious-if-possible until seeing the solution.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.