Skip to content

Commit

Permalink
Add type hinting consistency test. Fix Root.external value unset
Browse files Browse the repository at this point in the history
  • Loading branch information
amykyta3 committed Jan 8, 2025
1 parent f4059a9 commit df85e7e
Show file tree
Hide file tree
Showing 11 changed files with 299 additions and 52 deletions.
1 change: 1 addition & 0 deletions src/systemrdl/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ def elaborate(self, top_def_name: Optional[str]=None, inst_name: Optional[str]=N
root_inst.is_instance = True
root_inst.original_def = self.root
root_inst.inst_name = "$root"
root_inst.external = False # meaningless, but must not be None

# Create a top-level instance
top_inst = top_def._copy_for_inst({})
Expand Down
2 changes: 1 addition & 1 deletion src/systemrdl/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -2181,7 +2181,7 @@ def aliases(self, skip_not_present: bool = True) -> List['RegNode']:

#===============================================================================
class RegfileNode(AddressableNode):
parent: 'AddrmapNode'
parent: Union['AddrmapNode', 'RegfileNode']
inst: comp.Regfile

@overload # type: ignore[override]
Expand Down
Empty file added test/lib/__init__.py
Empty file.
70 changes: 70 additions & 0 deletions test/lib/type_hint_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import typing
import inspect

def hint_is_a_generic(hint) -> bool:
"""
Typing generics have an __origin__ attribute
"""
if "__origin__" in dir(hint):
return True
return False

def hint_is(hint, generic_origin) -> bool:
"""
Compare whether the hint matches a generic origin
"""
if not hint_is_a_generic(hint):
return False
return hint.__origin__ == generic_origin


def value_is_compatible(value, hint) -> bool:
"""
Given any value, check whether it is compatible with a type hint annotation
"""
if hint_is_a_generic(hint):
# Unpack the generic
if hint_is(hint, typing.Union):
# Check if value matches any of the types in the Union
for arg_hint in hint.__args__:
if value_is_compatible(value, arg_hint):
return True
return False
elif hint_is(hint, type):
if not inspect.isclass(value):
return False
expected_type = hint.__args__[0]
return issubclass(value, expected_type)
elif hint_is(hint, list):
# Check if value is a list
if inspect.isclass(value):
return False
if not isinstance(value, list):
return False
# Check that all members of the list match the expected list type
expected_hint = hint.__args__[0]
for element in value:
if not value_is_compatible(element, expected_hint):
return False
return True
elif hint_is(hint, dict):
# Check if value is a dict
if inspect.isclass(value):
return False
if not isinstance(value, dict):
return False
expected_key_hint, expected_value_hint = hint.__args__
# Check that all keys match the expected type
for key in value.keys():
if not value_is_compatible(key, expected_key_hint):
return False
# Check that all values match the expected type
for element in value.values():
if not value_is_compatible(element, expected_value_hint):
return False
return True
else:
raise RuntimeError(f"Unhandled generic {hint}: {hint.__origin__}")

# hint is an actual class
return isinstance(value, hint)
41 changes: 0 additions & 41 deletions test/rdl_src/prop_ref.rdl → test/rdl_err_src/prop_ref_err.rdl
Original file line number Diff line number Diff line change
@@ -1,30 +1,3 @@

addrmap prop_value_ref {
reg {
default sw=rw;
default hw=rw;
field {} a = 0;
field {} b;
field {} c;
field {} d;
field {} e;
b->next = a->reset;
c->next = a->reset;
d->next = a->anded;
e->next = b->anded;
}y;
};

addrmap ref_in_array {
reg {
default sw=rw;
default hw=rw;
field {} a = 0;
field {} b = 0;
b->next = a->anded;
} myreg[8];
};

addrmap err_missing_reset {
reg {
default sw=rw;
Expand Down Expand Up @@ -55,20 +28,6 @@ addrmap err_self_reset {
}y;
};

addrmap inferred_vector {
reg {
default sw=rw;
default hw=rw;
field {
sw=rw; hw=w; we;
} a;
field {} b;
field {} c;
b->next = a->we;
c->next = a->wel;
}y;
};

addrmap err_no_inferred {
reg {
default sw=rw;
Expand Down
9 changes: 9 additions & 0 deletions test/rdl_src/prop_ref-in_array.rdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
addrmap ref_in_array {
reg {
default sw=rw;
default hw=rw;
field {} a = 0;
field {} b = 0;
b->next = a->anded;
} myreg[8];
};
13 changes: 13 additions & 0 deletions test/rdl_src/prop_ref-inferred_vector.rdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
addrmap inferred_vector {
reg {
default sw=rw;
default hw=rw;
field {
sw=rw; hw=w; we;
} a;
field {} b;
field {} c;
b->next = a->we;
c->next = a->wel;
}y;
};
16 changes: 16 additions & 0 deletions test/rdl_src/prop_ref-value_ref.rdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

addrmap prop_value_ref {
reg {
default sw=rw;
default hw=rw;
field {} a = 0;
field {} b;
field {} c;
field {} d;
field {} e;
b->next = a->reset;
c->next = a->reset;
d->next = a->anded;
e->next = b->anded;
}y;
};
2 changes: 1 addition & 1 deletion test/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if exists ccache; then
fi

# Initialize venv
python3 -m venv .venv
python3.11 -m venv .venv
source .venv/bin/activate

# Install
Expand Down
179 changes: 179 additions & 0 deletions test/test_api_type_hints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import glob
import os
from typing import Union, List, Optional

from typing_extensions import Literal, get_overloads, get_type_hints
from parameterized import parameterized_class

from lib.type_hint_utils import value_is_compatible, hint_is
from systemrdl.node import AddressableNode, FieldNode, MemNode, Node, AddrmapNode, RegNode, RegfileNode, RootNode, SignalNode, VectorNode
from systemrdl import component as comp
from unittest_utils import RDLSourceTestCase

from systemrdl.walker import RDLListener, RDLWalker, WalkerAction

# Get all RDL sources
rdl_src_files = glob.glob("rdl_src/*.rdl")

# Exclude some files as root compile targets:
exclude = {
"rdl_src/preprocessor_incl.rdl", # is an include file
"rdl_src/udp_builtin.rdl", # uses builtins
}

# Build testcase list
cases = []
for file in rdl_src_files:
if file in exclude:
continue
args = {
"name": os.path.basename(file),
"src": file,
}
cases.append(args)

@parameterized_class(cases)
class TestTypeHints(RDLSourceTestCase):
def check_node_properties(self, node):
"""
Check that each of the node's properties returns a value that matches
the expected annotated type hint
"""
gp_overloads = get_overloads(node.get_property)
self.assertGreater(len(gp_overloads), 0)

for gp_overload in gp_overloads:
hints = get_type_hints(gp_overload)

# Skip overloads that use default override signature:
# get_property(*, default=...)
if "default" in hints:
continue

# Skip generic overloads that do not specify an explicit property
if not hint_is(hints["prop_name"], Literal):
continue

# Currently assuming only one arg to Literal
self.assertEqual(len(hints["prop_name"].__args__), 1)
property_name = hints["prop_name"].__args__[0]
#print(f"Checking {node.get_path()}->{property_name}")

value = node.get_property(property_name)
self.assertTrue(
value_is_compatible(value, hints["return"]),
f"Value '{value}' does not match expected type: {hints['return']}. "
f"for: {node.get_path()}->{property_name}"
)

def assert_attr_type_hint(self, node, attr_name, hint):
"""
Assert a node's attribute matches the expected type hint
"""
value = getattr(node, attr_name)
self.assertTrue(
value_is_compatible(value, hint),
f"Value '{value}' does not match expected type: {hint}."
f"for: {node.get_path()}::{attr_name}"
)

def test_all_nodes(self):
root = self.compile(
[self.src],
incl_search_paths=["rdl_src/incdir"]
)

walker = RDLWalker(skip_not_present=False)
listener = RDLTestListener(self)
walker.walk(root, listener)

# Test root itself
self.assertIsNone(root.parent)
self.assert_attr_type_hint(root, "inst", comp.Root)
self.assert_attr_type_hint(root, "inst_name", str)
self.assert_attr_type_hint(root, "type_name", Optional[str])
self.assert_attr_type_hint(root, "orig_type_name", Optional[str])
self.assert_attr_type_hint(root, "external", bool)

class RDLTestListener(RDLListener):
def __init__(self, test_class: TestTypeHints) -> None:
super().__init__()
self.test_class = test_class

def enter_Component(self, node: Node) -> None:
self.test_class.check_node_properties(node)
self.test_class.assert_attr_type_hint(node, "owning_addrmap", Optional[AddrmapNode])
self.test_class.assert_attr_type_hint(node, "inst_name", str)
self.test_class.assert_attr_type_hint(node, "type_name", Optional[str])
self.test_class.assert_attr_type_hint(node, "orig_type_name", Optional[str])
self.test_class.assert_attr_type_hint(node, "external", bool)

def enter_AddressableComponent(self, node: AddressableNode) -> None:
self.test_class.assert_attr_type_hint(node, "raw_address_offset", int)
self.test_class.assert_attr_type_hint(node, "raw_absolute_address", int)
self.test_class.assert_attr_type_hint(node, "size", int)
self.test_class.assert_attr_type_hint(node, "total_size", int)
if node.is_array:
self.test_class.assert_attr_type_hint(node, "array_dimensions", List[int])
self.test_class.assert_attr_type_hint(node, "array_stride", int)
else:
self.test_class.assertIsNone(node.array_dimensions)
self.test_class.assertIsNone(node.array_stride)


def enter_VectorComponent(self, node: VectorNode) -> None:
self.test_class.assert_attr_type_hint(node, "width", int)
self.test_class.assert_attr_type_hint(node, "msb", int)
self.test_class.assert_attr_type_hint(node, "lsb", int)
self.test_class.assert_attr_type_hint(node, "high", int)
self.test_class.assert_attr_type_hint(node, "low", int)

def enter_Signal(self, node: SignalNode) -> None:
self.test_class.assert_attr_type_hint(node, "parent", Node)
self.test_class.assert_attr_type_hint(node, "inst", comp.Signal)

def enter_Field(self, node: FieldNode) -> None:
self.test_class.assert_attr_type_hint(node, "parent", RegNode)
self.test_class.assert_attr_type_hint(node, "inst", comp.Field)
self.test_class.assert_attr_type_hint(node, "is_virtual", bool)
self.test_class.assert_attr_type_hint(node, "is_volatile", bool)
self.test_class.assert_attr_type_hint(node, "is_sw_writable", bool)
self.test_class.assert_attr_type_hint(node, "is_sw_readable", bool)
self.test_class.assert_attr_type_hint(node, "is_hw_writable", bool)
self.test_class.assert_attr_type_hint(node, "is_hw_readable", bool)
self.test_class.assert_attr_type_hint(node, "implements_storage", bool)
self.test_class.assert_attr_type_hint(node, "is_up_counter", bool)
self.test_class.assert_attr_type_hint(node, "is_down_counter", bool)
self.test_class.assert_attr_type_hint(node, "is_alias", bool)
self.test_class.assert_attr_type_hint(node, "has_aliases", bool)

def enter_Reg(self, node: RegNode) -> None:
self.test_class.assert_attr_type_hint(node, "parent", Union[AddrmapNode, RegfileNode, MemNode])
self.test_class.assert_attr_type_hint(node, "inst", comp.Reg)
self.test_class.assert_attr_type_hint(node, "size", int)
self.test_class.assert_attr_type_hint(node, "is_virtual", bool)
self.test_class.assert_attr_type_hint(node, "has_sw_writable", bool)
self.test_class.assert_attr_type_hint(node, "has_sw_readable", bool)
self.test_class.assert_attr_type_hint(node, "has_hw_writable", bool)
self.test_class.assert_attr_type_hint(node, "has_hw_readable", bool)
self.test_class.assert_attr_type_hint(node, "is_interrupt_reg", bool)
self.test_class.assert_attr_type_hint(node, "is_halt_reg", bool)
self.test_class.assert_attr_type_hint(node, "is_alias", bool)
self.test_class.assert_attr_type_hint(node, "has_aliases", bool)

def enter_Regfile(self, node: RegfileNode) -> None:
self.test_class.assert_attr_type_hint(node, "parent", Union[AddrmapNode, RegfileNode])
self.test_class.assert_attr_type_hint(node, "inst", comp.Regfile)
self.test_class.assert_attr_type_hint(node, "size", int)

def enter_Addrmap(self, node: AddrmapNode) -> None:
self.test_class.assert_attr_type_hint(node, "parent", Union[AddrmapNode, RootNode])
self.test_class.assert_attr_type_hint(node, "inst", comp.Addrmap)
self.test_class.assert_attr_type_hint(node, "size", int)

def enter_Mem(self, node: MemNode) -> None:
self.test_class.assert_attr_type_hint(node, "parent", AddrmapNode)
self.test_class.assert_attr_type_hint(node, "inst", comp.Mem)
self.test_class.assert_attr_type_hint(node, "size", int)
self.test_class.assert_attr_type_hint(node, "is_sw_writable", bool)
self.test_class.assert_attr_type_hint(node, "is_sw_readable", bool)
Loading

0 comments on commit df85e7e

Please sign in to comment.