Implement basic concept parsing

- requires clauses are collected into a Value and not interpreted at
  this time
This commit is contained in:
Dustin Spicuzza 2023-10-10 01:51:53 -04:00
parent e23c4db96d
commit e935959ad3
6 changed files with 479 additions and 1 deletions

View File

@ -83,6 +83,7 @@ class PlyLexer:
"char16_t", "char16_t",
"char32_t", "char32_t",
"class", "class",
"concept",
"const", "const",
"constexpr", "constexpr",
"const_cast", "const_cast",
@ -121,6 +122,7 @@ class PlyLexer:
"public", "public",
"register", "register",
"reinterpret_cast", "reinterpret_cast",
"requires",
"return", "return",
"short", "short",
"signed", "signed",

View File

@ -22,6 +22,7 @@ from .types import (
AutoSpecifier, AutoSpecifier,
BaseClass, BaseClass,
ClassDecl, ClassDecl,
Concept,
DecltypeSpecifier, DecltypeSpecifier,
DecoratedType, DecoratedType,
EnumDecl, EnumDecl,
@ -537,7 +538,7 @@ class CxxParser:
self._finish_class_decl(old_state) self._finish_class_decl(old_state)
# #
# Template parsing # Template and concept parsing
# #
def _parse_template_type_parameter( def _parse_template_type_parameter(
@ -640,6 +641,8 @@ class CxxParser:
self._parse_using(tok, doxygen, template) self._parse_using(tok, doxygen, template)
elif tok.type == "friend": elif tok.type == "friend":
self._parse_friend_decl(tok, doxygen, template) self._parse_friend_decl(tok, doxygen, template)
elif tok.type == "concept":
self._parse_concept(tok, doxygen, template)
else: else:
self._parse_declarations(tok, doxygen, template) self._parse_declarations(tok, doxygen, template)
@ -750,6 +753,32 @@ class CxxParser:
self.state, TemplateInst(typename, extern, doxygen) 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 # Attributes
# #

View File

@ -34,6 +34,7 @@ from dataclasses import dataclass, field
from .types import ( from .types import (
ClassDecl, ClassDecl,
Concept,
EnumDecl, EnumDecl,
Field, Field,
ForwardDecl, ForwardDecl,
@ -113,6 +114,9 @@ class NamespaceScope:
using_alias: typing.List[UsingAlias] = field(default_factory=list) using_alias: typing.List[UsingAlias] = field(default_factory=list)
ns_alias: typing.List[NamespaceAlias] = 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 #: Explicit template instantiations
template_insts: typing.List[TemplateInst] = field(default_factory=list) template_insts: typing.List[TemplateInst] = field(default_factory=list)
@ -243,6 +247,9 @@ class SimpleCxxVisitor:
def on_namespace_end(self, state: SNamespaceBlockState) -> None: def on_namespace_end(self, state: SNamespaceBlockState) -> None:
pass pass
def on_concept(self, state: SNonClassBlockState, concept: Concept) -> None:
state.user_data.concepts.append(concept)
def on_namespace_alias( def on_namespace_alias(
self, state: SNonClassBlockState, alias: NamespaceAlias self, state: SNonClassBlockState, alias: NamespaceAlias
) -> None: ) -> None:

View File

@ -551,6 +551,31 @@ class TemplateInst:
doxygen: typing.Optional[str] = None 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 <class T>
concept Meowable = is_meowable<T>;
template<typename T>
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 @dataclass
class ForwardDecl: class ForwardDecl:
""" """

View File

@ -8,6 +8,7 @@ else:
from .types import ( from .types import (
Concept,
EnumDecl, EnumDecl,
Field, Field,
ForwardDecl, ForwardDecl,
@ -89,6 +90,14 @@ class CxxVisitor(Protocol):
Called when a ``namespace`` alias is encountered Called when a ``namespace`` alias is encountered
""" """
def on_concept(self, state: NonClassBlockState, concept: Concept) -> None:
"""
.. code-block:: c++
template <class T>
concept Meowable = is_meowable<T>;
"""
def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None:
""" """
Called when a forward declaration is encountered Called when a forward declaration is encountered
@ -254,6 +263,9 @@ class NullVisitor:
def on_namespace_end(self, state: NamespaceBlockState) -> None: def on_namespace_end(self, state: NamespaceBlockState) -> None:
return None return None
def on_concept(self, state: NonClassBlockState, concept: Concept) -> None:
return None
def on_namespace_alias( def on_namespace_alias(
self, state: NonClassBlockState, alias: NamespaceAlias self, state: NonClassBlockState, alias: NamespaceAlias
) -> None: ) -> None:

403
tests/test_concepts.py Normal file
View File

@ -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 <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
template <Derived<Base> T> void f(T); // T is constrained by Derived<T, Base>
"""
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 <class T> constexpr bool is_meowable = true;
template <class T> constexpr bool is_cat = true;
template <class T>
concept Meowable = is_meowable<T>;
template <class T>
concept BadMeowableCat = is_meowable<T> && is_cat<T>;
"""
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 <typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
template <Hashable T> 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<class T>
concept Semiregular = DefaultConstructible<T> &&
CopyConstructible<T> && CopyAssignable<T> && Destructible<T> &&
requires(T a, std::size_t n)
{
requires Same<T*, decltype(&a)>; // nested: "Same<...> evaluates to true"
{ a.~T() } noexcept; // compound: "a.~T()" is a valid expression that doesn't throw
requires Same<T*, decltype(new T)>; // nested: "Same<...> evaluates to true"
requires Same<T*, decltype(new T[n])>; // 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="}"),
]
),
)
]
)
)