Plugins del Compilador

Introducción

rustc puede cargar plugins de compilador, que son bibliotecas de usuario que extienden el comportamiento del compilador con nuevas extensiones de sintaxis, lints, etc.

Un plugin puede ser una biblioteca dinámica con una función registradora designada se encarga registrar la extension con rustc. Otros crates pueden cargar estas extensiones a través del uso del atributo #![plugin(...)]. Dirígete a la documentación de rustc_plugin para mayor detalle acerca de la mecánica de la definición y carga de un plugin.

De estar presentes, los argumentos pasados como #![plugin(foo(... args ...))] no son interpretados por rustc. Son proporcionados al plugin a través del método args de Registry.

En la gran mayoría de los casos, un plugin solo debería ser usado a través de #![plugin] y no por medio de un item extern crate. Enlazar un plugin traería a libsyntax y librustc como dependencias de tu crate. Esto es generalmente indeseable a menos que estés construyendo un plugin. El lint plugin_as_library chequea estos lineamientos.

La practica usual es colocar a los plugins del compilador en su propio crate, separados de cualquier macro macro_rules! o código Rust ordinario que vayan a ser usados por los consumidores de la biblioteca.

Extensiones de sintaxis

Los plugins pueden extender la sintaxis de Rust de varias maneras. Una forma de extension de sintaxis son las macros procedurales. Estas son invocadas de la misma forma que las macros ordinarias, pero la expansión es llevada a cabo por código Rust arbitrario que manipula arboles de sintaxis en tiempo de compilación.

Escribamos un plugin roman_numerals.rs que implementa literales de enteros con números romanos.

fn main() { #![crate_type="dylib"] #![feature(plugin_registrar, rustc_private)] extern crate syntax; extern crate rustc; extern crate rustc_plugin; use syntax::codemap::Span; use syntax::parse::token; use syntax::ast::TokenTree; use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager}; use syntax::ext::build::AstBuilder; // trait para expr_usize use rustc_plugin::Registry; fn expand_rn(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> { static NUMERALS: &'static [(&'static str, usize)] = &[ ("M", 1000), ("CM", 900), ("D", 500), ("CD", 400), ("C", 100), ("XC", 90), ("L", 50), ("XL", 40), ("X", 10), ("IX", 9), ("V", 5), ("IV", 4), ("I", 1)]; if args.len() != 1 { cx.span_err( sp, &format!("argument should be a single identifier, but got {} arguments", args.len())); return DummyResult::any(sp); } let text = match args[0] { TokenTree::Token(_, token::Ident(s, _)) => s.to_string(), _ => { cx.span_err(sp, "argument should be a single identifier"); return DummyResult::any(sp); } }; let mut text = &*text; let mut total = 0; while !text.is_empty() { match NUMERALS.iter().find(|&&(rn, _)| text.starts_with(rn)) { Some(&(rn, val)) => { total += val; text = &text[rn.len()..]; } None => { cx.span_err(sp, "invalid Roman numeral"); return DummyResult::any(sp); } } } MacEager::expr(cx.expr_usize(sp, total)) } #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { reg.register_macro("rn", expand_rn); } }
#![crate_type="dylib"]
#![feature(plugin_registrar, rustc_private)]

extern crate syntax;
extern crate rustc;
extern crate rustc_plugin;

use syntax::codemap::Span;
use syntax::parse::token;
use syntax::ast::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::ext::build::AstBuilder;  // trait para expr_usize
use rustc_plugin::Registry;

fn expand_rn(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree])
        -> Box<MacResult + 'static> {

    static NUMERALS: &'static [(&'static str, usize)] = &[
        ("M", 1000), ("CM", 900), ("D", 500), ("CD", 400),
        ("C",  100), ("XC",  90), ("L",  50), ("XL",  40),
        ("X",   10), ("IX",   9), ("V",   5), ("IV",   4),
        ("I",    1)];

    if args.len() != 1 {
        cx.span_err(
            sp,
            &format!("argument should be a single identifier, but got {} arguments", args.len()));
        return DummyResult::any(sp);
    }

    let text = match args[0] {
        TokenTree::Token(_, token::Ident(s, _)) => s.to_string(),
        _ => {
            cx.span_err(sp, "argument should be a single identifier");
            return DummyResult::any(sp);
        }
    };

    let mut text = &*text;
    let mut total = 0;
    while !text.is_empty() {
        match NUMERALS.iter().find(|&&(rn, _)| text.starts_with(rn)) {
            Some(&(rn, val)) => {
                total += val;
                text = &text[rn.len()..];
            }
            None => {
                cx.span_err(sp, "invalid Roman numeral");
                return DummyResult::any(sp);
            }
        }
    }

    MacEager::expr(cx.expr_usize(sp, total))
}

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("rn", expand_rn);
}

Entonces podemos hacer uso de rn!() como cualquier otra macro:

#![feature(plugin)] #![plugin(roman_numerals)] fn main() { assert_eq!(rn!(MMXV), 2015); }
#![feature(plugin)]
#![plugin(roman_numerals)]

fn main() {
    assert_eq!(rn!(MMXV), 2015);
}

