From 37ed2ab91038567bafe3fd2e545c7d1631ff2ab0 Mon Sep 17 00:00:00 2001 From: Dan Aloni Date: Tue, 10 Apr 2018 02:08:47 +0300 Subject: [PATCH] Macros: Add a 'literal' fragment specifier Implements RFC 1576. See: https://github.com/rust-lang/rfcs/blob/master/text/1576-macros-literal-matcher.md Changes are mostly in libsyntax, docs, and tests. Feature gate is enabled for 1.27.0. Many thanks to Vadim Petrochenkov for following through code reviews and suggestions. Example: ````rust macro_rules! test_literal { ($l:literal) => { println!("literal: {}", $l); }; ($e:expr) => { println!("expr: {}", $e); }; } fn main() { let a = 1; test_literal!(a); test_literal!(2); test_literal!(-3); } ``` Output: ``` expr: 1 literal: 2 literal: -3 ``` --- .../macro-literal-matcher.md | 17 +++ src/librustc_passes/ast_validation.rs | 2 +- src/libsyntax/attr.rs | 2 +- src/libsyntax/ext/tt/macro_parser.rs | 2 + src/libsyntax/ext/tt/macro_rules.rs | 21 ++- src/libsyntax/feature_gate.rs | 6 + src/libsyntax/fold.rs | 1 + src/libsyntax/parse/parser.rs | 18 +-- src/libsyntax/parse/token.rs | 21 ++- src/libsyntax/print/pprust.rs | 1 + src/test/run-pass/macro-literal.rs | 143 ++++++++++++++++++ .../ui/feature-gate-macro-literal-matcher.rs | 19 +++ .../feature-gate-macro-literal-matcher.stderr | 11 ++ .../ui/macro-invalid-fragment-spec.stderr | 2 +- 14 files changed, 251 insertions(+), 15 deletions(-) create mode 100644 src/doc/unstable-book/src/language-features/macro-literal-matcher.md create mode 100644 src/test/run-pass/macro-literal.rs create mode 100644 src/test/ui/feature-gate-macro-literal-matcher.rs create mode 100644 src/test/ui/feature-gate-macro-literal-matcher.stderr diff --git a/src/doc/unstable-book/src/language-features/macro-literal-matcher.md b/src/doc/unstable-book/src/language-features/macro-literal-matcher.md new file mode 100644 index 0000000000000..7e3638fd1cf4c --- /dev/null +++ b/src/doc/unstable-book/src/language-features/macro-literal-matcher.md @@ -0,0 +1,17 @@ +# `macro_literal_matcher` + +The tracking issue for this feature is: [#35625] + +The RFC is: [rfc#1576]. + +With this feature gate enabled, the [list of fragment specifiers][frags] gains one more entry: + +* `literal`: a literal. Examples: 2, "string", 'c' + +A `literal` may be followed by anything, similarly to the `ident` specifier. + +[rfc#1576]: http://rust-lang.github.io/rfcs/1576-macros-literal-matcher.html +[#35625]: https://github.com/rust-lang/rust/issues/35625 +[frags]: ../book/first-edition/macros.html#syntactic-requirements + +------------------------ diff --git a/src/librustc_passes/ast_validation.rs b/src/librustc_passes/ast_validation.rs index 6708640379a54..4789e2e50ca54 100644 --- a/src/librustc_passes/ast_validation.rs +++ b/src/librustc_passes/ast_validation.rs @@ -114,7 +114,7 @@ impl<'a> AstValidator<'a> { } } - /// matches '-' lit | lit (cf. parser::Parser::parse_pat_literal_maybe_minus), + /// matches '-' lit | lit (cf. parser::Parser::parse_literal_maybe_minus), /// or path for ranges. /// /// FIXME: do we want to allow expr -> pattern conversion to create path expressions? diff --git a/src/libsyntax/attr.rs b/src/libsyntax/attr.rs index ace9904e0c021..fcda6ce9b164d 100644 --- a/src/libsyntax/attr.rs +++ b/src/libsyntax/attr.rs @@ -1348,7 +1348,7 @@ impl LitKind { Token::Ident(ident, false) if ident.name == "true" => Some(LitKind::Bool(true)), Token::Ident(ident, false) if ident.name == "false" => Some(LitKind::Bool(false)), Token::Interpolated(ref nt) => match nt.0 { - token::NtExpr(ref v) => match v.node { + token::NtExpr(ref v) | token::NtLiteral(ref v) => match v.node { ExprKind::Lit(ref lit) => Some(lit.node.clone()), _ => None, }, diff --git a/src/libsyntax/ext/tt/macro_parser.rs b/src/libsyntax/ext/tt/macro_parser.rs index 71634ada89458..f0339b89839c1 100644 --- a/src/libsyntax/ext/tt/macro_parser.rs +++ b/src/libsyntax/ext/tt/macro_parser.rs @@ -735,6 +735,7 @@ fn may_begin_with(name: &str, token: &Token) -> bool { "expr" => token.can_begin_expr(), "ty" => token.can_begin_type(), "ident" => get_macro_ident(token).is_some(), + "literal" => token.can_begin_literal_or_bool(), "vis" => match *token { // The follow-set of :vis + "priv" keyword + interpolated Token::Comma | Token::Ident(..) | Token::Interpolated(_) => true, @@ -821,6 +822,7 @@ fn parse_nt<'a>(p: &mut Parser<'a>, sp: Span, name: &str) -> Nonterminal { }, "pat" => token::NtPat(panictry!(p.parse_pat())), "expr" => token::NtExpr(panictry!(p.parse_expr())), + "literal" => token::NtLiteral(panictry!(p.parse_literal_maybe_minus())), "ty" => token::NtTy(panictry!(p.parse_ty())), // this could be handled like a token, since it is one "ident" => if let Some((ident, is_raw)) = get_macro_ident(&p.token) { diff --git a/src/libsyntax/ext/tt/macro_rules.rs b/src/libsyntax/ext/tt/macro_rules.rs index ffe68289d5224..1fc5aed7e7a23 100644 --- a/src/libsyntax/ext/tt/macro_rules.rs +++ b/src/libsyntax/ext/tt/macro_rules.rs @@ -647,7 +647,7 @@ fn check_matcher_core(sess: &ParseSess, let msg = format!("invalid fragment specifier `{}`", bad_frag); sess.span_diagnostic.struct_span_err(token.span(), &msg) .help("valid fragment specifiers are `ident`, `block`, `stmt`, `expr`, \ - `pat`, `ty`, `path`, `meta`, `tt`, `item` and `vis`") + `pat`, `ty`, `literal`, `path`, `meta`, `tt`, `item` and `vis`") .emit(); // (This eliminates false positives and duplicates // from error messages.) @@ -784,6 +784,7 @@ fn frag_can_be_followed_by_any(frag: &str) -> bool { "item" | // always terminated by `}` or `;` "block" | // exactly one token tree "ident" | // exactly one token tree + "literal" | // exactly one token tree "meta" | // exactly one token tree "lifetime" | // exactly one token tree "tt" => // exactly one token tree @@ -850,6 +851,10 @@ fn is_in_follow(tok: "ed::TokenTree, frag: &str) -> Result { + // literals may be of a single token, or two tokens (negative numbers) + Ok(true) + }, "meta" | "tt" => { // being either a single token or a delimited sequence, tt is // harmless @@ -873,7 +878,7 @@ fn is_in_follow(tok: "ed::TokenTree, frag: &str) -> Result Err((format!("invalid fragment specifier `{}`", frag), "valid fragment specifiers are `ident`, `block`, \ `stmt`, `expr`, `pat`, `ty`, `path`, `meta`, `tt`, \ - `item` and `vis`")) + `literal`, `item` and `vis`")) } } } @@ -913,6 +918,18 @@ fn is_legal_fragment_specifier(sess: &ParseSess, } true }, + "literal" => { + if !features.macro_literal_matcher && + !attr::contains_name(attrs, "allow_internal_unstable") { + let explain = feature_gate::EXPLAIN_LITERAL_MATCHER; + emit_feature_err(sess, + "macro_literal_matcher", + frag_span, + GateIssue::Language, + explain); + } + true + }, "vis" => { if !features.macro_vis_matcher && !attr::contains_name(attrs, "allow_internal_unstable") { diff --git a/src/libsyntax/feature_gate.rs b/src/libsyntax/feature_gate.rs index b27568a61f85c..562705462e2a4 100644 --- a/src/libsyntax/feature_gate.rs +++ b/src/libsyntax/feature_gate.rs @@ -463,6 +463,9 @@ declare_features! ( // Scoped attributes (active, tool_attributes, "1.25.0", Some(44690), None), + + // Allows use of the :literal macro fragment specifier (RFC 1576) + (active, macro_literal_matcher, "1.27.0", Some(35625), None), ); declare_features! ( @@ -1331,6 +1334,9 @@ pub const EXPLAIN_VIS_MATCHER: &'static str = pub const EXPLAIN_LIFETIME_MATCHER: &'static str = ":lifetime fragment specifier is experimental and subject to change"; +pub const EXPLAIN_LITERAL_MATCHER: &'static str = + ":literal fragment specifier is experimental and subject to change"; + pub const EXPLAIN_UNSIZED_TUPLE_COERCION: &'static str = "Unsized tuple coercion is not stable enough for use and is subject to change"; diff --git a/src/libsyntax/fold.rs b/src/libsyntax/fold.rs index a0cd831a9ba08..d67995761f627 100644 --- a/src/libsyntax/fold.rs +++ b/src/libsyntax/fold.rs @@ -635,6 +635,7 @@ pub fn noop_fold_interpolated(nt: token::Nonterminal, fld: &mut T) token::NtTy(ty) => token::NtTy(fld.fold_ty(ty)), token::NtIdent(ident, is_raw) => token::NtIdent(fld.fold_ident(ident), is_raw), token::NtLifetime(ident) => token::NtLifetime(fld.fold_ident(ident)), + token::NtLiteral(expr) => token::NtLiteral(fld.fold_expr(expr)), token::NtMeta(meta) => token::NtMeta(fld.fold_meta_item(meta)), token::NtPath(path) => token::NtPath(fld.fold_path(path)), token::NtTT(tt) => token::NtTT(fld.fold_tt(tt)), diff --git a/src/libsyntax/parse/parser.rs b/src/libsyntax/parse/parser.rs index 49b30c6f460fe..3f0df6d055b76 100644 --- a/src/libsyntax/parse/parser.rs +++ b/src/libsyntax/parse/parser.rs @@ -115,7 +115,7 @@ macro_rules! maybe_whole_expr { ($p:expr) => { if let token::Interpolated(nt) = $p.token.clone() { match nt.0 { - token::NtExpr(ref e) => { + token::NtExpr(ref e) | token::NtLiteral(ref e) => { $p.bump(); return Ok((*e).clone()); } @@ -1823,7 +1823,7 @@ impl<'a> Parser<'a> { pub fn parse_lit_token(&mut self) -> PResult<'a, LitKind> { let out = match self.token { token::Interpolated(ref nt) => match nt.0 { - token::NtExpr(ref v) => match v.node { + token::NtExpr(ref v) | token::NtLiteral(ref v) => match v.node { ExprKind::Lit(ref lit) => { lit.node.clone() } _ => { return self.unexpected_last(&self.token); } }, @@ -1862,7 +1862,7 @@ impl<'a> Parser<'a> { } /// matches '-' lit | lit (cf. ast_validation::AstValidator::check_expr_within_pat) - pub fn parse_pat_literal_maybe_minus(&mut self) -> PResult<'a, P> { + pub fn parse_literal_maybe_minus(&mut self) -> PResult<'a, P> { maybe_whole_expr!(self); let minus_lo = self.span; @@ -2407,10 +2407,10 @@ impl<'a> Parser<'a> { hi = pth.span; ex = ExprKind::Path(None, pth); } else { - match self.parse_lit() { - Ok(lit) => { - hi = lit.span; - ex = ExprKind::Lit(P(lit)); + match self.parse_literal_maybe_minus() { + Ok(expr) => { + hi = expr.span; + ex = expr.node.clone(); } Err(mut err) => { self.cancel(&mut err); @@ -3724,7 +3724,7 @@ impl<'a> Parser<'a> { let hi = self.prev_span; Ok(self.mk_expr(lo.to(hi), ExprKind::Path(qself, path), ThinVec::new())) } else { - self.parse_pat_literal_maybe_minus() + self.parse_literal_maybe_minus() } } @@ -3914,7 +3914,7 @@ impl<'a> Parser<'a> { } } else { // Try to parse everything else as literal with optional minus - match self.parse_pat_literal_maybe_minus() { + match self.parse_literal_maybe_minus() { Ok(begin) => { if self.eat(&token::DotDotDot) { let end = self.parse_pat_range_end()?; diff --git a/src/libsyntax/parse/token.rs b/src/libsyntax/parse/token.rs index 938711ca1d495..6bcc1b0f02691 100644 --- a/src/libsyntax/parse/token.rs +++ b/src/libsyntax/parse/token.rs @@ -280,7 +280,12 @@ impl Token { Lifetime(..) | // labeled loop Pound => true, // expression attributes Interpolated(ref nt) => match nt.0 { - NtIdent(..) | NtExpr(..) | NtBlock(..) | NtPath(..) | NtLifetime(..) => true, + NtLiteral(..) | + NtIdent(..) | + NtExpr(..) | + NtBlock(..) | + NtPath(..) | + NtLifetime(..) => true, _ => false, }, _ => false, @@ -324,6 +329,18 @@ impl Token { } } + /// Returns `true` if the token is any literal, a minus (which can follow a literal, + /// for example a '-42', or one of the boolean idents). + pub fn can_begin_literal_or_bool(&self) -> bool { + match *self { + Literal(..) => true, + BinOp(Minus) => true, + Ident(ident, false) if ident.name == keywords::True.name() => true, + Ident(ident, false) if ident.name == keywords::False.name() => true, + _ => false, + } + } + /// Returns an identifier if this token is an identifier. pub fn ident(&self) -> Option<(ast::Ident, /* is_raw */ bool)> { match *self { @@ -672,6 +689,7 @@ pub enum Nonterminal { NtTy(P), NtIdent(ast::Ident, /* is_raw */ bool), NtLifetime(ast::Ident), + NtLiteral(P), /// Stuff inside brackets for attributes NtMeta(ast::MetaItem), NtPath(ast::Path), @@ -713,6 +731,7 @@ impl fmt::Debug for Nonterminal { NtExpr(..) => f.pad("NtExpr(..)"), NtTy(..) => f.pad("NtTy(..)"), NtIdent(..) => f.pad("NtIdent(..)"), + NtLiteral(..) => f.pad("NtLiteral(..)"), NtMeta(..) => f.pad("NtMeta(..)"), NtPath(..) => f.pad("NtPath(..)"), NtTT(..) => f.pad("NtTT(..)"), diff --git a/src/libsyntax/print/pprust.rs b/src/libsyntax/print/pprust.rs index b8ddb063d9875..99a6fcf170dcb 100644 --- a/src/libsyntax/print/pprust.rs +++ b/src/libsyntax/print/pprust.rs @@ -273,6 +273,7 @@ pub fn token_to_string(tok: &Token) -> String { token::NtIdent(e, false) => ident_to_string(e), token::NtIdent(e, true) => format!("r#{}", ident_to_string(e)), token::NtLifetime(e) => ident_to_string(e), + token::NtLiteral(ref e) => expr_to_string(e), token::NtTT(ref tree) => tt_to_string(tree.clone()), token::NtArm(ref e) => arm_to_string(e), token::NtImplItem(ref e) => impl_item_to_string(e), diff --git a/src/test/run-pass/macro-literal.rs b/src/test/run-pass/macro-literal.rs new file mode 100644 index 0000000000000..0bcda7bc1447a --- /dev/null +++ b/src/test/run-pass/macro-literal.rs @@ -0,0 +1,143 @@ +// Copyright 2018 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![feature(macro_literal_matcher)] + +macro_rules! mtester { + ($l:literal) => { + &format!("macro caught literal: {}", $l) + }; + ($e:expr) => { + &format!("macro caught expr: {}", $e) + }; +} + +macro_rules! two_negative_literals { + ($l1:literal $l2:literal) => { + &format!("macro caught literals: {}, {}", $l1, $l2) + }; +} + +macro_rules! only_expr { + ($e:expr) => { + &format!("macro caught expr: {}", $e) + }; +} + +macro_rules! mtester_dbg { + ($l:literal) => { + &format!("macro caught literal: {:?}", $l) + }; + ($e:expr) => { + &format!("macro caught expr: {:?}", $e) + }; +} + +macro_rules! catch_range { + ($s:literal ... $e:literal) => { + &format!("macro caught literal: {} ... {}", $s, $e) + }; + (($s:expr) ... ($e:expr)) => { // Must use ')' before '...' + &format!("macro caught expr: {} ... {}", $s, $e) + }; +} + +macro_rules! pat_match { + ($s:literal ... $e:literal) => { + match 3 { + $s ... $e => "literal, in range", + _ => "literal, other", + } + }; + ($s:pat) => { + match 3 { + $s => "pat, single", + _ => "pat, other", + } + }; +} + +macro_rules! match_attr { + (#[$attr:meta] $e:literal) => { + "attr matched literal" + }; + (#[$attr:meta] $e:expr) => { + "attr matched expr" + }; +} + +macro_rules! match_produced_attr { + ($lit: literal) => { + // Struct with doc comment passed via $literal + #[doc = $lit] + struct LiteralProduced; + }; + ($expr: expr) => { + struct ExprProduced; + }; +} + +macro_rules! test_user { + ($s:literal, $e:literal) => { + { + let mut v = Vec::new(); + for i in $s .. $e { + v.push(i); + } + "literal" + } + }; + ($s:expr, $e: expr) => { + { + let mut v = Vec::new(); + for i in $s .. $e { + v.push(i); + } + "expr" + } + }; +} + +pub fn main() { + // Cases where 'literal' catches + assert_eq!(mtester!("str"), "macro caught literal: str"); + assert_eq!(mtester!(2), "macro caught literal: 2"); + assert_eq!(mtester!(2.2), "macro caught literal: 2.2"); + assert_eq!(mtester!(1u32), "macro caught literal: 1"); + assert_eq!(mtester!(0x32), "macro caught literal: 50"); + assert_eq!(mtester!('c'), "macro caught literal: c"); + assert_eq!(mtester!(-1.2), "macro caught literal: -1.2"); + assert_eq!(two_negative_literals!(-2 -3), "macro caught literals: -2, -3"); + assert_eq!(catch_range!(2 ... 3), "macro caught literal: 2 ... 3"); + assert_eq!(match_attr!(#[attr] 1), "attr matched literal"); + assert_eq!(test_user!(10, 20), "literal"); + assert_eq!(mtester!(false), "macro caught literal: false"); + assert_eq!(mtester!(true), "macro caught literal: true"); + match_produced_attr!("a"); + let _a = LiteralProduced; + assert_eq!(pat_match!(1 ... 3), "literal, in range"); + assert_eq!(pat_match!(4 ... 6), "literal, other"); + + // Cases where 'expr' catches + assert_eq!(mtester!((-1.2)), "macro caught expr: -1.2"); + assert_eq!(only_expr!(-1.2), "macro caught expr: -1.2"); + assert_eq!(mtester!((1 + 3)), "macro caught expr: 4"); + assert_eq!(mtester_dbg!(()), "macro caught expr: ()"); + assert_eq!(catch_range!((1 + 1) ... (2 + 2)), "macro caught expr: 2 ... 4"); + assert_eq!(match_attr!(#[attr] (1 + 2)), "attr matched expr"); + assert_eq!(test_user!(10, (20 + 2)), "expr"); + + match_produced_attr!((3 + 2)); + let _b = ExprProduced; + + // Cases where 'pat' matched + assert_eq!(pat_match!(3), "pat, single"); + assert_eq!(pat_match!(6), "pat, other"); +} diff --git a/src/test/ui/feature-gate-macro-literal-matcher.rs b/src/test/ui/feature-gate-macro-literal-matcher.rs new file mode 100644 index 0000000000000..db5cca193ab4e --- /dev/null +++ b/src/test/ui/feature-gate-macro-literal-matcher.rs @@ -0,0 +1,19 @@ +// Copyright 2018 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// Test that the :lifetime macro fragment cannot be used when macro_lifetime_matcher +// feature gate is not used. + +macro_rules! m { ($lt:literal) => {} } +//~^ ERROR :literal fragment specifier is experimental and subject to change + +fn main() { + m!("some string literal"); +} diff --git a/src/test/ui/feature-gate-macro-literal-matcher.stderr b/src/test/ui/feature-gate-macro-literal-matcher.stderr new file mode 100644 index 0000000000000..f714b916966a1 --- /dev/null +++ b/src/test/ui/feature-gate-macro-literal-matcher.stderr @@ -0,0 +1,11 @@ +error[E0658]: :literal fragment specifier is experimental and subject to change (see issue #35625) + --> $DIR/feature-gate-macro-literal-matcher.rs:14:19 + | +LL | macro_rules! m { ($lt:literal) => {} } + | ^^^^^^^^^^^ + | + = help: add #![feature(macro_literal_matcher)] to the crate attributes to enable + +error: aborting due to previous error + +For more information about this error, try `rustc --explain E0658`. diff --git a/src/test/ui/macro-invalid-fragment-spec.stderr b/src/test/ui/macro-invalid-fragment-spec.stderr index bdb0e4a5c4044..765621f51d4fd 100644 --- a/src/test/ui/macro-invalid-fragment-spec.stderr +++ b/src/test/ui/macro-invalid-fragment-spec.stderr @@ -4,7 +4,7 @@ error: invalid fragment specifier `foo` LL | ($x:foo) => () | ^^^^^^ | - = help: valid fragment specifiers are `ident`, `block`, `stmt`, `expr`, `pat`, `ty`, `path`, `meta`, `tt`, `item` and `vis` + = help: valid fragment specifiers are `ident`, `block`, `stmt`, `expr`, `pat`, `ty`, `literal`, `path`, `meta`, `tt`, `item` and `vis` error: aborting due to previous error