Пользовательские литералы через макросы Rust? - PullRequest
5 голосов
/ 21 марта 2020

Возможно ли в Rust определить макрос, который может анализировать пользовательские литералы, например, что-то вроде

vector!(3x + 15y)

Чтобы уточнить, я хотел бы иметь возможность приблизиться к синтаксису выше как можно (в пределах возможного, конечно).

1 Ответ

5 голосов
/ 21 марта 2020

Я собираюсь предположить, что под «пользовательским литералом» вы подразумеваете «обычный литерал Rust (исключая необработанные литералы), за которым сразу следует пользовательский идентификатор». Это включает в себя:

  • "str"x, строковый литерал "str" с пользовательским суффиксом x
  • 123x, числовой c литерал 123 с пользовательским суффиксом x
  • b"bytes"x, байтовый литерал b"bytes" с пользовательским суффиксом x

Если приведенное выше определение достаточно для вас, то вам повезло, так как все вышеперечисленное действительно является действительными буквенными токенами в Rust, согласно ссылка на Rust :

Суффикс - это необработанный идентификатор сразу (без пробела) после основной части. литерала.

Любой тип литерала (строка, целое число и т. д. c) с любым суффиксом является допустимым в качестве токена и может быть передан макросу без выдачи ошибки. Сам макрос решает, как интерпретировать такой токен и выдавать ошибку или нет.

Однако суффиксы на литеральных токенах, анализируемых как код Rust, ограничены. Любые суффиксы отклоняются на нечисловых c литеральных токенах, а числовые c литеральные токены принимаются только с суффиксами из списка ниже.

Так что Rust явно позволяет макросы для поддержки пользовательских строковых литералов.

Теперь, как бы вы go написали такой макрос? Вы не можете написать декларативный макрос с macro_rules!, так как невозможно обнаружить и манипулировать пользовательскими литералами с помощью простого сопоставления с образцом. Тем не менее, можно написать процедурный макрос , который делает это.

Я не буду go вдаваться в подробности о том, как писать процедурные макросы, так как это было бы слишком много, чтобы написать в одном ответе StackOverflow. Тем не менее, я дам вам этот пример процедурного макроса, который делает что-то в соответствии с тем, что вы просили, в качестве отправной точки. Он принимает любые пользовательские целочисленные литералы 123x или 123y в данном выражении и преобразует их в вызовы функций x_literal(123) и y_literal(123) вместо:

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{
    parse_macro_input, parse_quote,
    visit_mut::{self, VisitMut},
    Expr, ExprLit, Lit, LitInt,
};


// actual procedural macro
#[proc_macro]
pub fn vector(input: TokenStream) -> TokenStream {
    let mut input = parse_macro_input!(input as Expr);
    LiteralReplacer.visit_expr_mut(&mut input);
    input.into_token_stream().into()
}

// "visitor" that visits every node in the syntax tree
// we add our own behavior to replace custom literals with proper Rust code
struct LiteralReplacer;

impl VisitMut for LiteralReplacer {
    fn visit_expr_mut(&mut self, i: &mut Expr) {
        if let Expr::Lit(ExprLit { lit, .. }) = i {
            match lit {
                Lit::Int(lit) => {
                    // get literal suffix
                    let suffix = lit.suffix();
                    // get literal without suffix
                    let lit_nosuffix = LitInt::new(lit.base10_digits(), lit.span());

                    match suffix {
                        // replace literal expression with new expression
                        "x" => *i = parse_quote! { x_literal(#lit_nosuffix) },
                        "y" => *i = parse_quote! { y_literal(#lit_nosuffix) },
                        _ => (), // other literal suffix we won't modify
                    }
                }

                _ => (), // other literal type we won't modify
            }
        } else {
            // not a literal, use default visitor method
            visit_mut::visit_expr_mut(self, i)
        }
    }
}

Макрос, например, преобразует vector!(3x + 4y) в x_literal(3) + y_literal(4).

...