Skip to content

Commit

Permalink
Macros: Add a 'literal' fragment specifier
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
da-x committed May 13, 2018
1 parent 3e955a0 commit 37ed2ab
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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

------------------------
2 changes: 1 addition & 1 deletion src/librustc_passes/ast_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
2 changes: 1 addition & 1 deletion src/libsyntax/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 2 additions & 0 deletions src/libsyntax/ext/tt/macro_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
21 changes: 19 additions & 2 deletions src/libsyntax/ext/tt/macro_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -850,6 +851,10 @@ fn is_in_follow(tok: &quoted::TokenTree, frag: &str) -> Result<bool, (String, &'
// being a single token, idents and lifetimes are harmless
Ok(true)
},
"literal" => {
// 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
Expand All @@ -873,7 +878,7 @@ fn is_in_follow(tok: &quoted::TokenTree, frag: &str) -> Result<bool, (String, &'
_ => 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`"))
}
}
}
Expand Down Expand Up @@ -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") {
Expand Down
6 changes: 6 additions & 0 deletions src/libsyntax/feature_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! (
Expand Down Expand Up @@ -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";

Expand Down
1 change: 1 addition & 0 deletions src/libsyntax/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ pub fn noop_fold_interpolated<T: Folder>(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)),
Expand Down
18 changes: 9 additions & 9 deletions src/libsyntax/parse/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down Expand Up @@ -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); }
},
Expand Down Expand Up @@ -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<Expr>> {
pub fn parse_literal_maybe_minus(&mut self) -> PResult<'a, P<Expr>> {
maybe_whole_expr!(self);

let minus_lo = self.span;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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()?;
Expand Down
21 changes: 20 additions & 1 deletion src/libsyntax/parse/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -672,6 +689,7 @@ pub enum Nonterminal {
NtTy(P<ast::Ty>),
NtIdent(ast::Ident, /* is_raw */ bool),
NtLifetime(ast::Ident),
NtLiteral(P<ast::Expr>),
/// Stuff inside brackets for attributes
NtMeta(ast::MetaItem),
NtPath(ast::Path),
Expand Down Expand Up @@ -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(..)"),
Expand Down
1 change: 1 addition & 0 deletions src/libsyntax/print/pprust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
143 changes: 143 additions & 0 deletions src/test/run-pass/macro-literal.rs
Original file line number Diff line number Diff line change
@@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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");
}
Loading

0 comments on commit 37ed2ab

Please sign in to comment.