From e935959ad312abeac2683ff96a28738eaf8f1eb6 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 10 Oct 2023 01:51:53 -0400 Subject: [PATCH] Implement basic concept parsing - requires clauses are collected into a Value and not interpreted at this time --- cxxheaderparser/lexer.py | 2 + cxxheaderparser/parser.py | 31 ++- cxxheaderparser/simple.py | 7 + cxxheaderparser/types.py | 25 +++ cxxheaderparser/visitor.py | 12 ++ tests/test_concepts.py | 403 +++++++++++++++++++++++++++++++++++++ 6 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 tests/test_concepts.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index 341ef76..38c0b59 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -83,6 +83,7 @@ class PlyLexer: "char16_t", "char32_t", "class", + "concept", "const", "constexpr", "const_cast", @@ -121,6 +122,7 @@ class PlyLexer: "public", "register", "reinterpret_cast", + "requires", "return", "short", "signed", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 18614c5..52a035b 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -22,6 +22,7 @@ from .types import ( AutoSpecifier, BaseClass, ClassDecl, + Concept, DecltypeSpecifier, DecoratedType, EnumDecl, @@ -537,7 +538,7 @@ class CxxParser: self._finish_class_decl(old_state) # - # Template parsing + # Template and concept parsing # def _parse_template_type_parameter( @@ -640,6 +641,8 @@ class CxxParser: self._parse_using(tok, doxygen, template) elif tok.type == "friend": self._parse_friend_decl(tok, doxygen, template) + elif tok.type == "concept": + self._parse_concept(tok, doxygen, template) else: self._parse_declarations(tok, doxygen, template) @@ -750,6 +753,32 @@ class CxxParser: self.state, TemplateInst(typename, extern, doxygen) ) + def _parse_concept( + self, + tok: LexToken, + doxygen: typing.Optional[str], + template: TemplateDecl, + ) -> None: + name = self._next_token_must_be("NAME") + self._next_token_must_be("=") + + # not trying to understand this for now + raw_constraint = self._create_value(self._consume_value_until([], ",", ";")) + + state = self.state + if isinstance(state, ClassBlockState): + raise CxxParseError("concept cannot be defined in a class") + + self.visitor.on_concept( + state, + Concept( + template=template, + name=name.value, + raw_constraint=raw_constraint, + doxygen=doxygen, + ), + ) + # # Attributes # diff --git a/cxxheaderparser/simple.py b/cxxheaderparser/simple.py index 3efb048..2538683 100644 --- a/cxxheaderparser/simple.py +++ b/cxxheaderparser/simple.py @@ -34,6 +34,7 @@ from dataclasses import dataclass, field from .types import ( ClassDecl, + Concept, EnumDecl, Field, ForwardDecl, @@ -113,6 +114,9 @@ class NamespaceScope: using_alias: typing.List[UsingAlias] = field(default_factory=list) ns_alias: typing.List[NamespaceAlias] = field(default_factory=list) + #: Concepts + concepts: typing.List[Concept] = field(default_factory=list) + #: Explicit template instantiations template_insts: typing.List[TemplateInst] = field(default_factory=list) @@ -243,6 +247,9 @@ class SimpleCxxVisitor: def on_namespace_end(self, state: SNamespaceBlockState) -> None: pass + def on_concept(self, state: SNonClassBlockState, concept: Concept) -> None: + state.user_data.concepts.append(concept) + def on_namespace_alias( self, state: SNonClassBlockState, alias: NamespaceAlias ) -> None: diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py index b71ff6b..722960a 100644 --- a/cxxheaderparser/types.py +++ b/cxxheaderparser/types.py @@ -551,6 +551,31 @@ class TemplateInst: doxygen: typing.Optional[str] = None +@dataclass +class Concept: + """ + Preliminary support for consuming headers that contain concepts, but + not trying to actually make sense of them at this time. If this is + something you care about, pull requests are welcomed! + + .. code-block:: c++ + + template + concept Meowable = is_meowable; + + template + concept Addable = requires (T x) { x + x; }; + """ + + template: TemplateDecl + name: str + + #: In the future this will be removed if we fully parse the expression + raw_constraint: Value + + doxygen: typing.Optional[str] = None + + @dataclass class ForwardDecl: """ diff --git a/cxxheaderparser/visitor.py b/cxxheaderparser/visitor.py index aca2b2a..df0129d 100644 --- a/cxxheaderparser/visitor.py +++ b/cxxheaderparser/visitor.py @@ -8,6 +8,7 @@ else: from .types import ( + Concept, EnumDecl, Field, ForwardDecl, @@ -89,6 +90,14 @@ class CxxVisitor(Protocol): Called when a ``namespace`` alias is encountered """ + def on_concept(self, state: NonClassBlockState, concept: Concept) -> None: + """ + .. code-block:: c++ + + template + concept Meowable = is_meowable; + """ + def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: """ Called when a forward declaration is encountered @@ -254,6 +263,9 @@ class NullVisitor: def on_namespace_end(self, state: NamespaceBlockState) -> None: return None + def on_concept(self, state: NonClassBlockState, concept: Concept) -> None: + return None + def on_namespace_alias( self, state: NonClassBlockState, alias: NamespaceAlias ) -> None: diff --git a/tests/test_concepts.py b/tests/test_concepts.py new file mode 100644 index 0000000..ae29aec --- /dev/null +++ b/tests/test_concepts.py @@ -0,0 +1,403 @@ +from cxxheaderparser.simple import NamespaceScope, ParsedData, parse_string +from cxxheaderparser.tokfmt import Token +from cxxheaderparser.types import ( + Concept, + Function, + FundamentalSpecifier, + NameSpecifier, + PQName, + Parameter, + TemplateArgument, + TemplateDecl, + TemplateNonTypeParam, + TemplateSpecialization, + TemplateTypeParam, + Type, + Value, + Variable, +) + + +def test_concept_basic_constraint() -> None: + content = """ + template + concept Derived = std::is_base_of::value; + + template T> void f(T); // T is constrained by Derived + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[ + Parameter( + type=Type( + typename=PQName(segments=[NameSpecifier(name="T")]) + ) + ) + ], + template=TemplateDecl( + params=[ + TemplateNonTypeParam( + type=Type( + typename=PQName( + segments=[ + NameSpecifier( + name="Derived", + specialization=TemplateSpecialization( + args=[ + TemplateArgument( + arg=Type( + typename=PQName( + segments=[ + NameSpecifier( + name="Base" + ) + ] + ) + ) + ) + ] + ), + ) + ] + ) + ), + name="T", + ) + ] + ), + ) + ], + concepts=[ + Concept( + template=TemplateDecl( + params=[ + TemplateTypeParam(typekey="class", name="T"), + TemplateTypeParam(typekey="class", name="U"), + ] + ), + name="Derived", + raw_constraint=Value( + tokens=[ + Token(value="std"), + Token(value="::"), + Token(value="is_base_of"), + Token(value="<"), + Token(value="U"), + Token(value=","), + Token(value="T"), + Token(value=">"), + Token(value="::"), + Token(value="value"), + ] + ), + ) + ], + ) + ) + + +def test_concept_basic_constraint2() -> None: + content = """ + template constexpr bool is_meowable = true; + + template constexpr bool is_cat = true; + + template + concept Meowable = is_meowable; + + template + concept BadMeowableCat = is_meowable && is_cat; + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="is_meowable")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="bool")]) + ), + value=Value(tokens=[Token(value="true")]), + constexpr=True, + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="is_cat")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="bool")]) + ), + value=Value(tokens=[Token(value="true")]), + constexpr=True, + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + ), + ], + concepts=[ + Concept( + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + name="Meowable", + raw_constraint=Value( + tokens=[ + Token(value="is_meowable"), + Token(value="<"), + Token(value="T"), + Token(value=">"), + ] + ), + ), + Concept( + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + name="BadMeowableCat", + raw_constraint=Value( + tokens=[ + Token(value="is_meowable"), + Token(value="<"), + Token(value="T"), + Token(value=">"), + Token(value="&&"), + Token(value="is_cat"), + Token(value="<"), + Token(value="T"), + Token(value=">"), + ] + ), + ), + ], + ) + ) + + +def test_concept_basic_requires() -> None: + content = """ + template + concept Hashable = requires(T a) { + { std::hash{}(a) } -> std::convertible_to; + }; + + template void f(T) {} + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[ + Parameter( + type=Type( + typename=PQName(segments=[NameSpecifier(name="T")]) + ) + ) + ], + has_body=True, + template=TemplateDecl( + params=[ + TemplateNonTypeParam( + type=Type( + typename=PQName( + segments=[NameSpecifier(name="Hashable")] + ) + ), + name="T", + ) + ] + ), + ) + ], + concepts=[ + Concept( + template=TemplateDecl( + params=[TemplateTypeParam(typekey="typename", name="T")] + ), + name="Hashable", + raw_constraint=Value( + tokens=[ + Token(value="requires"), + Token(value="("), + Token(value="T"), + Token(value="a"), + Token(value=")"), + Token(value="{"), + Token(value="{"), + Token(value="std"), + Token(value="::"), + Token(value="hash"), + Token(value="<"), + Token(value="T"), + Token(value=">"), + Token(value="{"), + Token(value="}"), + Token(value="("), + Token(value="a"), + Token(value=")"), + Token(value="}"), + Token(value="->"), + Token(value="std"), + Token(value="::"), + Token(value="convertible_to"), + Token(value="<"), + Token(value="std"), + Token(value="::"), + Token(value="size_t"), + Token(value=">"), + Token(value=";"), + Token(value="}"), + ] + ), + ) + ], + ) + ) + + +def test_concept_nested_requirements() -> None: + content = """ + template + concept Semiregular = DefaultConstructible && + CopyConstructible && CopyAssignable && Destructible && + requires(T a, std::size_t n) + { + requires Same; // nested: "Same<...> evaluates to true" + { a.~T() } noexcept; // compound: "a.~T()" is a valid expression that doesn't throw + requires Same; // nested: "Same<...> evaluates to true" + requires Same; // nested + { delete new T }; // compound + { delete new T[n] }; // compound + }; + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + concepts=[ + Concept( + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + name="Semiregular", + raw_constraint=Value( + tokens=[ + Token(value="DefaultConstructible"), + Token(value="<"), + Token(value="T"), + Token(value=">"), + Token(value="&&"), + Token(value="CopyConstructible"), + Token(value="<"), + Token(value="T"), + Token(value=">"), + Token(value="&&"), + Token(value="CopyAssignable"), + Token(value="<"), + Token(value="T"), + Token(value=">"), + Token(value="&&"), + Token(value="Destructible"), + Token(value="<"), + Token(value="T"), + Token(value=">"), + Token(value="&&"), + Token(value="requires"), + Token(value="("), + Token(value="T"), + Token(value="a"), + Token(value=","), + Token(value="std"), + Token(value="::"), + Token(value="size_t"), + Token(value="n"), + Token(value=")"), + Token(value="{"), + Token(value="requires"), + Token(value="Same"), + Token(value="<"), + Token(value="T"), + Token(value="*"), + Token(value=","), + Token(value="decltype"), + Token(value="("), + Token(value="&"), + Token(value="a"), + Token(value=")"), + Token(value=">"), + Token(value=";"), + Token(value="{"), + Token(value="a"), + Token(value="."), + Token(value="~T"), + Token(value="("), + Token(value=")"), + Token(value="}"), + Token(value="noexcept"), + Token(value=";"), + Token(value="requires"), + Token(value="Same"), + Token(value="<"), + Token(value="T"), + Token(value="*"), + Token(value=","), + Token(value="decltype"), + Token(value="("), + Token(value="new"), + Token(value="T"), + Token(value=")"), + Token(value=">"), + Token(value=";"), + Token(value="requires"), + Token(value="Same"), + Token(value="<"), + Token(value="T"), + Token(value="*"), + Token(value=","), + Token(value="decltype"), + Token(value="("), + Token(value="new"), + Token(value="T"), + Token(value="["), + Token(value="n"), + Token(value="]"), + Token(value=")"), + Token(value=">"), + Token(value=";"), + Token(value="{"), + Token(value="delete"), + Token(value="new"), + Token(value="T"), + Token(value="}"), + Token(value=";"), + Token(value="{"), + Token(value="delete"), + Token(value="new"), + Token(value="T"), + Token(value="["), + Token(value="n"), + Token(value="]"), + Token(value="}"), + Token(value=";"), + Token(value="}"), + ] + ), + ) + ] + ) + )