Skip to content

Commit

Permalink
new: Attribute parser
Browse files Browse the repository at this point in the history
Beta release of a C++ attribute parser that allows creating attribute signatures with the expected types - with basic support for numbers and strings for now. The API may still change slightly. This is a preview before the full release of the preprocessor module.
  • Loading branch information
JhnW committed Oct 6, 2024
1 parent 0f1be1e commit 0cf5178
Show file tree
Hide file tree
Showing 10 changed files with 559 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from typing import Type, Iterable, Optional
from typing import Iterable, Optional, Dict, Any
from itertools import count
from devana.preprocessing.components.property.parsers.types import *
from devana.preprocessing.components.property.parsers.descriptions import IDescribedProperty, IDescribedType
from devana.preprocessing.components.property.parsers.descriptions import IDescribedProperty
from devana.preprocessing.preprocessor import IGenerator
from devana.syntax_abstraction.attribute import Attribute
from devana.preprocessing.components.property.parsers.result import Result
from devana.preprocessing.components.property.parsers.result import Result, Arguments, Value, PropertySignature
from devana.preprocessing.components.property.parsers.configuration import Configuration
from devana.preprocessing.components.property.parsers.parser import ParsingBackend
from devana.preprocessing.components.property.parsers.types import parsable_element_from_described_type


class AttributeParser(IGenerator):
Expand All @@ -14,15 +17,19 @@ def __init__(self, properties: Optional[List[IDescribedProperty]] = None,
configuration: Optional[Configuration] = None):
self._properties = properties if properties is not None else []
self._configuration = configuration if configuration else Configuration(ignore_unknown=True)
self._backend = ParsingBackend()
if self._configuration.ignore_unknown is False:
raise ValueError("The attribute parser does not allow a restrictive approach to unknown attributes due to "
"the existence of attributes that are compiler extensions.")
for p in self._properties:
self._validate_property(p)
for prop in self._properties:
self._validate_property(prop)
self._add_types(prop)


def add_property(self, prop: IDescribedProperty):
self._validate_property(prop)
self._properties.append(prop)
self._add_types(prop)

@classmethod
def get_required_type(cls) -> Type:
Expand All @@ -33,11 +40,119 @@ def get_produced_type(cls) -> Type:
return Result

def generate(self, data: Iterable[Attribute]) -> Iterable[Result]:
return []
for attr in data:

def maybe_proto_gen(in_attr):
maybe_prop_result = filter(lambda e: e.namespace == in_attr.namespace, self._properties)
maybe_prop_result = list(filter(lambda e: e.name == in_attr.name, maybe_prop_result))
return maybe_prop_result

maybe_prop = maybe_proto_gen(attr)
if len(maybe_prop) == 0:
raise ValueError(f"Unknown property: {attr.name} in namespace: {attr.namespace}")
prop: IDescribedProperty = maybe_prop[0]
# matching property find as prop, so lets parse arguments
parsed_arguments = []
if attr.arguments is not None:
parsed_arguments = [self._backend.parse_argument(a) for a in attr.arguments]

# check if positional arguments are not mixed with named ones
is_positional_finished = False
for a in parsed_arguments:
if a.name is None and is_positional_finished:
raise ValueError(f"Mixed positional arguments and named is not allowed. Error in {prop.name}")
if a.name is not None:
is_positional_finished = True

result_args = Arguments()

result_positional_arguments = []
result_named_arguments: Dict[str, Any] = {}

# now try a match parsing result to expected arguments

for i, a in zip(count(), parsed_arguments):
if a.name is None:
result_positional_arguments.append(a)
else:
if a.name in result_named_arguments:
raise ValueError(f"Duplicated argument name {a.name} while parsing property {prop.name}.")
result_named_arguments[a.name] = a

# now we need to do the main validation - connect the paired parameters with the expected arguments
for i, a in zip(count(), result_positional_arguments):
if i >= len(prop.arguments):
raise ValueError("Too many arguments.")
expected_type = prop.arguments[i].type
given_type = a.type
if expected_type.name != given_type.name: # types have unique names
raise ValueError(f"Expected type {expected_type} but got {given_type}")
result_args.positional.append(Value(a.value))

for key in result_named_arguments:

def expected_types_gen(expected_name: str, props: IDescribedProperty):
return list(filter(lambda e: e.name == expected_name, props.arguments))

expected_type_list = expected_types_gen(key, prop)
if len(expected_type_list) == 0:
raise ValueError(f"Unknown argument named {key} in {prop.name}")
expected_type = expected_type_list[0].type
given_type = result_named_arguments[key].type
if expected_type.name != given_type.name: # types have unique names
raise ValueError(f"Expected type {expected_type} but got {given_type}")
result_args.named[key] = Value(result_named_arguments[key].value)

