13
$\begingroup$

This is an example for the use of Rust's macro_rules!:

macro_rules! add {
    ($a:expr,$b:expr) => {{
        $a + $b
    }};
    ($a:expr) => {{
        $a
    }};
}

A similar example for Scheme using syntax-rules could be:

(define-syntax add
  (syntax-rules ()
    ((_ a b) (+ a b))
    ((_ a)   a)))

One might think that this two language features are equivalent. But is it the case or is there a hidden difference?

$\endgroup$

1 Answer 1

14
$\begingroup$

Rust's macros are directly inspired by Scheme's macros, but they aren't the same.

First, I should clarify that while there is a formal specification to Scheme (RnRS), there are many more commonly used extended dialects, and each have their own syntax-rules implementations which are slightly different. Here I cite R6RS, Chez Scheme, Guile and Racket. I believe all the similarities and differences listed apply between Rust and any of the Scheme dialects.

Similarities

  • All have function-like syntax, but unlike functions, are evaluated at compile-time.
  • All do a sort of pattern-match on the AST provided and produce a new AST.
  • All can have multiple cases, which are tried top-to-bottom.
  • All have some form of hygiene, which prevents identifiers inside the macro from mixing with identifiers with the same name outside of the macro (Rust, R6RS, Guile, Racket).
  • All allow recursive macros (macros whose expansion calls themselves again) and nested macros (macros whose expansion defines other macros). For nested macros, there are ways to get the $ in Rust and ... in Scheme within the inner macros working correctly.
  • All have a long list of "tricks" to create complicated macros (examples in Rust, 2, Scheme, 2, 3; just Google "Rust macro tricks" and "Scheme macro tricks" for more examples).

Differences

  • Rust macros may match syntax elements of particular types (e.g. expr). These syntax elements may actually consist of multiple syntax-nodes and require further parsing before they get matched (very complicated). syntax-rules only matches individual nodes (equivalent to tt), and matching is done after all parsing (very simple).
  • Rust has "optional fragments" ($(...)?) which match zero-or-one repetition of a sequence of syntax-nodes, and $(...)+ fragments which match one-or-many. Scheme only has ... which matches zero-or-many repetitions of a syntax-node.
    • Related, Rust's repetitions can contain multiple syntax-nodes (which are matched in repeating sequence, e.g. $(a b)* matches a b a b a b), Scheme's repetitions only contain one.
    • Scheme patterns may also have a dotted-pair, which matches the remaining elements of a Scheme "list" including the dotted-pair if it ends in one. Rust has no analogue to dotted-pairs.
  • Rust has a large set of ambiguity rules due to its complicated macro parsing and forwards-compatibility with new syntax. Scheme has no ambiguity rules, because there's no ambiguity to how the macro pattern is matched, and Scheme's syntax will never change to introduce one.
  • Macros in Rust can only be local to a module, unless you use macros 2.0 which is experimental and requires nightly Rust. Scheme has let-syntax which creates its own local scope and defines macros which only apply to that scope (e.g. (let-syntax ([f (syntax-rules () [(_ x) x])]) (f 1))).
  • This is technically off-topic because it goes beyond syntax-rules and macro_rules!, but the way that Rust and Scheme let you write more complicated macros are completely different.
    • Rust's macro_rules! (AKA "Macro By Example") is very limited. If you want to "pull back the curtain" and manipulate syntax programmatically within the Rust language, you must use procedural macros. These must be defined in a separate proc-macro crate which the code which uses the macros must depend on. Moreover, proc-macros abandon Rust's powerful syntax pattern-matching and give you the raw stream of tokens to manipulate directly.
    • In contrast, Scheme has syntax-case which is what syntax-rules expands to. syntax-case still lets you pattern-match syntax, but instead of the RHS being the result AST, syntax-case contains arbitrary Scheme code which gets evaluated and produces a "syntax object" like #'(foo (bar . baz)). Macros defined with define-syntax/let-syntax and syntax-case can be used within the same file, and Racket in particular even lets you define macros for other macros within the same file using statements like begin-for-syntax (see phase levels)

syntax/parse: the real origin of Rust-style macros

Racket in particular has a newer macro system, syntax/parse, which is what specifically inspired Rust's macros. define-syntax-parse-rule is the analogue to Rust's macro_rules!.

Like macro_rules!, syntax/parse can match different kinds of syntax using :expr, :id, etc. (called syntax classes, although in Rust they're called designators). syntax/parse can also match repeating sequences of nodes using the ~seq syntax pattern.

But syntax/parse is much more expressive than macro_rules!. You can define your own syntax classes, and there are other syntax patterns; e.g. ~or lets you match local alternatives (e.g. (number (~or 1 2 3)) matches (number 1) and (number 3)), and ~not creates a macro which matches any syntax node except ones which match its argument. Moreover, syntax/parse provides syntax-parse, the analogue to syntax-case, which lets you evaluate arbitrary Racket code when producing the result syntax.


Main references

$\endgroup$
4
  • $\begingroup$ Scheme requires additional parenthesis: (a b) ... instead of $(a b)*. $\endgroup$
    – ceving
    Nov 19, 2023 at 17:58
  • $\begingroup$ The difference is that (a b) is one syntax node. In Scheme you can't match repetitions without the parenthesis (e.g. a b a b a b) without using syntax-case and manually checking, you can only match something like (a b) (a b) (a b) $\endgroup$
    – tarzh
    Nov 19, 2023 at 18:09
  • 2
    $\begingroup$ A crucial missing citation here is syntax-parse, which was presented in the paper Fortifying Macros. syntax-parse is a descendant of syntax-case that supports both repetitions without parentheses and matching syntax elements of particular types (and even uses the same colon syntax for the latter). It was a direct inspiration for those aspects of the Rust macro system. $\endgroup$
    – Alexis King
    Nov 19, 2023 at 19:58
  • $\begingroup$ Thanks, I added it as a new section $\endgroup$
    – tarzh
    Nov 19, 2023 at 21:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .