From 1612ed351d991e7ce97028f36945539ce4aa19fd Mon Sep 17 00:00:00 2001 From: Joseph Eng <91924258+KangarooKoala@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:58:54 -0800 Subject: [PATCH 1/2] Improve requires clause parsing --- cxxheaderparser/parser.py | 69 ++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 8c6f65e..49be8e4 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -803,13 +803,6 @@ def _parse_concept( ), ) - # fmt: off - _expr_operators = { - "<", ">", "|", "%", "^", "!", "*", "-", "+", "&", "=", - "&&", "||", "<<" - } - # fmt: on - def _parse_requires( self, tok: LexToken, @@ -818,38 +811,48 @@ def _parse_requires( rawtoks: typing.List[LexToken] = [] - # The easier case -- requires requires - if tok.type == "requires": - rawtoks.append(tok) - for tt in ("(", "{"): - tok = self._next_token_must_be(tt) + # The expression in a requires clause must be one of the following: + # 1) A primary expression + # 2) A sequence of (1) joined with && + # 3) A sequence of (2) joined with || + # + # In terms of validity, this is equivalent to a sequence of primary expressions + # joined with && and/or ||. + # + # In general, a primary expression is one of the following: + # 1) this + # 2) a literal + # 3) an identifier expression + # 4) a lambda expression + # 5) a fold expression + # 6) a requires expression + # 7) any parenthesized expression + # + # For simplicity, we only consider the following primary expressions: + # 1) parenthesized expressions (which includes fold expressions) + # 2) requires expressions + # 3) identifer expressions (possibly qualified, possibly templated) + while True: + if tok.type == "(": rawtoks.extend(self._consume_balanced_tokens(tok)) - # .. and that's it? - - # this is either a parenthesized expression or a primary clause - elif tok.type == "(": - rawtoks.extend(self._consume_balanced_tokens(tok)) - else: - while True: - if tok.type == "(": + tok = self.lex.token() + elif tok.type == "requires": + rawtoks.append(tok) + for tt in ("(", "{"): + tok = self._next_token_must_be(tt) rawtoks.extend(self._consume_balanced_tokens(tok)) - else: - tok = self._parse_requires_segment(tok, rawtoks) + tok = self.lex.token() + else: + tok = self._parse_requires_segment(tok, rawtoks) - # If this is not an operator of some kind, we don't know how - # to proceed so let the next parser figure it out - if tok.value not in self._expr_operators: - break + if tok.value not in ("&&", "||"): + break - rawtoks.append(tok) + rawtoks.append(tok) - # check once more for compound operator? - tok = self.lex.token() - if tok.value in self._expr_operators: - rawtoks.append(tok) - tok = self.lex.token() + tok = self.lex.token() - self.lex.return_token(tok) + self.lex.return_token(tok) return self._create_value(rawtoks) From dc63715a03511e178a749f46f8a97125f561ae97 Mon Sep 17 00:00:00 2001 From: Joseph Eng <91924258+KangarooKoala@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:05:59 -0800 Subject: [PATCH 2/2] Add test --- tests/test_concepts.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_concepts.py b/tests/test_concepts.py index 85ceec8..2495f86 100644 --- a/tests/test_concepts.py +++ b/tests/test_concepts.py @@ -659,6 +659,59 @@ def test_requires_compound() -> None: ) +def test_requires_compound_parenthesized() -> None: + content = """ + template + requires (X == 0) || (X == 1) + int Fibonacci() { return X; } + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName(segments=[NameSpecifier(name="Fibonacci")]), + parameters=[], + has_body=True, + template=TemplateDecl( + params=[ + TemplateNonTypeParam( + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="X", + ) + ], + raw_requires_pre=Value( + tokens=[ + Token(value="("), + Token(value="X"), + Token(value="="), + Token(value="="), + Token(value="0"), + Token(value=")"), + Token(value="||"), + Token(value="("), + Token(value="X"), + Token(value="="), + Token(value="="), + Token(value="1"), + Token(value=")"), + ] + ), + ), + ) + ] + ) + ) + + def test_requires_ad_hoc() -> None: content = """ template