Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

内置 syn 与 quote!

syn 是随标准库源码发布的模块(std/std/syn.rid),quote! 是编译器内置的函数宏。两者都不属于随程序链接的 std:编译过程宏包时,clue 把 std/std/proc_macro.rid 与 std/std/syn.rid 的文本前置进宿主源码,宏包因此直接写use syn::{...}就能用,不需要在Clue.toml里声明依赖,quote!也无需导入。普通程序里既没有syn模块也没有TokenStream类型,本页片段只在过程宏包内成立,所以全部用 text 围栏。

解析入口

三个函数都返回 Result<T, Error>:

use syn::{Expr, Type, parse, parse_str};

let expr = parse::<Expr>(tokens);          // 从 TokenStream 解析
let ty = parse_str::<Type>("&mut [i32; 2]"); // 先词法分析,再解析
  • parse::<T>(tokens: TokenStream) -> Result<T, Error>:从流的开头解析一个 T。
  • parse_str::<T>(source: &str) -> Result<T, Error>:字符串先过 TokenStream::from_str,词法错误(LexError)转成带位置的 Error。
  • ParseStream::parse::<T>() -> Result<T, Error>:从当前游标继续解析,用在自定义 Parse 里。

实现了 Parse 的类型只有七个:

类型接受的输入
DeriveInputstruct 或 enum 的完整定义
File一串条目与语句
Item一个顶层条目
Stmt一条语句
Expr一个表达式
Type一个类型
Pat一个模式

Error 的两个字段都是公开的:

pub struct Error {
    pub span: Span,
    pub message: String,
}

Error::new(span, message)构造,error.emit()把它作为 error 级诊断交给编译器。解析失败时先emit()再返回空TokenStream,调用点就会看到一条带位置的错误,而不是“宏没有输出”。

ParseStream

ParseStream保留整个 token 流和一个游标:

pub struct ParseStream {
    tokens: TokenStream,
    index: usize,
}
方法返回行为
new(tokens)ParseStream游标置于开头
is_empty()bool游标是否已经越过最后一个 token
peek_ident(expected)bool下一个 token 是否是同名标识符,不移动游标
peek_punct(expected)bool下一个 token 是否是给定标点,不移动游标
span()Span当前 token 的跨度;已到末尾时是Span::call_site()
next()Option<TokenTree>取出下一个 token 并前移游标
remaining()TokenStream游标之后的全部 token 副本,不移动游标
parse::<T>()Result<T, Error>把游标交给 T 的 Parse 实现

接口直接操作结构化 token,不做字符串切分。

自定义 Parse

自定义类型只要实现 Parse 就能被 parse、parse_str 和 input.parse::<T>() 解析:

use syn::{Error, Parse, ParseStream};

struct NameInput {
    name: Ident,
}

impl Parse for NameInput {
    fun parse(input: &mut ParseStream) -> Result<NameInput, Error> {
        match input.next() {
            Option::Some(TokenTree::Ident(name)) => {
                if input.is_empty() {
                    Result::Ok(NameInput { name })
                } else {
                    Result::Err(Error::new(input.span(), "expected end of input"))
                }
            },
            Option::Some(tree) => Result::Err(Error::new(tree.span(), "expected an identifier")),
            Option::None => Result::Err(Error::new(input.span(), "expected an identifier")),
        }
    }
}

Parse 的签名是 fun parse(input: &mut ParseStream) -> Result<Self, Error>,实现里写出具体类型即可。

DeriveInput

derive 宏的输入通常是整个条目,DeriveInput把它拆成可读的字段:

pub struct DeriveInput {
    pub attrs: Vector<Attribute>,
    pub vis: Visibility,
    pub ident: Ident,
    pub generics: Generics,
    pub data: Data,
    tokens: TokenStream,   // 私有:完整输入的 token
}

pub换成Visibility::Public,没有可见性修饰符则是Visibility::Inherited。Data分两支:

pub enum Data {
    Struct(DataStruct),
    Enum(DataEnum),
}