# now make a list of the remaining arguments based on the property description
remaining_expected_args = prop.arguments.copy()
del remaining_expected_args[: len(result_args.positional)]

for key in result_args.named:
def elements_gen(key_name: str, expected):
return list(filter(lambda e: e.name == key_name, expected))

elements = elements_gen(key, remaining_expected_args)
if len(elements) == 0:
raise ValueError(f"Unknown argument named {key} in {prop.name}")
if len(elements) > 1:
raise ValueError(f"Multiple argument named {key} in {prop.name}")
element = elements[0]
remaining_expected_args.remove(element)

# the parser does not provide default parameter values (because it is not its job)
# but we need to check if the function signature is satisfied - that is,
# if all parameters that do not
# have default values have been provided
if list(filter(lambda e: e.default_value is None, remaining_expected_args)):
raise ValueError(f"Not all parameters were provided for: {prop.name}")

signature = PropertySignature(
name = prop.name,
namespaces = [prop.namespace] if prop.namespace else [],
arguments = [self._backend.types[n].result_type for n in [e.type.name for e in prop.arguments]]
)

yield Result(
property = signature,
arguments= result_args,
target=attr.parent
)


@staticmethod
def _validate_property(prop: IDescribedProperty):
"""Check if property is able to usage."""
if not prop.namespace:
raise ValueError("Due to the existence of compiler extension attributes and standard standard "
"attributes, it is required that preprocessor attributes use namespace.")
# check duplicates
for a1 in prop.arguments:
for a2 in prop.arguments:
if a1 != a2:
if (a1.name is not None or a2.name is not None) and a1.name == a2.name:
raise ValueError(f"Duplicated property name for property: {prop.name}")

def _add_types(self, desc: IDescribedProperty):
"""Add types to parser from IDescribedProperty."""
for arg in desc.arguments:
self._backend.add_type(parsable_element_from_described_type(arg.type))
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ def type(self) -> IDescribedType:
def default_value(self) -> Optional[IDescribedValue]:
"""Default value of argument, if any."""

@property
@abstractmethod
def name(self) -> Optional[str]:
"""Name of argument.
If the argument is only and exclusively positional, it can be None.
Otherwise, the syntax name1 = val1, name2 = val, etc. is allowed."""


class IDescribedProperty(ABC):
"""Description property for parsing and diagnostic messages."""
Expand Down
72 changes: 70 additions & 2 deletions src/devana/preprocessing/components/property/parsers/parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Optional, Any, Union, Type
import re
from typing import Optional, Any, Union, Type, Dict
from abc import ABC, abstractmethod
from dataclasses import dataclass

from devana.preprocessing.components.property.parsers.descriptions import IDescribedType

@dataclass
class ParsableElementError:
Expand All @@ -26,3 +27,70 @@ def parse(self, text: str) -> Union[ParsableElementError, Any]:
@abstractmethod
def result_type(self) -> Type:
"""Python type of result."""


class IParsableType(IParsableElement, IDescribedType, ABC):
"""Mix of two interfaces needed by parser."""


class ParsingBackend:
"""Core backend for all parser."""

@dataclass
class Argument:
"""Class holding result arguments."""
value: Any
type: IDescribedType
name: Optional[str] = None


def __init__(self):
self._types: Dict[str, IParsableElement] = {}

@property
def types(self) -> Dict[str, IParsableElement]:
return self._types

def add_type(self, element: IParsableType):
"""Add a new type to parser. May raise exceptions for duplicate types."""
if element.name not in self._types:
self._types[element.name] = element
if element != self._types[element.name]:
raise ValueError("Duplicate type name.")

#__argument_pattern = re.compile(r'(^\s*(?P<name>(\w|")+)\s*=\s*(?P<named_value>\w+)\s*$)|^\s*(?P<value>(\w|")+)\s*$')

__argument_pattern = re.compile(
r'(^\s*(?P<name>\w+)\s*=\s*(?P<named_value>.+)\s*$)|^\s*(?P<value>.+)\s*$')


@dataclass
class ParsedValue:
"""Internal value parsing result."""
text: str
type: IDescribedType


def _parse_value(self, text: str) -> ParsedValue:
for t in self._types.values():
result = t.parse(text)
if isinstance(result, ParsableElementError):
if result.is_meaningless:
continue
raise ValueError(f"Parsing error: {result.what}")
return ParsingBackend.ParsedValue(result, t)
raise ValueError("Unable to find matching type.")

