Merge pull request #7380 from jakobandersen/cpp_comma_operator

C++, comma operator, pack expansion, error messages
This commit is contained in:
Jakob Lykke Andersen 2020-03-25 15:14:11 +01:00 committed by GitHub
commit 6be31aa05c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 177 additions and 94 deletions

View File

@ -20,6 +20,8 @@ Bugs fixed
* #7370: autosummary: raises UnboundLocalError when unknown module given
* #7367: C++, alternate operator spellings are now supported.
* C, alternate operator spellings are now supported.
* #7368: C++, comma operator in expressions, pack expansion in template
argument lists, and more comprehensive error messages in some cases.
Testing
--------

View File

@ -1505,8 +1505,38 @@ class ASTBinOpExpr(ASTExpression):
self.exprs[i].describe_signature(signode, mode, env, symbol)
class ASTBracedInitList(ASTBase):
def __init__(self, exprs: List[Union[ASTExpression, "ASTBracedInitList"]],
trailingComma: bool) -> None:
self.exprs = exprs
self.trailingComma = trailingComma
def get_id(self, version: int) -> str:
return "il%sE" % ''.join(e.get_id(version) for e in self.exprs)
def _stringify(self, transform: StringifyTransform) -> str:
exprs = [transform(e) for e in self.exprs]
trailingComma = ',' if self.trailingComma else ''
return '{%s%s}' % (', '.join(exprs), trailingComma)
def describe_signature(self, signode: TextElement, mode: str,
env: "BuildEnvironment", symbol: "Symbol") -> None:
verify_description_mode(mode)
signode.append(nodes.Text('{'))
first = True
for e in self.exprs:
if not first:
signode.append(nodes.Text(', '))
else:
first = False
e.describe_signature(signode, mode, env, symbol)
if self.trailingComma:
signode.append(nodes.Text(','))
signode.append(nodes.Text('}'))
class ASTAssignmentExpr(ASTExpression):
def __init__(self, exprs: List[ASTExpression], ops: List[str]):
def __init__(self, exprs: List[Union[ASTExpression, ASTBracedInitList]], ops: List[str]):
assert len(exprs) > 0
assert len(exprs) == len(ops) + 1
self.exprs = exprs
@ -1540,6 +1570,31 @@ class ASTAssignmentExpr(ASTExpression):
self.exprs[i].describe_signature(signode, mode, env, symbol)
class ASTCommaExpr(ASTExpression):
def __init__(self, exprs: List[ASTExpression]):
assert len(exprs) > 0
self.exprs = exprs
def _stringify(self, transform: StringifyTransform) -> str:
return ', '.join(transform(e) for e in self.exprs)
def get_id(self, version: int) -> str:
id_ = _id_operator_v2[',']
res = []
for i in range(len(self.exprs) - 1):
res.append(id_)
res.append(self.exprs[i].get_id(version))
res.append(self.exprs[-1].get_id(version))
return ''.join(res)
def describe_signature(self, signode: TextElement, mode: str,
env: "BuildEnvironment", symbol: "Symbol") -> None:
self.exprs[0].describe_signature(signode, mode, env, symbol)
for i in range(1, len(self.exprs)):
signode.append(nodes.Text(', '))
self.exprs[i].describe_signature(signode, mode, env, symbol)
class ASTFallbackExpr(ASTExpression):
def __init__(self, expr: str):
self.expr = expr
@ -1658,9 +1713,11 @@ class ASTTemplateArgConstant(ASTBase):
class ASTTemplateArgs(ASTBase):
def __init__(self, args: List[Union["ASTType", ASTTemplateArgConstant]]) -> None:
def __init__(self, args: List[Union["ASTType", ASTTemplateArgConstant]],
packExpansion: bool) -> None:
assert args is not None
self.args = args
self.packExpansion = packExpansion
def get_id(self, version: int) -> str:
if version == 1:
@ -1672,13 +1729,21 @@ class ASTTemplateArgs(ASTBase):
res = []
res.append('I')
for a in self.args:
res.append(a.get_id(version))
if len(self.args) > 0:
for a in self.args[:-1]:
res.append(a.get_id(version))
if self.packExpansion:
res.append('J')
res.append(self.args[-1].get_id(version))
if self.packExpansion:
res.append('E')
res.append('E')
return ''.join(res)
def _stringify(self, transform: StringifyTransform) -> str:
res = ', '.join(transform(a) for a in self.args)
if self.packExpansion:
res += '...'
return '<' + res + '>'
def describe_signature(self, signode: TextElement, mode: str,
@ -1691,6 +1756,8 @@ class ASTTemplateArgs(ASTBase):
signode += nodes.Text(', ')
first = False
a.describe_signature(signode, 'markType', env, symbol=symbol)
if self.packExpansion:
signode += nodes.Text('...')
signode += nodes.Text('>')
@ -2641,7 +2708,7 @@ class ASTDeclaratorParen(ASTDeclarator):
##############################################################################################
class ASTPackExpansionExpr(ASTExpression):
def __init__(self, expr: ASTExpression):
def __init__(self, expr: Union[ASTExpression, ASTBracedInitList]):
self.expr = expr
def _stringify(self, transform: StringifyTransform) -> str:
@ -2658,7 +2725,7 @@ class ASTPackExpansionExpr(ASTExpression):
class ASTParenExprList(ASTBase):
def __init__(self, exprs: List[ASTExpression]) -> None:
def __init__(self, exprs: List[Union[ASTExpression, ASTBracedInitList]]) -> None:
self.exprs = exprs
def get_id(self, version: int) -> str:
@ -2682,35 +2749,6 @@ class ASTParenExprList(ASTBase):
signode.append(nodes.Text(')'))
class ASTBracedInitList(ASTBase):
def __init__(self, exprs: List[ASTExpression], trailingComma: bool) -> None:
self.exprs = exprs
self.trailingComma = trailingComma
def get_id(self, version: int) -> str:
return "il%sE" % ''.join(e.get_id(version) for e in self.exprs)
def _stringify(self, transform: StringifyTransform) -> str:
exprs = [transform(e) for e in self.exprs]
trailingComma = ',' if self.trailingComma else ''
return '{%s%s}' % (', '.join(exprs), trailingComma)
def describe_signature(self, signode: TextElement, mode: str,
env: "BuildEnvironment", symbol: "Symbol") -> None:
verify_description_mode(mode)
signode.append(nodes.Text('{'))
first = True
for e in self.exprs:
if not first:
signode.append(nodes.Text(', '))
else:
first = False
e.describe_signature(signode, mode, env, symbol)
if self.trailingComma:
signode.append(nodes.Text(','))
signode.append(nodes.Text('}'))
class ASTInitializer(ASTBase):
def __init__(self, value: Union[ASTExpression, ASTBracedInitList],
hasAssign: bool = True) -> None:
@ -4755,7 +4793,7 @@ class DefinitionParser(BaseParser):
self.pos = pos
# fall back to a paren expression
try:
res = self._parse_expression(inTemplate=False)
res = self._parse_expression()
self.skip_ws()
if not self.skip_string(')'):
self.fail("Expected ')' in end of parenthesized expression.")
@ -4803,7 +4841,9 @@ class DefinitionParser(BaseParser):
return None
def _parse_initializer_list(self, name: str, open: str, close: str
) -> Tuple[List[ASTExpression], bool]:
) -> Tuple[List[Union[ASTExpression,
ASTBracedInitList]],
bool]:
# Parse open and close with the actual initializer-list inbetween
# -> initializer-clause '...'[opt]
# | initializer-list ',' initializer-clause '...'[opt]
@ -4813,11 +4853,11 @@ class DefinitionParser(BaseParser):
if self.skip_string(close):
return [], False
exprs = [] # type: List[ASTExpression]
exprs = [] # type: List[Union[ASTExpression, ASTBracedInitList]]
trailingComma = False
while True:
self.skip_ws()
expr = self._parse_expression(inTemplate=False)
expr = self._parse_initializer_clause()
self.skip_ws()
if self.skip_string('...'):
exprs.append(ASTPackExpansionExpr(expr))
@ -4847,6 +4887,12 @@ class DefinitionParser(BaseParser):
return None
return ASTParenExprList(exprs)
def _parse_initializer_clause(self) -> Union[ASTExpression, ASTBracedInitList]:
bracedInitList = self._parse_braced_init_list()
if bracedInitList is not None:
return bracedInitList
return self._parse_assignment_expression(inTemplate=False)
def _parse_braced_init_list(self) -> ASTBracedInitList:
# -> '{' initializer-list ','[opt] '}'
# | '{' '}'
@ -4906,7 +4952,7 @@ class DefinitionParser(BaseParser):
self.fail("Expected '(' in '%s'." % cast)
def parser():
return self._parse_expression(inTemplate=False)
return self._parse_expression()
expr = self._parse_expression_fallback([')'], parser)
self.skip_ws()
if not self.skip_string(")"):
@ -4927,7 +4973,7 @@ class DefinitionParser(BaseParser):
try:
def parser():
return self._parse_expression(inTemplate=False)
return self._parse_expression()
expr = self._parse_expression_fallback([')'], parser)
prefix = ASTTypeId(expr, isType=False)
if not self.skip_string(')'):
@ -4974,7 +5020,7 @@ class DefinitionParser(BaseParser):
self.skip_ws()
if prefixType in ['expr', 'cast', 'typeid']:
if self.skip_string_and_ws('['):
expr = self._parse_expression(inTemplate=False)
expr = self._parse_expression()
self.skip_ws()
if not self.skip_string(']'):
self.fail("Expected ']' in end of postfix expression.")
@ -5065,7 +5111,7 @@ class DefinitionParser(BaseParser):
if self.skip_word_and_ws('noexcept'):
if not self.skip_string_and_ws('('):
self.fail("Expecting '(' after 'noexcept'.")
expr = self._parse_expression(inTemplate=False)
expr = self._parse_expression()
self.skip_ws()
if not self.skip_string(')'):
self.fail("Expecting ')' to end 'noexcept'.")
@ -5198,7 +5244,7 @@ class DefinitionParser(BaseParser):
# logical-or-expression
# | logical-or-expression "?" expression ":" assignment-expression
# | logical-or-expression assignment-operator initializer-clause
exprs = []
exprs = [] # type: List[Union[ASTExpression, ASTBracedInitList]]
ops = []
orExpr = self._parse_logical_or_expression(inTemplate=inTemplate)
exprs.append(orExpr)
@ -5213,7 +5259,7 @@ class DefinitionParser(BaseParser):
else:
if not self.skip_string(op):
continue
expr = self._parse_logical_or_expression(False)
expr = self._parse_initializer_clause()
exprs.append(expr)
ops.append(op)
oneMore = True
@ -5230,11 +5276,19 @@ class DefinitionParser(BaseParser):
# TODO: use _parse_conditional_expression_tail
return orExpr
def _parse_expression(self, inTemplate: bool) -> ASTExpression:
def _parse_expression(self) -> ASTExpression:
# -> assignment-expression
# | expression "," assignment-expresion
# TODO: actually parse the second production
return self._parse_assignment_expression(inTemplate=inTemplate)
exprs = [self._parse_assignment_expression(inTemplate=False)]
while True:
self.skip_ws()
if not self.skip_string(','):
break
exprs.append(self._parse_assignment_expression(inTemplate=False))
if len(exprs) == 1:
return exprs[0]
else:
return ASTCommaExpr(exprs)
def _parse_expression_fallback(self, end: List[str],
parser: Callable[[], ASTExpression],
@ -5313,13 +5367,21 @@ class DefinitionParser(BaseParser):
return ASTOperatorType(type)
def _parse_template_argument_list(self) -> ASTTemplateArgs:
# template-argument-list: (but we include the < and > here
# template-argument ...[opt]
# template-argument-list, template-argument ...[opt]
# template-argument:
# constant-expression
# type-id
# id-expression
self.skip_ws()
if not self.skip_string_and_ws('<'):
return None
if self.skip_string('>'):
return ASTTemplateArgs([])
return ASTTemplateArgs([], False)
prevErrors = []
templateArgs = [] # type: List[Union[ASTType, ASTTemplateArgConstant]]
packExpansion = False
while 1:
pos = self.pos
parsedComma = False
@ -5327,31 +5389,35 @@ class DefinitionParser(BaseParser):
try:
type = self._parse_type(named=False)
self.skip_ws()
if self.skip_string('>'):
if self.skip_string_and_ws('...'):
packExpansion = True
parsedEnd = True
if not self.skip_string('>'):
self.fail('Expected ">" after "..." in template argument list.')
elif self.skip_string('>'):
parsedEnd = True
elif self.skip_string(','):
parsedComma = True
else:
self.fail('Expected ">" or "," in template argument list.')
self.fail('Expected "...>", ">" or "," in template argument list.')
templateArgs.append(type)
except DefinitionError as e:
prevErrors.append((e, "If type argument"))
self.pos = pos
try:
# actually here we shouldn't use the fallback parser (hence allow=False),
# because if actually took the < in an expression, then we _will_ fail,
# which is handled elsewhere. E.g., :cpp:expr:`A <= 0`.
def parser():
return self._parse_constant_expression(inTemplate=True)
value = self._parse_expression_fallback(
[',', '>'], parser, allow=False)
value = self._parse_constant_expression(inTemplate=True)
self.skip_ws()
if self.skip_string('>'):
if self.skip_string_and_ws('...'):
packExpansion = True
parsedEnd = True
if not self.skip_string('>'):
self.fail('Expected ">" after "..." in template argument list.')
elif self.skip_string('>'):
parsedEnd = True
elif self.skip_string(','):
parsedComma = True
else:
self.fail('Expected ">" or "," in template argument list.')
self.fail('Expected "...>", ">" or "," in template argument list.')
templateArgs.append(ASTTemplateArgConstant(value))
except DefinitionError as e:
self.pos = pos
@ -5361,7 +5427,9 @@ class DefinitionParser(BaseParser):
if parsedEnd:
assert not parsedComma
break
return ASTTemplateArgs(templateArgs)
else:
assert not packExpansion
return ASTTemplateArgs(templateArgs, packExpansion)
def _parse_nested_name(self, memberPointer: bool = False) -> ASTNestedName:
names = [] # type: List[ASTNestedNameElement]
@ -5451,7 +5519,7 @@ class DefinitionParser(BaseParser):
if not self.skip_string(')'):
self.fail("Expected ')' after 'decltype(auto'.")
return ASTTrailingTypeSpecDecltypeAuto()
expr = self._parse_expression(inTemplate=False)
expr = self._parse_expression()
self.skip_ws()
if not self.skip_string(')'):
self.fail("Expected ')' after 'decltype(<expr>'.")
@ -5464,7 +5532,6 @@ class DefinitionParser(BaseParser):
if self.skip_word_and_ws(k):
prefix = k
break
nestedName = self._parse_nested_name()
return ASTTrailingTypeSpecName(prefix, nestedName)
@ -5696,7 +5763,7 @@ class DefinitionParser(BaseParser):
continue
def parser():
return self._parse_expression(inTemplate=False)
return self._parse_expression()
value = self._parse_expression_fallback([']'], parser)
if not self.skip_string(']'):
self.fail("Expected ']' in end of array operator.")
@ -5758,32 +5825,6 @@ class DefinitionParser(BaseParser):
if typed and self.skip_string("..."):
next = self._parse_declarator(named, paramMode, False)
return ASTDeclaratorParamPack(next=next)
if typed: # pointer to member
pos = self.pos
try:
name = self._parse_nested_name(memberPointer=True)
self.skip_ws()
if not self.skip_string('*'):
self.fail("Expected '*' in pointer to member declarator.")
self.skip_ws()
except DefinitionError as e:
self.pos = pos
prevErrors.append((e, "If pointer to member declarator"))
else:
volatile = False
const = False
while 1:
if not volatile:
volatile = self.skip_word_and_ws('volatile')
if volatile:
continue
if not const:
const = self.skip_word_and_ws('const')
if const:
continue
break
next = self._parse_declarator(named, paramMode, typed)
return ASTDeclaratorMemPtr(name, const, volatile, next=next)
if typed and self.current_char == '(': # note: peeking, not skipping
if paramMode == "operatorCast":
# TODO: we should be able to parse cast operators which return
@ -5820,9 +5861,40 @@ class DefinitionParser(BaseParser):
prevErrors.append((exNoPtrParen, "If parenthesis in noptr-declarator"))
header = "Error in declarator"
raise self._make_multi_error(prevErrors, header)
if typed: # pointer to member
pos = self.pos
try:
name = self._parse_nested_name(memberPointer=True)
self.skip_ws()
if not self.skip_string('*'):
self.fail("Expected '*' in pointer to member declarator.")
self.skip_ws()
except DefinitionError as e:
self.pos = pos
prevErrors.append((e, "If pointer to member declarator"))
else:
volatile = False
const = False
while 1:
if not volatile:
volatile = self.skip_word_and_ws('volatile')
if volatile:
continue
if not const:
const = self.skip_word_and_ws('const')
if const:
continue
break
next = self._parse_declarator(named, paramMode, typed)
return ASTDeclaratorMemPtr(name, const, volatile, next=next)
pos = self.pos
try:
return self._parse_declarator_name_suffix(named, paramMode, typed)
res = self._parse_declarator_name_suffix(named, paramMode, typed)
# this is a heuristic for error messages, for when there is a < after a
# nested name, but it was not a successful template argument list
if self.current_char == '<':
self.otherErrors.append(self._make_multi_error(prevErrors, ""))
return res
except DefinitionError as e:
self.pos = pos
prevErrors.append((e, "If declarator-id"))
@ -5891,7 +5963,6 @@ class DefinitionParser(BaseParser):
raise Exception('Internal error, unknown outer "%s".' % outer)
if outer != 'operatorCast':
assert named
if outer in ('type', 'function'):
# We allow type objects to just be a name.
# Some functions don't have normal return types: constructors,
@ -6085,8 +6156,8 @@ class DefinitionParser(BaseParser):
self.skip_ws()
if not self.skip_string("<"):
self.fail("Expected '<' after 'template'")
prevErrors = []
while 1:
prevErrors = []
self.skip_ws()
if self.skip_word('template'):
# declare a tenplate template parameter
@ -6139,6 +6210,7 @@ class DefinitionParser(BaseParser):
if self.skip_string('>'):
return ASTTemplateParams(templateParams)
elif self.skip_string(','):
prevErrors = []
continue
else:
header = "Error in template parameter list."
@ -6358,7 +6430,7 @@ class DefinitionParser(BaseParser):
def parse_expression(self) -> Union[ASTExpression, ASTType]:
pos = self.pos
try:
expr = self._parse_expression(False)
expr = self._parse_expression()
self.skip_ws()
self.assert_end()
return expr

View File

@ -274,6 +274,9 @@ def test_expressions():
exprCheck('a xor_eq 5', 'eO1aL5E')
exprCheck('a |= 5', 'oR1aL5E')
exprCheck('a or_eq 5', 'oR1aL5E')
exprCheck('a = {1, 2, 3}', 'aS1ailL1EL2EL3EE')
# comma operator
exprCheck('a, 5', 'cm1aL5E')
# Additional tests
# a < expression that starts with something that could be a template
@ -626,6 +629,12 @@ def test_class_definitions():
{2: 'I0E7has_varI1TNSt6void_tIDTadN1T3varEEEEE'})
check('class', 'template<typename ...Ts> T<int (*)(Ts)...>',
{2: 'IDpE1TIJPFi2TsEEE'})
check('class', 'template<int... Is> T<(Is)...>',
{2: 'I_DpiE1TIJX(Is)EEE', 3: 'I_DpiE1TIJX2IsEEE'})
def test_union_definitions():
check('union', 'A', {2: "1A"})