pub struct DataStruct {
    pub fields: TokenStream,     // 花括号里的原始 token
    pub named: Vector<Field>,
}

pub struct DataEnum {
    pub variants: TokenStream,   // 花括号里的原始 token
    pub items: Vector<Variant>,
}

字段与变体:

pub struct Field {
    pub attrs: Vector<Attribute>,
    pub vis: Visibility,
    pub ident: Ident,
    pub ty: Type,
    pub tokens: TokenStream,
}

pub enum Fields {
    Unit,
    Named(Vector<Field>),
    Unnamed(Vector<Type>),
}

pub struct Variant {
    pub attrs: Vector<Attribute>,
    pub ident: Ident,
    pub fields: Fields,
    pub tokens: TokenStream,
}

Attribute.tokens是两个 token:#加方括号组。要按内容判断就attr.tokens.to_string().contains("skip")。

泛型与 where 子句分开保存:

pub struct Generics {
    pub tokens: TokenStream,       // <...>,空则无泛型参数
    pub where_clause: TokenStream, // where ...,空则没有
    pub params: Vector<GenericParam>,
    pub predicates: Vector<WherePredicate>,
}

pub enum GenericParam {
    Type(TypeParam),
    Const(ConstParam),
}

tokens与where_clause是原样的 token,直接插进quote!就能还原出impl<T> Foo<T> where T: Copy这样的头部。

DeriveInput的私有字段tokens保存完整输入,to_token_stream()返回它的副本,ToTokens也写同一份内容。用结构体模式拆DeriveInput会漏掉这个私有字段:当前实现下拆完再移动data会让宏进程卡住,直到单次展开的 10 秒超时,因此按字段取值——parsed.ident、parsed.generics.tokens、&parsed.data。

其他语法节点

Item、Stmt、Expr、Type、Pat 会校验语法并记录类别,每个变体存的是该节点的完整 token:

节点变体
ItemModule、Use、Function、Struct、Enum、Trait、Impl、Const、TypeAlias、Extern
StmtItem、Local、Expr、Break、Continue、Return
ExprLiteral、Path、Block、Tuple、Array、Struct、Call、Field、Index、Unary、Binary、Cast、Try、Closure、If、While、For、Match、Unsafe、Macro
TypePath、Reference、Pointer、Tuple、Array、Const、Never、ImplTrait、Macro
PatWildcard、Literal、Tuple、Struct、Enum、Binding、Reference、Macro

这些节点没有字段级 AST:Expr::Binary只知道自己是二元表达式,左右操作数要从 token 里读,或者交给 Visit、Fold 递归处理。Stmt是唯一嵌套别的节点的变体(Stmt::Item持有Item)。

ToTokens

ToTokens是往TokenStream写 token 的接口,syn重导出了它:

pub trait ToTokens {
    fun to_tokens(&self, output: &mut TokenStream);
}
use syn::{Expr, ToTokens, parse_str};

let expr = parse_str::<Expr>("value + 1").unwrap();
let mut output = TokenStream::new();
expr.to_tokens(&mut output);

syn为这些类型实现了它:File、Item、Stmt、Expr、Type、Pat、Attribute、Field、Variant、Fields、DataStruct、DataEnum、Data、Visibility、Generics、GenericParam、TypeParam、ConstParam、WherePredicate、DeriveInput。proc_macro一侧的TokenStream、TokenTree、Group、Ident、Punct、Literal、String、str同样实现了它,所以这些值都能直接用在quote!的#后面。

遍历:Visit

Visit按借用遍历。七个方法都有默认实现,只覆盖需要观察的节点,并在自定义实现里调用对应的walk_*继续递归:

pub trait Visit {
    fun visit_file(&mut self, node: &File) { walk_file(self, node); }
    fun visit_item(&mut self, node: &Item) { walk_item(self, node); }
    fun visit_stmt(&mut self, node: &Stmt) { walk_stmt(self, node); }
    fun visit_expr(&mut self, node: &Expr) { walk_expr(self, node); }
    fun visit_type(&mut self, node: &Type) { walk_type(self, node); }
    fun visit_pat(&mut self, node: &Pat) { walk_pat(self, node); }
    fun visit_derive_input(&mut self, node: &DeriveInput) { walk_derive_input(self, node); }
}
use syn::{Expr, Type, Visit, parse_str};