def parse_argument(self, text: str) -> Argument:
match = self.__argument_pattern.match(text)
if match is None:
raise ValueError("Cannot parse argument.")
matches = match.groupdict()
if matches["value"] is not None:
parsing_result = self._parse_value(matches["value"])
return self.Argument(parsing_result.text, parsing_result.type)
elif matches["named_value"] is not None and matches["name"] is not None:
parsing_result = self._parse_value(matches["named_value"])
return self.Argument(parsing_result.text, parsing_result.type, matches["name"])
else:
raise ValueError("Cannot parse argument.")
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ def type(self) -> Type:
class PropertySignature:
"""Invoking target property identification data."""
name: str
namespaces: List[str] = field(default_factory=lambda: [])
arguments: List[Type] = field(default_factory=lambda: [])
namespaces: List[str] = field(default_factory=list)
arguments: List[Type] = field(default_factory=list)


@dataclass
class Arguments:
"""Calling arguments."""
positional: List[Value]
named: Dict[str, Value]
positional: List[Value] = field(default_factory=list)
named: Dict[str, Value] = field(default_factory=dict)


@dataclass
Expand Down
37 changes: 28 additions & 9 deletions src/devana/preprocessing/components/property/parsers/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from typing import List, Union, Type
import re
from devana.preprocessing.components.property.parsers.descriptions import IDescribedType
from devana.preprocessing.components.property.parsers.parser import IParsableElement, ParsableElementError
from devana.preprocessing.components.property.parsers.parser import IParsableType, ParsableElementError
from devana.utility.typeregister import register


class IntegerType(IDescribedType, IParsableElement):
__register = []

@register(__register)
class IntegerType(IParsableType):
"""Representation of integer number."""

@property
Expand All @@ -24,7 +28,8 @@ def result_type(self) -> Type:
return int


class FloatType(IDescribedType, IParsableElement):
@register(__register)
class FloatType(IParsableType):
"""Representation of floating point number."""

@property
Expand All @@ -44,7 +49,8 @@ def result_type(self) -> Type:
return float


class StringType(IDescribedType, IParsableElement):
@register(__register)
class StringType(IParsableType):
"""Representation of text."""

@property
Expand All @@ -64,7 +70,8 @@ def result_type(self) -> Type:
return str


class BooleanType(IDescribedType, IParsableElement):
@register(__register)
class BooleanType(IParsableType):
"""Representation of true/false."""

@property
Expand Down Expand Up @@ -105,7 +112,7 @@ def name(self) -> str:


class _GenericType(IDescribedType):
"""internal mixin for generic implementation."""
"""Internal mixin for generic implementation."""
def __init__(self, name: str, specialization: IDescribedType):
self._name = name
self._specialization = specialization
Expand All @@ -120,19 +127,19 @@ def name(self) -> str:


class ListType(_GenericType):
"""internal of list like []"""
"""Internal of list like []"""
def __init__(self, specialization: IDescribedType):
super().__init__("List", specialization)


class OptionalType(_GenericType):
"""internal of optional can be value of specialization type or none."""
"""Internal of optional can be value of specialization type or none."""
def __init__(self, specialization: IDescribedType):
super().__init__("Optional", specialization)


class UnionType(IDescribedType):
"""internal of union type can be value of many types."""
"""Internal of union type can be value of many types."""
def __init__(self, types: List[IDescribedType]):
self._types = types

Expand All @@ -143,3 +150,15 @@ def types(self) -> List[IDescribedType]:
@property
def name(self) -> str:
return f"Union<{'|'.join([s.name for s in self._types])}>"


def parsable_element_from_described_type(desc: IDescribedType) -> IParsableType:
"""Create a parsing type from described."""
if isinstance(desc, IParsableType):
return desc
matches = list(filter(lambda e: e.name == desc.name, __register))
if len(matches) == 0:
raise ValueError(f"Could not find parser for type: {desc.name}")
if len(matches) > 1:
raise ValueError(f"Found more than one parser for type: {desc.name}")
return matches[0]
1 change: 1 addition & 0 deletions src/devana/utility/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
from .lazy import LazyNotInit, lazy_invoke
from .errors import CodeError, ParserError
from .fakeenum import FakeEnum
from .typeregister import register
5 changes: 3 additions & 2 deletions src/devana/utility/lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@


class LazyNotInit:
"""Value used by lazy_invoke to determine property is initialized or not. Set this type to value of property
in init function to inform that value must be initialized. For example, self._name = LazyNotInit"""
"""The Value used by lazy_invoke to determine property is initialized or not.
Set this type to value of property in init function to inform that value must be initialized.
For example, self._name = LazyNotInit"""

def __new__(cls):
return cls
Expand Down
10 changes: 10 additions & 0 deletions src/devana/utility/typeregister.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import List, Type

def register(current_register: List[Type]):
"""Registers a type in the given variable - usually a global module variable.
Useful for automatically creating lists of default supported classes, etc."""

def wrapper(cls: Type):
current_register.append(cls())
return cls
return wrapper
Loading

0 comments on commit 0cf5178

Please sign in to comment.