Skip to content

Commit

Permalink
ifex_ast: Prepare support for variant type
Browse files Browse the repository at this point in the history
Signed-off-by: Gunnar Andersson <[email protected]>
  • Loading branch information
gunnar-mb committed Dec 4, 2024
1 parent 00af57e commit 924df20
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 17 deletions.
6 changes: 5 additions & 1 deletion ifex/model/ifex_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,9 +436,12 @@ class Typedef:
name: str
""" Specifies the name of the typedef. """

datatype: str
datatype: Optional[str] = str()
""" Specifies datatype name of the typedef. """

datatypes: Optional[List[str]] = field(default_factory=EmptyList)
""" If specified, then the type is a variant type. At least two datatypes should be listed. The single datatype: field must *not* be used at the same time. """

description: Optional[str] = str()
""" Specifies the description of the typedef. """

Expand Down Expand Up @@ -685,5 +688,6 @@ class FundamentalTypes:
# name, description, min value, max value
["set", "A set (unique values), each of the same type. Format: set<ItemType>", "N/A", "N/A"],
["map", "A key-value mapping type. Format: map<keytype,valuetype>", "N/A", "N/A"],
["variant", "A variant (union) type that can carry any of a predefined set of types, akin to Union in C. Format: variant<type1,type2,type3...>", "N/A", "N/A"],
["opaque", "Indicates a complex type which is not explicitly defined in this context.", "N/A","N/A"]
]
114 changes: 98 additions & 16 deletions ifex/model/ifex_ast_introspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,44 @@