struct Counter {
    exprs: usize,
    types: usize,
}

impl Visit for Counter {
    fun visit_expr(&mut self, node: &Expr) {
        self.exprs += 1usize;
        syn::walk_expr(self, node);
    }

    fun visit_type(&mut self, node: &Type) {
        self.types += 1usize;
        syn::walk_type(self, node);
    }
}

let mut counter = Counter { exprs: 0usize, types: 0usize };
counter.visit_expr(&parse_str::<Expr>("value.call(1)? + 2").unwrap());

递归函数是syn::walk_file、syn::walk_item、syn::walk_stmt、syn::walk_expr、syn::walk_type、syn::walk_pat、syn::walk_derive_input。

改写:Fold

Fold按值取得节点并返回改写后的节点。默认实现做递归,覆盖某个方法后调用同名的syn::fold_*即可回到默认递归:

use syn::{Expr, Fold, parse_str};

struct ReplaceTwo {}

impl Fold for ReplaceTwo {
    fun fold_expr(&mut self, node: Expr) -> Expr {
        let replace = match &node {
            Expr::Literal(tokens) => tokens.to_string().as_str() == "2",
            _ => false,
        };
        if replace {
            match parse_str::<Expr>("3") {
                Result::Ok(value) => { return value; },
                Result::Err(_) => {},
            }
        }
        syn::fold_expr(self, node)
    }
}

syn::fold_expr(&mut folder, node)只做默认递归,不会回调folder.fold_expr,所以不会自递归。可覆盖的方法是fold_file、fold_item、fold_stmt、fold_expr、fold_type、fold_pat、fold_derive_input,对应syn::fold_file等七个自由函数。

quote!

quote!把写在一对花括号里的 token 拼成新的TokenStream。#name插入实现了ToTokens的值:

let name = Ident::new("answer", Span::call_site());
let body = parse_str::<Expr>("40 + 2").unwrap();

let output = quote! {
    fun #name() -> i32 { #body }
};

#(...)*是重复块,*前可以放一个分隔 token:

let mut names: Vector<Ident> = Vector::new();
names.push(Ident::new("first", Span::call_site()));
names.push(Ident::new("second", Span::call_site()));

let tuple = quote! { (#(#names),*) };
// tuple.to_string() == "(first , second)"

块内有多个#name时按下标配对:

let zipped = quote! { { #(#names: #values),* } };
// zipped.to_string() == "{first : one , second : two}"

规则:

  • 重复变量要有len()和as_slice(),两次迭代之间按#name.as_slice()[i]取值,Vector<T>满足这个要求。
  • 参与同一个重复块的变量长度必须相等,否则宏进程运行时 panic(quote repetition variables have different lengths),本次展开以 E0400 失败。
  • 重复块至少要有一个#name,否则展开报错,消息以quote repetition must contain开头。
  • 分隔符只能是一个 token,写成#(...),*或#(...);*,不能是 token 序列。
  • to_string()按 token 渲染并补空格,所以比较结果时要写(first , second)这种形式。

quote!展开出的代码构造crate::TokenStream,因此它只在过程宏宿主里可用,普通程序里没有这个类型。

边界

  • 通用语法节点只保留分类后的 token,不提供与字段一一对应的 AST。
  • DeriveInput只接受struct与enum的定义形式,且必须带花括号体——Riddle 的结构体只有命名字段一种写法(struct Wrapper(i32);是解析错误),也没有 union 条目,所以Fields::Unnamed只出现在枚举变体上。
  • ParseStream只有peek_*、next、remaining、parse这几个接口,没有 Rust syn 的解析宏与组合子。
  • Span::mixed_site()当前等价于Span::call_site(),Span::join返回Option<Span>。
  • 重复语法只有*,没有?或+。