about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2018-02-11 18:11:01 +0000
committerbors <bors@rust-lang.org>2018-02-11 18:11:01 +0000
commitb8398d947d160ad4f26cc22da66e5fbc7030817b (patch)
tree915e92eb58e30190ca3fa21aa4c8dc72e3e99438
parent0196b20f6943963fd7598c0486ce3f58189c04d1 (diff)
parentb92e542ddd41affaf6fb5d1267b8e8dfc03089a5 (diff)
downloadrust-b8398d947d160ad4f26cc22da66e5fbc7030817b.tar.gz
rust-b8398d947d160ad4f26cc22da66e5fbc7030817b.zip
Auto merge of #47752 - mark-i-m:at-most-once-rep, r=nikomatsakis
Implement `?` macro repetition

See rust-lang/rfcs#2298 (with disposition merge)
-rw-r--r--src/doc/unstable-book/src/language-features/macro-at-most-once-rep.md17
-rw-r--r--src/libsyntax/ext/tt/macro_parser.rs15
-rw-r--r--src/libsyntax/ext/tt/macro_rules.rs6
-rw-r--r--src/libsyntax/ext/tt/quoted.rs203
-rw-r--r--src/libsyntax/feature_gate.rs6
-rw-r--r--src/test/compile-fail/issue-39388.rs2
-rw-r--r--src/test/compile-fail/macro-at-most-once-rep-ambig.rs53
-rw-r--r--src/test/parse-fail/issue-33569.rs2
-rw-r--r--src/test/run-pass-fulldeps/auxiliary/procedural_mbe_matching.rs9
-rw-r--r--src/test/run-pass/macro-at-most-once-rep.rs88
-rw-r--r--src/test/ui/feature-gate-macro_at_most_once_rep.rs19
-rw-r--r--src/test/ui/feature-gate-macro_at_most_once_rep.stderr10
12 files changed, 374 insertions, 56 deletions
diff --git a/src/doc/unstable-book/src/language-features/macro-at-most-once-rep.md b/src/doc/unstable-book/src/language-features/macro-at-most-once-rep.md
new file mode 100644
index 00000000000..dbaf91b6e78
--- /dev/null
+++ b/src/doc/unstable-book/src/language-features/macro-at-most-once-rep.md
@@ -0,0 +1,17 @@
+# `macro_at_most_once_rep`
+
+The tracking issue for this feature is: TODO(mark-i-m)
+
+With this feature gate enabled, one can use `?` as a Kleene operator meaning "0
+or 1 repetitions" in a macro definition. Previously only `+` and `*` were allowed.
+
+For example:
+```rust
+macro_rules! foo {
+    (something $(,)?) // `?` indicates `,` is "optional"...
+        => {}
+}
+```
+
+------------------------
+
diff --git a/src/libsyntax/ext/tt/macro_parser.rs b/src/libsyntax/ext/tt/macro_parser.rs
index 1a9849ca530..0621f728e2a 100644
--- a/src/libsyntax/ext/tt/macro_parser.rs
+++ b/src/libsyntax/ext/tt/macro_parser.rs
@@ -181,6 +181,8 @@ struct MatcherPos {
     match_hi: usize,
 
     // Specifically used if we are matching a repetition. If we aren't both should be `None`.
+    /// The KleeneOp of this sequence if we are in a repetition.
+    seq_op: Option<quoted::KleeneOp>,
     /// The separator if we are in a repetition
     sep: Option<Token>,
     /// The "parent" matcher position if we are in a repetition. That is, the matcher position just
@@ -263,6 +265,7 @@ fn initial_matcher_pos(ms: Vec<TokenTree>, lo: BytePos) -> Box<MatcherPos> {
         stack: vec![],
 
         // Haven't descended into any sequences, so both of these are `None`.
+        seq_op: None,
         sep: None,
         up: None,
     })
@@ -466,8 +469,8 @@ fn inner_parse_loop(
                     }
                 }
                 // We don't need a separator. Move the "dot" back to the beginning of the matcher
