diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5dcf7491e9a9b767e7108f5eff6dbf7bd72d37ee..9566276dc6981ac7556807af8503b9ea64af20e9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,8 +5,20 @@ "tasks": [ { "type": "shell", - "label": "cargo run --example ex1", - "command": "cargo run --example ex1", + "label": "cargo run --example ex_lit", + "command": "cargo run --example ex_lit", + "problemMatcher": [ + "$rustc" + ], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "type": "shell", + "label": "cargo run --example ex_lite", + "command": "cargo run --example ex_lite", "problemMatcher": [ "$rustc" ], diff --git a/README.md b/README.md index 5c5b7b9312c8362c5a695bc56a8505808cc59af9..089d1e65ea72262a382895ff05f96df92551890a 100644 --- a/README.md +++ b/README.md @@ -4,67 +4,71 @@ Disclaimer, I'm NOT the author of the `syn` and `nom` parsing frameworks, so no The same goes for all references to supporting crates and references to other Rust libraries, crates and the language itself, so use material at own risk :) -## Parsing a `LitInt` and rejecting invalid range -As a simple example let us start out with `mylit!(LitInt)`, where +## Built in parsers + +The `syn::parse` function allows to parse any struct that implements the `syn::synom::Synom` trait. The `syn` crate provides a large set of structs which satisfies this requirement, which we can use directly or for building parser combinators. + +We can invoke custom parsers through procedural macros in Rust. + +As a simple example let us start out with simple macro `lit!` parsing a `LitInt`, where `LitInt` should be an intereger literal `x` in the range `10 <= x < 100`. -Consider the following program (`ex1.rs`): +Consider the following program (`ex_lit.rs`): ``` rust #![feature(proc_macro)] extern crate parsetest; -use parsetest::mylit; +use parsetest::lit; fn main() { // should pass - let _v = mylit!(99); + let _v = lit!(99); // should be rejected - let _v = mylit!(102); + let _v = lit!(102); // should be rejected - let _v = mylit!(9o9); + let _v = lit!(9o9); // should be rejected - let _v = mylit!((99)); + let _v = lit!((99)); } - ``` -The expected outcome for `mylit!` is: +The expected outcome for `lit!` is: ``` shell -> Executing task: cargo run --example ex1 < +> Executing task: cargo run --example ex_lit < - Compiling parsetest v0.1.0 (file:///home/pln/course/parsetest) + Compiling parsetest v0.1.0 (file:///home/pln/rtfm/parsetest) error: expected literal 10 <= x < 100, got 102 - --> examples/ex1.rs:11:21 + --> examples/ex_lit.rs:11:19 | -11 | let _v = mylit!(102); - | ^^^ +11 | let _v = lit!(102); + | ^^^ error: expected literal 10 <= x < 100, got 9 - --> examples/ex1.rs:14:21 + --> examples/ex_lit.rs:14:19 | -14 | let _v = mylit!(9o9); - | ^^^ +14 | let _v = lit!(9o9); + | ^^^ error: invalid suffix `o9` for numeric literal - --> examples/ex1.rs:14:21 + --> examples/ex_lit.rs:14:19 | -14 | let _v = mylit!(9o9); - | ^^^ +14 | let _v = lit!(9o9); + | ^^^ | = help: the suffix must be one of the integral types (`u32`, `isize`, etc) error: proc macro panicked - --> examples/ex1.rs:17:14 + --> examples/ex_lit.rs:17:14 | -17 | let _v = mylit!((99)); - | ^^^^^^^^^^^^ +17 | let _v = lit!((99)); + | ^^^^^^^^^^ | - = help: message: called `Result::unwrap()` on an `Err` value: ParseError(None) + = help: message: called `Result::unwrap()` on an `Err` value: ParseError(Some("failed to parse integer literal: failed to parse")) error: Could not compile `parsetest`. ``` @@ -87,6 +91,108 @@ use quote::ToTokens; use std::convert::From; +/// Procedural macro `lit` +/// Parsing a LinInt with compile time range checking +#[proc_macro] +pub fn lit(input: TokenStream) -> TokenStream { + let v: LitInt = syn::parse(input).unwrap(); + let value = v.value(); + if !(10 <= value && value < 100) { + v.span() + .unstable() + .error(format!("expected literal 10 <= x < 100, got {}", value,)) + .emit(); + } + From::from(v.into_tokens()) +} +``` + +The `syn::LitInt` type implement the `syn::synom::Synom` trait (required by the `syn::parse` function). The unwrapped result is of type `LitInt`, for which we can get the inner value representation (a `u64`). + +In this example we use that to (at compile time) ensure that the value is within a given range, (between 10 and 100), and reject faulty assignment with a sensible error message. + +The error message is presented as a `TokenStream` throught the `.emit()` function. This does NOT imply an immediate abort (unlike a `panic!`), thus rustc will continue compilation and catch all errors. + +For the first case (`lit!(99)`) the parsing succeeded and the returned token stream would amount to the literate integer 99. As our `v` is a `LitInt` we need to convert it into a `TokenStream`, a two stage process by first turning `v` into tokens, from wich we get a token stream. (You may comment out the failing parts and print the value of `_v` to convince yourself.) + +For the second and third cases, we see that appropriate errors and spans are reported by the built in parser. However the last case is somewhat unsatisfying, due to the `syn::parse` failing (unwrap on an `Err`). We can handle that as well as follows, for the example `ex_lite.rs`. + +``` rust +#![feature(proc_macro)] + +extern crate parsetest; +use parsetest::lite; + +fn main() { + // should be rejected + let _v = lite!((99)); +} +``` + +With the improved error reporting, we can get. + +``` shell +> Executing task: cargo run --example ex_lite < +Compiling parsetest v0.1.0 (file:///home/pln/rtfm/parsetest) +error: failed to parse integer literal: failed to parse + --> examples/ex_lite.rs:8:14 + | +8 | let _v = lite!((99)); + | ^^^^^^^^^^^ + +error: aborting due to previous error + +error: Could not compile `parsetest`. +``` + +The backing implementation now matches the parsed result and inserts a `compiler_error!`, instead of the previous unwrap. + +``` rust +#![feature(proc_macro)] + +extern crate proc_macro; +#[macro_use] +extern crate quote; +#[macro_use] +extern crate syn; + +use proc_macro::TokenStream; +use syn::spanned::Spanned; +use syn::synom::Synom; +use syn::LitInt; +use quote::ToTokens; + +use std::convert::From; +use std::error::Error; + +/// Procedural macro `lite` +/// Parsing a LinInt with compile time range checking +#[proc_macro] +pub fn lite(input: TokenStream) -> TokenStream { + match syn::parse::<LitInt>(input) { + Ok(v) => { + let value = v.value(); + if !(10 <= value && value < 100) { + v.span() + .unstable() + .error(format!("expected literal 10 <= x < 100, got {}", value,)) + .emit(); + } + From::from(v.into_tokens()) + } + Err(err) => { + let desc = err.description(); + let tokens = quote! { + compile_error!(#desc) + }; + return tokens.into(); + } +``` + +The description here is the default error, provided by the built in `LitInt` parser. + +### Direct approach + /// MyLit struct MyLit { val: LitInt, @@ -155,6 +261,3 @@ pub fn mylit(input: TokenStream) -> TokenStream { } ``` -Besides just parsing an `IntLit` (that is a `u64`), we want at compile time to ensure that it is in a given range, (between 10 and 100 in this example), and report a sensible error message for illegal usage. - -The error message is presented as a `TokenStream` throught the `.emit()` function. \ No newline at end of file diff --git a/examples/ex_lit.rs b/examples/ex_lit.rs new file mode 100644 index 0000000000000000000000000000000000000000..ce02ca9f3da0891673359131980daf22728b39bd --- /dev/null +++ b/examples/ex_lit.rs @@ -0,0 +1,18 @@ +#![feature(proc_macro)] + +extern crate parsetest; +use parsetest::lit; + +fn main() { + // should pass + let _v = lit!(99); + + // should be rejected + let _v = lit!(102); + + // should be rejected + let _v = lit!(9o9); + + // should be rejected + let _v = lit!((99)); +} diff --git a/examples/ex_lite.rs b/examples/ex_lite.rs new file mode 100644 index 0000000000000000000000000000000000000000..3718a49dcb81dedcd35690fe21d97f2f9b3b5b5b --- /dev/null +++ b/examples/ex_lite.rs @@ -0,0 +1,9 @@ +#![feature(proc_macro)] + +extern crate parsetest; +use parsetest::lite; + +fn main() { + // should be rejected + let _v = lite!((99)); +} diff --git a/src/lib.rs b/src/lib.rs index 4c6cf70e195108446fc3194765f50b2c202d67ee..c889c5530edbcb35299df83c7daff563510958eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![feature(proc_macro)] extern crate proc_macro; -// #[macro_use] +#[macro_use] extern crate quote; #[macro_use] extern crate syn; @@ -13,7 +13,47 @@ use syn::LitInt; use quote::ToTokens; use std::convert::From; -// use std::error::Error; +use std::error::Error; + +/// Procedural macro `lit` +/// Parsing a LinInt with compile time range checking +#[proc_macro] +pub fn lit(input: TokenStream) -> TokenStream { + let v: LitInt = syn::parse(input).unwrap(); + let value = v.value(); + if !(10 <= value && value < 100) { + v.span() + .unstable() + .error(format!("expected literal 10 <= x < 100, got {}", value,)) + .emit(); + } + From::from(v.into_tokens()) +} + +/// Procedural macro `lite` +/// Parsing a LinInt with compile time range checking +#[proc_macro] +pub fn lite(input: TokenStream) -> TokenStream { + match syn::parse::<LitInt>(input) { + Ok(v) => { + let value = v.value(); + if !(10 <= value && value < 100) { + v.span() + .unstable() + .error(format!("expected literal 10 <= x < 100, got {}", value,)) + .emit(); + } + From::from(v.into_tokens()) + } + Err(err) => { + let desc = err.description(); + let tokens = quote! { + compile_error!(#desc) + }; + return tokens.into(); + } + } +} /// MyLit struct MyLit {