"""
Provide helper functions to inspect the IFEX Core IDL language definition,
as it is defined by the class tree/hierarchy (not an inheritance hierarchy)
in the `ifex_ast` python file. These function can be used by any other code
that needs to process this underlying meta-model. It helps to ensure that the
fundamental language is defined in a single file. """
as it is defined by the AST structure tree/hierarchy (not an inheritance hierarchy)
in the `ifex_ast` python file. These function can be used by implementations
that process the IFEX AST, or any other model designed in the same way.
"""

from ifex.model import ifex_ast
import re
import ifex_ast
from dataclasses import is_dataclass, fields
from typing import get_args, get_origin, List, Optional, Union, Any, ForwardRef
import typing

# As we traverse the "tree" of dataclass definitions, it can be quite difficult
# As we traverse the tree of dataclass definitions, it can be quite difficult
# to keep track of which type each variable has. Here is an explanation of how
# we try to keep track:
#
# The following functions come in two flavors each. Functions like: is_xxx()
# take an object, which is an instance of typing.<some class> which is
# essentially an object that indicates the "type hint" using concepts from
# the 'typing' python module. Examples are: Optional, List, Union, Any, etc.
# We here call variables that reference such a typing.Something object a
# type_indicator. It corresponds to the type hint information on the right
# side of the colon : in an expression like this:
# In these functions we call variables that reference such a typing.Something
# object, a type_indicator. The type_indicator corresponds to the type hint
# information on the right side of the colon : in an expression like this:
#
# namespaces: Optional[List[Namespace]]
#
# The type_indicator is the: `Optional[List[Namespace]]`
# (or if fully qualified: `typing.Optional[typing.List[ifex_ast.Namespace]]`)
# Note that instead of being a dataclass like ifex_ast.Namespace, the inner
# type can of course be a built-in simple type like str. e.g. typing.List[str]
# The inner type can be an instance of an AST node, in other words an instance
# of a @dataclass like ifex_ast.Namespace, or it can be a built-in simple type
# like str. e.g. typing.List[str]
#
# Next, in the 'dataclasses' python module we find the function fields().
# It returns a list that represents the fields (members) of the dataclass.
# Next, in the 'dataclasses' python module we find the function: fields().
# fields() returns a list of the fields (i.e member variables) of the dataclass.
# Each field is represented by an object (an instance of the dataclasses.Field
# class). We name variables that refer to such Field() instances as `field`.
# A field thus represents a member variable in the python (data)class.
# class). In the following function we name variables that refer to such
# Field() instances as `field`. A variable named field thus represents a
# member variable in the python (data)class.
#
# A field object contains several informations such as the name of the member
# variable (field.name), and the `.type` member, which gives us the
# type_indicator as described above.
Expand All @@ -55,7 +59,7 @@
# NOTE: Here in the descriptions we might refer to an object's "type" when we
# strictly mean its Type Indicator. Since typing in python is dynamic,
# the actual type of an object could be different (and can be somewhat fungible
# too in theory, but generally not in this code).
# too in theory, but usually not in this code).

def is_dataclass_type(cls):
"""Check if a class is a dataclass."""
Expand Down Expand Up @@ -142,6 +146,73 @@ def field_referenced_type(f):
else:
return field_actual_type(f)


# --- End of generic functions --

# ------------------------------------------------------------------------------------------------
# Above thes line we had generic functions that give information about a AST-model built
# from a number of @dataclasses and field typing information.
#
# We can notice that those only refers to *python* concepts such as @dataclasses and the typing module.
# This means, those functions could also be used for other similarly described AST models, beyond
# the IFEX Core IDL model.
#
# Below this line follows functions that are specific to IFEX. For example is_ifex_variant_typedef evaluates
# an actual IFEX-specific concern about the IFEX variant type. It is not a variant type of the python
# typing concept (which is called typing.Union anyhow) - these functions are about IFEX-specific concerns.

# Check if string is "variant<something>" where something can be empty
p_shortform_variant0 = r'variant<\s*([^,]*\s*(?:,\s*[^,]*)*)\s*>'

# ...and this for variant<at_least_one>
p_shortform_variant1 = r'variant<\s*([^,]+(?:\s*,\s*[^,]+)*)\s*>'

# ...and this is the one we actually need. A valid variant has at least 2
# types listed or it would not make sense. This last pattern guarantees this:
p_shortform_variant2 = r'variant<\s*[^,]+(?:\s*,\s*[^,]+)+\s*>'

def is_ifex_variant_shortform(s):
""" Answer if a Typedef object has datatype defined to using the short form: variant<type1,type2,type3...> """

# Convert "truthy" result object to actual bool for nicer debugging
return bool(s and s != '' and re.match(p_shortform_variant2, s))

def is_ifex_variant_typedef(f):
""" Answer if a Typedef object uses a variant type.
A variant type can be defined in either one of these two ways:
1. The field "datatypes" has a value, in other words there is a *list* of datatypes, as opposed to only one
or:
2. That the short form syntax is used in the datatype name: variant<type1,type2,...>."""

# Convert "truthy" result object to actual bool for nicer debugging
return bool( isinstance(f, ifex_ast.Typedef) and (f.datatypes or is_ifex_variant_shortform(f.datatype)) )

def is_ifex_invalid_typedef(f):
"""Check if both a single and multiple datatypes are defined. That is invalid."""
return is_ifex_variant_typedef(f) and f.datatype and f.datatypes

def get_variant_types(obj):
"""Return a list of the types handled by the given variant type. The function accepts either a Typedef object or a string with the type name. The string must then be the variant<a,b,c> fundamental type - it cannot be the name of a typedef."""
if isinstance(obj, ifex_ast.Typedef):
if is_ifex_invalid_typedef(obj):
raise TypeException('Provided variant object is misconfigured')
# Process datatypes list
if obj.datatypes:
return obj.datatypes
# or process single datatype (string)
else:
return get_variant_types(obj.datatype)
elif type(obj) == str:
match = re.search(r'variant *<(.*?)>', obj)
if match:
types = match.group(1).split(',')
return [t.strip() for t in types]

# (else) Any other cases = error
raise Exception('Provided object is not a variant type: {obj=}')

# ------------------------------------------------------------------------------------------------

VERBOSE = False

# Tree processing function:
Expand Down Expand Up @@ -210,3 +281,14 @@ def _simple_process(arg):
if __name__ == "__main__":
print("TEST: Note that already seen types are skipped, and this is a depth-first search => The structure of the tree is not easily seen from this output.")
walk_type_tree(ifex_ast.Namespace, _simple_process)

x = ifex_ast.Typedef("name", datatype="variant<a,b>")
print(f"{is_ifex_variant_shortform(x.datatype)=}")
print(f"{is_ifex_variant_typedef(x)=}")

y = ifex_ast.Typedef("name", datatypes=["foo", "bar", "baz"])
print(f"{is_ifex_variant_shortform(y.datatype)=}")
print(f"{is_ifex_variant_typedef(y)=}")
print(f"The types of y are: {get_variant_types(y)}")
print(f"The types of x are: {get_variant_types(x)}")
print(f"The types of variant<this, that ,and,another > are: {get_variant_types("variant<this, that ,and,another >")}")

0 comments on commit 924df20

Please sign in to comment.