-                // and try to match again.
-                else {
+                // and try to match again UNLESS we are only allowed to have _one_ repetition.
+                else if item.seq_op != Some(quoted::KleeneOp::ZeroOrOne) {
                     item.match_cur = item.match_lo;
                     item.idx = 0;
                     cur_items.push(item);
@@ -486,8 +489,10 @@ fn inner_parse_loop(
             match item.top_elts.get_tt(idx) {
                 // Need to descend into a sequence
                 TokenTree::Sequence(sp, seq) => {
-                    if seq.op == quoted::KleeneOp::ZeroOrMore {
-                        // Examine the case where there are 0 matches of this sequence
+                    // Examine the case where there are 0 matches of this sequence
+                    if seq.op == quoted::KleeneOp::ZeroOrMore
+                        || seq.op == quoted::KleeneOp::ZeroOrOne
+                    {
                         let mut new_item = item.clone();
                         new_item.match_cur += seq.num_captures;
                         new_item.idx += 1;
@@ -497,11 +502,11 @@ fn inner_parse_loop(
                         cur_items.push(new_item);
                     }
 
-                    // Examine the case where there is at least one match of this sequence
                     let matches = create_matches(item.matches.len());
                     cur_items.push(Box::new(MatcherPos {
                         stack: vec![],
                         sep: seq.separator.clone(),
+                        seq_op: Some(seq.op),
                         idx: 0,
                         matches,
                         match_lo: item.match_cur,
diff --git a/src/libsyntax/ext/tt/macro_rules.rs b/src/libsyntax/ext/tt/macro_rules.rs
index 9efb4faa635..5254c751e6b 100644
--- a/src/libsyntax/ext/tt/macro_rules.rs
+++ b/src/libsyntax/ext/tt/macro_rules.rs
@@ -237,7 +237,8 @@ pub fn compile(sess: &ParseSess, features: &RefCell<Features>, def: &ast::Item)
             s.iter().map(|m| {
                 if let MatchedNonterminal(ref nt) = *m {
                     if let NtTT(ref tt) = **nt {
-                        let tt = quoted::parse(tt.clone().into(), true, sess).pop().unwrap();
+                        let tt = quoted::parse(tt.clone().into(), true, sess, features, &def.attrs)
+                            .pop().unwrap();
                         valid &= check_lhs_nt_follows(sess, features, &def.attrs, &tt);
                         return tt;
                     }
@@ -253,7 +254,8 @@ pub fn compile(sess: &ParseSess, features: &RefCell<Features>, def: &ast::Item)
             s.iter().map(|m| {
                 if let MatchedNonterminal(ref nt) = *m {
                     if let NtTT(ref tt) = **nt {
-                        return quoted::parse(tt.clone().into(), false, sess).pop().unwrap();
+                        return quoted::parse(tt.clone().into(), false, sess, features, &def.attrs)
+                            .pop().unwrap();
                     }
                 }
                 sess.span_diagnostic.span_bug(def.span, "wrong-structured lhs")
diff --git a/src/libsyntax/ext/tt/quoted.rs b/src/libsyntax/ext/tt/quoted.rs
index c55dfaba8f6..982b60b81e4 100644
--- a/src/libsyntax/ext/tt/quoted.rs
+++ b/src/libsyntax/ext/tt/quoted.rs
@@ -8,14 +8,17 @@
 // option. This file may not be copied, modified, or distributed
 // except according to those terms.
 
-use ast;
+use {ast, attr};
 use ext::tt::macro_parser;
+use feature_gate::{self, emit_feature_err, Features, GateIssue};
 use parse::{token, ParseSess};
 use print::pprust;
 use symbol::keywords;
 use syntax_pos::{BytePos, Span, DUMMY_SP};
 use tokenstream;
 
+use std::cell::RefCell;
+use std::iter::Peekable;
 use std::rc::Rc;
 
 /// Contains the sub-token-trees of a "delimited" token tree, such as the contents of `(`. Note
@@ -78,6 +81,7 @@ pub enum KleeneOp {
     ZeroOrMore,
     /// Kleene plus (`+`) for one or more repetitions
     OneOrMore,
+    ZeroOrOne,
 }
 
 /// Similar to `tokenstream::TokenTree`, except that `$i`, `$i:ident`, and `$(...)`
@@ -169,6 +173,8 @@ impl TokenTree {
 ///   `ident` are "matchers". They are not present in the body of a macro rule -- just in the
 ///   pattern, so we pass a parameter to indicate whether to expect them or not.
 /// - `sess`: the parsing session. Any errors will be emitted to this session.
+/// - `features`, `attrs`: language feature flags and attributes so that we know whether to use
+///   unstable features or not.
 ///
 /// # Returns
 ///
@@ -177,18 +183,19 @@ pub fn parse(
     input: tokenstream::TokenStream,
     expect_matchers: bool,
     sess: &ParseSess,
+    features: &RefCell<Features>,
+    attrs: &[ast::Attribute],
 ) -> Vec<TokenTree> {
     // Will contain the final collection of `self::TokenTree`
     let mut result = Vec::new();
 
     // For each token tree in `input`, parse the token into a `self::TokenTree`, consuming
     // additional trees if need be.
-    let mut trees = input.trees();
+    let mut trees = input.trees().peekable();
     while let Some(tree) = trees.next() {
-        let tree = parse_tree(tree, &mut trees, expect_matchers, sess);
-
         // Given the parsed tree, if there is a metavar and we are expecting matchers, actually
         // parse out the matcher (i.e. in `$id:ident` this would parse the `:` and `ident`).
+        let tree = parse_tree(tree, &mut trees, expect_matchers, sess, features, attrs);
         match tree {
             TokenTree::MetaVar(start_sp, ident) if expect_matchers => {
                 let span = match trees.next() {
@@ -237,11 +244,15 @@ pub fn parse(
 ///   converting `tree`
 /// - `expect_matchers`: same as for `parse` (see above).
 /// - `sess`: the parsing session. Any errors will be emitted to this session.
+/// - `features`, `attrs`: language feature flags and attributes so that we know whether to use
+///   unstable features or not.
 fn parse_tree<I>(
     tree: tokenstream::TokenTree,
-    trees: &mut I,
+    trees: &mut Peekable<I>,
     expect_matchers: bool,
     sess: &ParseSess,
+    features: &RefCell<Features>,
+    attrs: &[ast::Attribute],
 ) -> TokenTree
 where
     I: Iterator<Item = tokenstream::TokenTree>,
@@ -260,9 +271,9 @@ where
                     sess.span_diagnostic.span_err(span, &msg);
                 }
                 // Parse the contents of the sequence itself
-                let sequence = parse(delimited.tts.into(), expect_matchers, sess);
+                let sequence = parse(delimited.tts.into(), expect_matchers, sess, features, attrs);
                 // Get the Kleene operator and optional separator
-                let (separator, op) = parse_sep_and_kleene_op(trees, span, sess);
+                let (separator, op) = parse_sep_and_kleene_op(trees, span, sess, features, attrs);
                 // Count the number of captured "names" (i.e. named metavars)
                 let name_captures = macro_parser::count_names(&sequence);
                 TokenTree::Sequence(
@@ -315,12 +326,46 @@ where
             span,
             Rc::new(Delimited {
                 delim: delimited.delim,
-                tts: parse(delimited.tts.into(), expect_matchers, sess),
+                tts: parse(delimited.tts.into(), expect_matchers, sess, features, attrs),
             }),
         ),
     }
 }
 
+/// Takes a token and returns `Some(KleeneOp)` if the token is `+` `*` or `?`. Otherwise, return
+/// `None`.
+fn kleene_op(token: &token::Token) -> Option<KleeneOp> {
+    match *token {
+        token::BinOp(token::Star) => Some(KleeneOp::ZeroOrMore),
+        token::BinOp(token::Plus) => Some(KleeneOp::OneOrMore),
+        token::Question => Some(KleeneOp::ZeroOrOne),
+        _ => None,
+    }
+}
+
+/// Parse the next token tree of the input looking for a KleeneOp. Returns
+///
+/// - Ok(Ok(op)) if the next token tree is a KleeneOp
+/// - Ok(Err(tok, span)) if the next token tree is a token but not a KleeneOp
+/// - Err(span) if the next token tree is not a token
+fn parse_kleene_op<I>(
+    input: &mut I,
+    span: Span,
+) -> Result<Result<KleeneOp, (token::Token, Span)>, Span>
+where
+    I: Iterator<Item = tokenstream::TokenTree>,
+{
+    match input.next() {
+        Some(tokenstream::TokenTree::Token(span, tok)) => match kleene_op(&tok) {
+            Some(op) => Ok(Ok(op)),
+            None => Ok(Err((tok, span))),
+        },
+        tree => Err(tree.as_ref()
+            .map(tokenstream::TokenTree::span)
+            .unwrap_or(span)),
+    }
+}
+
 /// Attempt to parse a single Kleene star, possibly with a separator.
 ///
 /// For example, in a pattern such as `$(a),*`, `a` is the pattern to be repeated, `,` is the
@@ -334,55 +379,121 @@ where
 /// operator and separator, then a tuple with `(separator, KleeneOp)` is returned. Otherwise, an
 /// error with the appropriate span is emitted to `sess` and a dummy value is returned.
 fn parse_sep_and_kleene_op<I>(
-    input: &mut I,
+    input: &mut Peekable<I>,
     span: Span,
     sess: &ParseSess,
+    features: &RefCell<Features>,
+    attrs: &[ast::Attribute],
 ) -> (Option<token::Token>, KleeneOp)
 where
     I: Iterator<Item = tokenstream::TokenTree>,
 {
-    fn kleene_op(token: &token::Token) -> Option<KleeneOp> {
-        match *token {
-            token::BinOp(token::Star) => Some(KleeneOp::ZeroOrMore),
-            token::BinOp(token::Plus) => Some(KleeneOp::OneOrMore),
-            _ => None,
+    // We basically look at two token trees here, denoted as #1 and #2 below
+    let span = match parse_kleene_op(input, span) {
+        // #1 is a `+` or `*` KleeneOp
+        //
+        // `?` is ambiguous: it could be a separator or a Kleene::ZeroOrOne, so we need to look
+        // ahead one more token to be sure.
+        Ok(Ok(op)) if op != KleeneOp::ZeroOrOne => return (None, op),
+
+        // #1 is `?` token, but it could be a Kleene::ZeroOrOne without a separator or it could
+        // be a `?` separator followed by any Kleene operator. We need to look ahead 1 token to
+        // find out which.
+        Ok(Ok(op)) => {
+            assert_eq!(op, KleeneOp::ZeroOrOne);
+
+            // Lookahead at #2. If it is a KleenOp, then #1 is a separator.
+            let is_1_sep = if let Some(&tokenstream::TokenTree::Token(_, ref tok2)) = input.peek() {
+                kleene_op(tok2).is_some()
+            } else {
+                false
+            };
+
+            if is_1_sep {
+                // #1 is a separator and #2 should be a KleepeOp::*
+                // (N.B. We need to advance the input iterator.)
+                match parse_kleene_op(input, span) {
+                    // #2 is a KleeneOp (this is the only valid option) :)
+                    Ok(Ok(op)) if op == KleeneOp::ZeroOrOne => {
+                        if !features.borrow().macro_at_most_once_rep
+                            && !attr::contains_name(attrs, "allow_internal_unstable")
+                        {
+                            let explain = feature_gate::EXPLAIN_MACRO_AT_MOST_ONCE_REP;
+                            emit_feature_err(
+                                sess,
+                                "macro_at_most_once_rep",
+                                span,
+                                GateIssue::Language,
+                                explain,
+                            );
+                        }
+                        return (Some(token::Question), op);
+                    }
+                    Ok(Ok(op)) => return (Some(token::Question), op),
+
+                    // #2 is a random token (this is an error) :(
+                    Ok(Err((_, span))) => span,
+
+                    // #2 is not even a token at all :(
+                    Err(span) => span,
+                }
+            } else {
+                if !features.borrow().macro_at_most_once_rep
+                    && !attr::contains_name(attrs, "allow_internal_unstable")
+                {
+                    let explain = feature_gate::EXPLAIN_MACRO_AT_MOST_ONCE_REP;
+                    emit_feature_err(
+                        sess,
+                        "macro_at_most_once_rep",
+                        span,
+                        GateIssue::Language,
+                        explain,
+                    );
+                }
+
+                // #2 is a random tree and #1 is KleeneOp::ZeroOrOne
+                return (None, op);
+            }
         }
-    }
 
-    // We attempt to look at the next two token trees in `input`. I will call the first #1 and the
-    // second #2. If #1 and #2 don't match a valid KleeneOp with/without separator, that is an
-    // error, and we should emit an error on the most specific span possible.
-    let span = match input.next() {
-        // #1 is a token
-        Some(tokenstream::TokenTree::Token(span, tok)) => match kleene_op(&tok) {
-            // #1 is a KleeneOp with no separator
-            Some(op) => return (None, op),
-
-            // #1 is not a KleeneOp, but may be a separator... need to look at #2
-            None => match input.next() {
-                // #2 is a token
-                Some(tokenstream::TokenTree::Token(span, tok2)) => match kleene_op(&tok2) {
-                    // #2 is a KleeneOp, so #1 must be a separator
-                    Some(op) => return (Some(tok), op),
-
-                    // #2 is not a KleeneOp... error
-                    None => span,
-                },
-
-                // #2 is not a token at all... error
-                tree => tree.as_ref()
-                    .map(tokenstream::TokenTree::span)
-                    .unwrap_or(span),
-            },
+        // #1 is a separator followed by #2, a KleeneOp
+        Ok(Err((tok, span))) => match parse_kleene_op(input, span) {
+            // #2 is a KleeneOp :D
+            Ok(Ok(op)) if op == KleeneOp::ZeroOrOne => {
+                if !features.borrow().macro_at_most_once_rep
+                    && !attr::contains_name(attrs, "allow_internal_unstable")
+                {
+                    let explain = feature_gate::EXPLAIN_MACRO_AT_MOST_ONCE_REP;
+                    emit_feature_err(
+                        sess,
+                        "macro_at_most_once_rep",
+                        span,
+                        GateIssue::Language,
+                        explain,
+                    );
+                }
+                return (Some(tok), op);
+            }
+            Ok(Ok(op)) => return (Some(tok), op),
+
+            // #2 is a random token :(
+            Ok(Err((_, span))) => span,
+
+            // #2 is not a token at all :(
+            Err(span) => span,
         },
 
-        // #1 is not a token at all... error
-        tree => tree.as_ref()
-            .map(tokenstream::TokenTree::span)
-            .unwrap_or(span),
+        // #1 is not a token
+        Err(span) => span,
     };
 
-    // Error...
-    sess.span_diagnostic.span_err(span, "expected `*` or `+`");
+    if !features.borrow().macro_at_most_once_rep
+        && !attr::contains_name(attrs, "allow_internal_unstable")
+    {
+        sess.span_diagnostic
+            .span_err(span, "expected one of: `*`, `+`, or `?`");
+    } else {
+        sess.span_diagnostic.span_err(span, "expected `*` or `+`");
+    }
     (None, KleeneOp::ZeroOrMore)
 }
diff --git a/src/libsyntax/feature_gate.rs b/src/libsyntax/feature_gate.rs
index ae0556320b0..ea916d5168c 100644
--- a/src/libsyntax/feature_gate.rs
+++ b/src/libsyntax/feature_gate.rs
@@ -446,6 +446,9 @@ declare_features! (
 
     // Allows `#[repr(transparent)]` attribute on newtype structs
     (active, repr_transparent, "1.25.0", Some(43036)),
+
+    // Use `?` as the Kleene "at most one" operator
+    (active, macro_at_most_once_rep, "1.25.0", Some(48075)),
 );
 
 declare_features! (
@@ -1258,6 +1261,9 @@ pub const EXPLAIN_PLACEMENT_IN: &'static str =
 pub const EXPLAIN_UNSIZED_TUPLE_COERCION: &'static str =
     "Unsized tuple coercion is not stable enough for use and is subject to change";
 
+pub const EXPLAIN_MACRO_AT_MOST_ONCE_REP: &'static str =
+    "Using the `?` macro Kleene operator for \"at most one\" repetition is unstable";
+
 struct PostExpansionVisitor<'a> {
     context: &'a Context<'a>,
 }
diff --git a/src/test/compile-fail/issue-39388.rs b/src/test/compile-fail/issue-39388.rs
index 15eef429eab..6da04937408 100644
--- a/src/test/compile-fail/issue-39388.rs
+++ b/src/test/compile-fail/issue-39388.rs
@@ -11,7 +11,7 @@
 #![allow(unused_macros)]
 
 macro_rules! assign {
-    (($($a:tt)*) = ($($b:tt))*) => { //~ ERROR expected `*` or `+`
+    (($($a:tt)*) = ($($b:tt))*) => { //~ ERROR expected one of: `*`, `+`, or `?`
         $($a)* = $($b)*
     }
 }
diff --git a/src/test/compile-fail/macro-at-most-once-rep-ambig.rs b/src/test/compile-fail/macro-at-most-once-rep-ambig.rs
new file mode 100644
index 00000000000..a5660f8b41f
--- /dev/null
+++ b/src/test/compile-fail/macro-at-most-once-rep-ambig.rs
@@ -0,0 +1,53 @@
+// Copyright 2012 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.
+
+// The logic for parsing Kleene operators in macros has a special case to disambiguate `?`.
+// Specifically, `$(pat)?` is the ZeroOrOne operator whereas `$(pat)?+` or `$(pat)?*` are the
+// ZeroOrMore and OneOrMore operators using `?` as a separator. These tests are intended to
+// exercise that logic in the macro parser.
+//
+// Moreover, we also throw in some tests for using a separator with `?`, which is meaningless but
+// included for consistency with `+` and `*`.
+//
+// This test focuses on error cases.
+
+#![feature(macro_at_most_once_rep)]
+
+macro_rules! foo {
+    ($(a)?) => {}
+}
+
+macro_rules! baz {
+    ($(a),?) => {} // comma separator is meaningless for `?`
+}
+
+macro_rules! barplus {
+    ($(a)?+) => {}
+}
+
+macro_rules! barstar {
+    ($(a)?*) => {}
+}
+
+pub fn main() {
+    foo!(a?a?a); //~ ERROR no rules expected the token `?`
+    foo!(a?a); //~ ERROR no rules expected the token `?`
+    foo!(a?); //~ ERROR no rules expected the token `?`
+    baz!(a?a?a); //~ ERROR no rules expected the token `?`
+    baz!(a?a); //~ ERROR no rules expected the token `?`
+    baz!(a?); //~ ERROR no rules expected the token `?`
+    baz!(a,); //~ ERROR unexpected end of macro invocation
+    baz!(a?a?a,); //~ ERROR no rules expected the token `?`
+    baz!(a?a,); //~ ERROR no rules expected the token `?`
+    baz!(a?,); //~ ERROR no rules expected the token `?`
+    barplus!(); //~ ERROR unexpected end of macro invocation
+    barplus!(a?); //~ ERROR unexpected end of macro invocation
+    barstar!(a?); //~ ERROR unexpected end of macro invocation
+}
diff --git a/src/test/parse-fail/issue-33569.rs b/src/test/parse-fail/issue-33569.rs
index 15d491719a6..af90d0a83c9 100644
--- a/src/test/parse-fail/issue-33569.rs
+++ b/src/test/parse-fail/issue-33569.rs
@@ -13,7 +13,7 @@
 macro_rules! foo {
     { $+ } => { //~ ERROR expected identifier, found `+`
                 //~^ ERROR missing fragment specifier
-        $(x)(y) //~ ERROR expected `*` or `+`
+        $(x)(y) //~ ERROR expected one of: `*`, `+`, or `?`
     }
 }
 
diff --git a/src/test/run-pass-fulldeps/auxiliary/procedural_mbe_matching.rs b/src/test/run-pass-fulldeps/auxiliary/procedural_mbe_matching.rs
index b5d6ff595af..9ebc438ad5a 100644
--- a/src/test/run-pass-fulldeps/auxiliary/procedural_mbe_matching.rs
+++ b/src/test/run-pass-fulldeps/auxiliary/procedural_mbe_matching.rs
@@ -18,6 +18,7 @@ extern crate syntax_pos;
 extern crate rustc;
 extern crate rustc_plugin;
 
+use syntax::feature_gate::Features;
 use syntax::parse::token::{NtExpr, NtPat};
 use syntax::ast::{Ident, Pat};
 use syntax::tokenstream::{TokenTree};
@@ -31,11 +32,17 @@ use syntax::ptr::P;
 use syntax_pos::Span;
 use rustc_plugin::Registry;
 
+use std::cell::RefCell;
+
 fn expand_mbe_matches(cx: &mut ExtCtxt, _: Span, args: &[TokenTree])
         -> Box<MacResult + 'static> {
 
     let mbe_matcher = quote_tokens!(cx, $$matched:expr, $$($$pat:pat)|+);
-    let mbe_matcher = quoted::parse(mbe_matcher.into_iter().collect(), true, cx.parse_sess);
+    let mbe_matcher = quoted::parse(mbe_matcher.into_iter().collect(),
+                                    true,
+                                    cx.parse_sess,
+                                    &RefCell::new(Features::new()),
+                                    &[]);
     let map = match TokenTree::parse(cx, &mbe_matcher, args.iter().cloned().collect()) {
         Success(map) => map,
         Failure(_, tok) => {
diff --git a/src/test/run-pass/macro-at-most-once-rep.rs b/src/test/run-pass/macro-at-most-once-rep.rs
new file mode 100644
index 00000000000..b7e942f9383
--- /dev/null
+++ b/src/test/run-pass/macro-at-most-once-rep.rs
@@ -0,0 +1,88 @@
+// Copyright 2012 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.
+
+// The logic for parsing Kleene operators in macros has a special case to disambiguate `?`.
+// Specifically, `$(pat)?` is the ZeroOrOne operator whereas `$(pat)?+` or `$(pat)?*` are the
+// ZeroOrMore and OneOrMore operators using `?` as a separator. These tests are intended to
+// exercise that logic in the macro parser.
+//
+// Moreover, we also throw in some tests for using a separator with `?`, which is meaningless but
+// included for consistency with `+` and `*`.
+//
+// This test focuses on non-error cases and making sure the correct number of repetitions happen.
+
+#![feature(macro_at_most_once_rep)]
+
+macro_rules! foo {
+    ($($a:ident)? ; $num:expr) => { {
+        let mut x = 0;
+
+        $(
+            x += $a;
+         )?
+
+        assert_eq!(x, $num);
+    } }
+}
+
+macro_rules! baz {
+    ($($a:ident),? ; $num:expr) => { { // comma separator is meaningless for `?`
+        let mut x = 0;
+
+        $(
+            x += $a;
+         )?
+
+        assert_eq!(x, $num);
+    } }
+}
+
+macro_rules! barplus {
+    ($($a:ident)?+ ; $num:expr) => { {
+        let mut x = 0;
+
+        $(
+            x += $a;
+         )+
+
+        assert_eq!(x, $num);
+    } }
+}
+
+macro_rules! barstar {
+    ($($a:ident)?* ; $num:expr) => { {
+        let mut x = 0;
+
+        $(
+            x += $a;
+         )*
+
+        assert_eq!(x, $num);
+    } }
+}
+
+pub fn main() {
+    let a = 1;
+
+    // accept 0 or 1 repetitions
+    foo!( ; 0);
+    foo!(a ; 1);
+    baz!( ; 0);
+    baz!(a ; 1);
+
+    // Make sure using ? as a separator works as before
+    barplus!(a ; 1);
+    barplus!(a?a ; 2);
+    barplus!(a?a?a ; 3);
+    barstar!( ; 0);
+    barstar!(a ; 1);
+    barstar!(a?a ; 2);
+    barstar!(a?a?a ; 3);
+}
diff --git a/src/test/ui/feature-gate-macro_at_most_once_rep.rs b/src/test/ui/feature-gate-macro_at_most_once_rep.rs
new file mode 100644
index 00000000000..19f5aca5730
--- /dev/null
+++ b/src/test/ui/feature-gate-macro_at_most_once_rep.rs
@@ -0,0 +1,19 @@
+// Copyright 2017 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.
+
+// Test that `?` macro Kleene operator can not be used when the `macro_at_most_once_rep` feature
+// gate is not used.
+
+macro_rules! m { ($(a)?) => {} }
+//~^ ERROR Using the `?` macro Kleene operator for "at most one" repetition is unstable
+
+fn main() {
+    m!();
+}
diff --git a/src/test/ui/feature-gate-macro_at_most_once_rep.stderr b/src/test/ui/feature-gate-macro_at_most_once_rep.stderr
new file mode 100644
index 00000000000..02dbab07bde
--- /dev/null
+++ b/src/test/ui/feature-gate-macro_at_most_once_rep.stderr
@@ -0,0 +1,10 @@
+error[E0658]: Using the `?` macro Kleene operator for "at most one" repetition is unstable (see issue #48075)
+  --> $DIR/feature-gate-macro_at_most_once_rep.rs:14:20
+   |
+14 | macro_rules! m { ($(a)?) => {} }
+   |                    ^^^
+   |
+   = help: add #![feature(macro_at_most_once_rep)] to the crate attributes to enable
+
+error: aborting due to previous error
+