Las ventajas de esto por encima de una simple fn(&str) -> u32 son:

En adicion a las macros procedurales, puedes definir attributos derive y otros tipos de expansiones. Dirigete a Registry::register_syntax_extension y a el enum SyntaxExtension. Para un ejemplo mas involucrado con macros, ve a regex_macros.

Tips y trucos

Algunos de los tips de depuración de macros son aplicables.

Puedes usar syntax::parse para transformar arboles de tokens en elementos de sintaxis de mas alto nivel, como expresiones:

fn main() { fn expandir_foo(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult+'static> { let mut parser = cx.new_parser_from_tts(args); let expr: P<Expr> = parser.parse_expr(); }
fn expandir_foo(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree])
        -> Box<MacResult+'static> {

    let mut parser = cx.new_parser_from_tts(args);

    let expr: P<Expr> = parser.parse_expr();

Mirar a el código del parser libsyntax te dara una idea de como funciona la infraestructura de parseo.

Mantén los Spans de todo lo que parsees, para un mejor reporte de errores. Puedes envolver Spanned alrededor de tus estructuras de datos,

Llamar a ExtCtxt::span_fatal abortara de manera inmediata la compilación. Es mejor llamar a ExtCtxt::span_err retornando un DummyResult, de manera que el compilador pueda continuar encontrando mas errores.

Para imprimir fragmentos de sintaxis para depuración, pudes usar span_note en conjunción con syntax::print::pprust::*_to_string.

El ejemplo anterior produjo un literal de entero usando AstBuilder::expr_usize. Como alternativa a el trait AstBuilder, libsyntax proporciona un conjunto de macros quasiquote. Estos están indocumentados y un poco rústicos en los bordes. Sin embargo, la implementación puede ser un buen punto de partida para un quasiquote mejorado como una biblioteca plugin ordinaria.

Plugins lint

Los plugins pueden extender la infraestructura de lints de Rust con chequeos adicionales para estilo de código, seguridad, etc. Escribamos ahora un plugin lint_plugin_test.rs que nos advierte acerca de cualquier item llamado lintme.

#![feature(plugin_registrar)] #![feature(box_syntax, rustc_private)] fn main() { extern crate syntax; // Cargando rustc como un plugin para obtener las macros #[macro_use] extern crate rustc; extern crate rustc_plugin; use rustc::lint::{EarlyContext, LintContext, LintPass, EarlyLintPass, EarlyLintPassObject, LintArray}; use rustc_plugin::Registry; use syntax::ast; declare_lint!(TEST_LINT, Warn, "Warn about items named 'lintme'"); struct Pass; impl LintPass for Pass { fn get_lints(&self) -> LintArray { lint_array!(TEST_LINT) } } impl EarlyLintPass for Pass { fn check_item(&mut self, cx: &EarlyContext, it: &ast::Item) { if it.ident.name.as_str() == "lintme" { cx.span_lint(TEST_LINT, it.span, "item is named 'lintme'"); } } } #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { reg.register_early_lint_pass(box Pass as EarlyLintPassObject); } }
#![feature(plugin_registrar)]
#![feature(box_syntax, rustc_private)]

extern crate syntax;

// Cargando rustc como un plugin para obtener las macros
#[macro_use]
extern crate rustc;
extern crate rustc_plugin;

use rustc::lint::{EarlyContext, LintContext, LintPass, EarlyLintPass,
                  EarlyLintPassObject, LintArray};
use rustc_plugin::Registry;
use syntax::ast;

declare_lint!(TEST_LINT, Warn, "Warn about items named 'lintme'");

struct Pass;

impl LintPass for Pass {
    fn get_lints(&self) -> LintArray {
        lint_array!(TEST_LINT)
    }
}

impl EarlyLintPass for Pass {
    fn check_item(&mut self, cx: &EarlyContext, it: &ast::Item) {
        if it.ident.name.as_str() == "lintme" {
            cx.span_lint(TEST_LINT, it.span, "item is named 'lintme'");
        }
    }
}

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_early_lint_pass(box Pass as EarlyLintPassObject);
}

Entonces código como

fn main() { #![plugin(lint_plugin_test)] fn lintme() { } }
#![plugin(lint_plugin_test)]

fn lintme() { }

producirá una advertencia del compilador:

foo.rs:4:1: 4:16 warning: item is named 'lintme', #[warn(test_lint)] on by default
foo.rs:4 fn lintme() { }
         ^~~~~~~~~~~~~~~

Los componentes de un plugin lint son:

Los passes lint son recorridos de sintaxis, pero corren en una etapa de la compilación en la que la información de tipos esta disponible. Los lints integrados de Rust usan mayormente la infraestructura de los plugins lint, y proveen ejemplos de como acceder a la información de tipos.

Los lints definidos por plugins son controlados por los flags y atributos usuales del compilador, e.j #[allow(test_lint)] o -A test-lint. estos identificadores son derivados del primer argumentos a declare_lint!, con las conversiones de capitalización y puntuación apropiadas.

Puedes ejecutar rustc -W help foo.rs para ver una lista de los lints que rustc conoce, incluyendo aquellos proporcionados por plugins cargados por foo.rs.