Skip to content

Commit 2474ef4

Browse files
committed
Add type hinting consistency test. Fix Root.external value unset
1 parent f4059a9 commit 2474ef4

File tree

11 files changed

+305
-52
lines changed

11 files changed

+305
-52
lines changed

src/systemrdl/compiler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ def elaborate(self, top_def_name: Optional[str]=None, inst_name: Optional[str]=N
369369
root_inst.is_instance = True
370370
root_inst.original_def = self.root
371371
root_inst.inst_name = "$root"
372+
root_inst.external = False # meaningless, but must not be None
372373

373374
# Create a top-level instance
374375
top_inst = top_def._copy_for_inst({})

src/systemrdl/node.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2181,7 +2181,7 @@ def aliases(self, skip_not_present: bool = True) -> List['RegNode']:
21812181

21822182
#===============================================================================
21832183
class RegfileNode(AddressableNode):
2184-
parent: 'AddrmapNode'
2184+
parent: Union['AddrmapNode', 'RegfileNode']
21852185
inst: comp.Regfile
21862186

21872187
@overload # type: ignore[override]

test/lib/__init__.py

Whitespace-only changes.

test/lib/type_hint_utils.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import typing
2+
import inspect
3+
4+
def hint_is_a_generic(hint) -> bool:
5+
"""
6+
Typing generics have an __origin__ attribute
7+
"""
8+
if "__origin__" in dir(hint):
9+
return True
10+
return False
11+
12+
def hint_is(hint, generic_origin) -> bool:
13+
"""
14+
Compare whether the hint matches a generic origin
15+
"""
16+
if not hint_is_a_generic(hint):
17+
return False
18+
return hint.__origin__ == generic_origin
19+
20+
21+
def value_is_compatible(value, hint) -> bool:
22+
"""
23+
Given any value, check whether it is compatible with a type hint annotation
24+
"""
25+
if hint_is_a_generic(hint):
26+
# Unpack the generic
27+
if hint_is(hint, typing.Union):
28+
# Check if value matches any of the types in the Union
29+
for arg_hint in hint.__args__:
30+
if value_is_compatible(value, arg_hint):
31+
return True
32+
return False
33+
elif hint_is(hint, type):
34+
if not inspect.isclass(value):
35+
return False
36+
expected_type = hint.__args__[0]
37+
return issubclass(value, expected_type)
38+
elif hint_is(hint, list):
39+
# Check if value is a list
40+
if inspect.isclass(value):
41+
return False
42+
if not isinstance(value, list):
43+
return False
44+
# Check that all members of the list match the expected list type
45+
expected_hint = hint.__args__[0]
46+
for element in value:
47+
if not value_is_compatible(element, expected_hint):
48+
return False
49+
return True
50+
elif hint_is(hint, dict):
51+
# Check if value is a dict
52+
if inspect.isclass(value):
53+
return False
54+
if not isinstance(value, dict):
55+
return False
56+
expected_key_hint, expected_value_hint = hint.__args__
57+
# Check that all keys match the expected type
58+
for key in value.keys():
59+
if not value_is_compatible(key, expected_key_hint):
60+
return False
61+
# Check that all values match the expected type
62+
for element in value.values():
63+
if not value_is_compatible(element, expected_value_hint):
64+
return False
65+
return True
66+
else:
67+
raise RuntimeError(f"Unhandled generic {hint}: {hint.__origin__}")
68+
69+
# hint is an actual class
70+
return isinstance(value, hint)

test/rdl_src/prop_ref.rdl renamed to test/rdl_err_src/prop_ref_err.rdl

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,3 @@
1-
2-
addrmap prop_value_ref {
3-
reg {
4-
default sw=rw;
5-
default hw=rw;
6-
field {} a = 0;
7-
field {} b;
8-
field {} c;
9-
field {} d;
10-
field {} e;
11-
b->next = a->reset;
12-
c->next = a->reset;
13-
d->next = a->anded;
14-
e->next = b->anded;
15-
}y;
16-
};
17-
18-
addrmap ref_in_array {
19-
reg {
20-
default sw=rw;
21-
default hw=rw;
22-
field {} a = 0;
23-
field {} b = 0;
24-
b->next = a->anded;
25-
} myreg[8];
26-
};
27-
281
addrmap err_missing_reset {
292
reg {
303
default sw=rw;
@@ -55,20 +28,6 @@ addrmap err_self_reset {
5528
}y;
5629
};
5730

58-
addrmap inferred_vector {
59-
reg {
60-
default sw=rw;
61-
default hw=rw;
62-
field {
63-
sw=rw; hw=w; we;
64-
} a;
65-
field {} b;
66-
field {} c;
67-
b->next = a->we;
68-
c->next = a->wel;
69-
}y;
70-
};
71-
7231
addrmap err_no_inferred {
7332
reg {
7433
default sw=rw;

test/rdl_src/prop_ref-in_array.rdl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
addrmap ref_in_array {
2+
reg {
3+
default sw=rw;
4+
default hw=rw;
5+
field {} a = 0;
6+
field {} b = 0;
7+
b->next = a->anded;
8+
} myreg[8];
9+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
addrmap inferred_vector {
2+
reg {
3+
default sw=rw;
4+
default hw=rw;
5+
field {
6+
sw=rw; hw=w; we;
7+
} a;
8+
field {} b;
9+
field {} c;
10+
b->next = a->we;
11+
c->next = a->wel;
12+
}y;
13+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
addrmap prop_value_ref {
3+
reg {
4+
default sw=rw;
5+
default hw=rw;
6+
field {} a = 0;
7+
field {} b;
8+
field {} c;
9+
field {} d;
10+
field {} e;
11+
b->next = a->reset;
12+
c->next = a->reset;
13+
d->next = a->anded;
14+
e->next = b->anded;
15+
}y;
16+
};

test/run.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ if exists ccache; then
1515
fi
1616

1717
# Initialize venv
18-
python3 -m venv .venv
18+
python3.11 -m venv .venv
1919
source .venv/bin/activate
2020

2121
# Install

test/test_api_type_hints.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import glob
2+
import os
3+
from typing import Union, List, Optional
4+
import sys
5+
6+
from typing_extensions import Literal, get_overloads, get_type_hints
7+
from parameterized import parameterized_class
8+
import pytest
9+
10+
from lib.type_hint_utils import value_is_compatible, hint_is
11+
from systemrdl.node import AddressableNode, FieldNode, MemNode, Node, AddrmapNode, RegNode, RegfileNode, RootNode, SignalNode, VectorNode
12+
from systemrdl import component as comp
13+
from unittest_utils import RDLSourceTestCase
14+
15+
from systemrdl.walker import RDLListener, RDLWalker, WalkerAction
16+
17+
# Get all RDL sources
18+
rdl_src_files = glob.glob("rdl_src/*.rdl")
19+
20+
# Exclude some files as root compile targets:
21+
exclude = {
22+
"rdl_src/preprocessor_incl.rdl", # is an include file
23+
"rdl_src/udp_builtin.rdl", # uses builtins
24+
}
25+
26+
# Build testcase list
27+
cases = []
28+
for file in rdl_src_files:
29+
if file in exclude:
30+
continue
31+
args = {
32+
"name": os.path.basename(file),
33+
"src": file,
34+
}
35+
cases.append(args)
36+
37+
@pytest.mark.skipif(
38+
sys.version_info < (3, 11),
39+
reason="requires python3.11 or newer for proper type introspection"
40+
)
41+
@parameterized_class(cases)
42+
class TestTypeHints(RDLSourceTestCase):
43+
def check_node_properties(self, node):
44+
"""
45+
Check that each of the node's properties returns a value that matches
46+
the expected annotated type hint
47+
"""
48+
gp_overloads = get_overloads(node.get_property)
49+
self.assertGreater(len(gp_overloads), 0)
50+
51+
for gp_overload in gp_overloads:
52+
hints = get_type_hints(gp_overload)
53+
54+
# Skip overloads that use default override signature:
55+
# get_property(*, default=...)
56+
if "default" in hints:
57+
continue
58+
59+
# Skip generic overloads that do not specify an explicit property
60+
if not hint_is(hints["prop_name"], Literal):
61+
continue
62+
63+
# Currently assuming only one arg to Literal
64+
self.assertEqual(len(hints["prop_name"].__args__), 1)
65+
property_name = hints["prop_name"].__args__[0]
66+
#print(f"Checking {node.get_path()}->{property_name}")
67+
68+
value = node.get_property(property_name)
69+
self.assertTrue(
70+
value_is_compatible(value, hints["return"]),
71+
f"Value '{value}' does not match expected type: {hints['return']}. "
72+
f"for: {node.get_path()}->{property_name}"
73+
)
74+
75+
def assert_attr_type_hint(self, node, attr_name, hint):
76+
"""
77+
Assert a node's attribute matches the expected type hint
78+
"""
79+
value = getattr(node, attr_name)
80+
self.assertTrue(
81+
value_is_compatible(value, hint),
82+
f"Value '{value}' does not match expected type: {hint}."
83+
f"for: {node.get_path()}::{attr_name}"
84+
)
85+
86+
def test_all_nodes(self):
87+
root = self.compile(
88+
[self.src],
89+
incl_search_paths=["rdl_src/incdir"]
90+
)
91+
92+
walker = RDLWalker(skip_not_present=False)
93+
listener = RDLTestListener(self)
94+
walker.walk(root, listener)
95+
96+
# Test root itself
97+
self.assertIsNone(root.parent)
98+
self.assert_attr_type_hint(root, "inst", comp.Root)
99+
self.assert_attr_type_hint(root, "inst_name", str)
100+
self.assert_attr_type_hint(root, "type_name", Optional[str])
101+
self.assert_attr_type_hint(root, "orig_type_name", Optional[str])
102+
self.assert_attr_type_hint(root, "external", bool)
103+
104+
class RDLTestListener(RDLListener):
105+
def __init__(self, test_class: TestTypeHints) -> None:
106+
super().__init__()
107+
self.test_class = test_class
108+
109+
def enter_Component(self, node: Node) -> None:
110+
self.test_class.check_node_properties(node)
111+
self.test_class.assert_attr_type_hint(node, "owning_addrmap", Optional[AddrmapNode])
112+
self.test_class.assert_attr_type_hint(node, "inst_name", str)
113+
self.test_class.assert_attr_type_hint(node, "type_name", Optional[str])
114+
self.test_class.assert_attr_type_hint(node, "orig_type_name", Optional[str])
115+
self.test_class.assert_attr_type_hint(node, "external", bool)
116+
117+
def enter_AddressableComponent(self, node: AddressableNode) -> None:
118+
self.test_class.assert_attr_type_hint(node, "raw_address_offset", int)
119+
self.test_class.assert_attr_type_hint(node, "raw_absolute_address", int)
120+
self.test_class.assert_attr_type_hint(node, "size", int)
121+
self.test_class.assert_attr_type_hint(node, "total_size", int)
122+
if node.is_array:
123+
self.test_class.assert_attr_type_hint(node, "array_dimensions", List[int])
124+
self.test_class.assert_attr_type_hint(node, "array_stride", int)
125+
else:
126+
self.test_class.assertIsNone(node.array_dimensions)
127+
self.test_class.assertIsNone(node.array_stride)
128+
129+
130+
def enter_VectorComponent(self, node: VectorNode) -> None:
131+
self.test_class.assert_attr_type_hint(node, "width", int)
132+
self.test_class.assert_attr_type_hint(node, "msb", int)
133+
self.test_class.assert_attr_type_hint(node, "lsb", int)
134+
self.test_class.assert_attr_type_hint(node, "high", int)
135+
self.test_class.assert_attr_type_hint(node, "low", int)
136+
137+
def enter_Signal(self, node: SignalNode) -> None:
138+
self.test_class.assert_attr_type_hint(node, "parent", Node)
139+
self.test_class.assert_attr_type_hint(node, "inst", comp.Signal)
140+
141+
def enter_Field(self, node: FieldNode) -> None:
142+
self.test_class.assert_attr_type_hint(node, "parent", RegNode)
143+
self.test_class.assert_attr_type_hint(node, "inst", comp.Field)
144+
self.test_class.assert_attr_type_hint(node, "is_virtual", bool)
145+
self.test_class.assert_attr_type_hint(node, "is_volatile", bool)
146+
self.test_class.assert_attr_type_hint(node, "is_sw_writable", bool)
147+
self.test_class.assert_attr_type_hint(node, "is_sw_readable", bool)
148+
self.test_class.assert_attr_type_hint(node, "is_hw_writable", bool)
149+
self.test_class.assert_attr_type_hint(node, "is_hw_readable", bool)
150+
self.test_class.assert_attr_type_hint(node, "implements_storage", bool)
151+
self.test_class.assert_attr_type_hint(node, "is_up_counter", bool)
152+
self.test_class.assert_attr_type_hint(node, "is_down_counter", bool)
153+
self.test_class.assert_attr_type_hint(node, "is_alias", bool)
154+
self.test_class.assert_attr_type_hint(node, "has_aliases", bool)
155+
156+
def enter_Reg(self, node: RegNode) -> None:
157+
self.test_class.assert_attr_type_hint(node, "parent", Union[AddrmapNode, RegfileNode, MemNode])
158+
self.test_class.assert_attr_type_hint(node, "inst", comp.Reg)
159+
self.test_class.assert_attr_type_hint(node, "size", int)
160+
self.test_class.assert_attr_type_hint(node, "is_virtual", bool)
161+
self.test_class.assert_attr_type_hint(node, "has_sw_writable", bool)
162+
self.test_class.assert_attr_type_hint(node, "has_sw_readable", bool)
163+
self.test_class.assert_attr_type_hint(node, "has_hw_writable", bool)
164+
self.test_class.assert_attr_type_hint(node, "has_hw_readable", bool)
165+
self.test_class.assert_attr_type_hint(node, "is_interrupt_reg", bool)
166+
self.test_class.assert_attr_type_hint(node, "is_halt_reg", bool)
167+
self.test_class.assert_attr_type_hint(node, "is_alias", bool)
168+
self.test_class.assert_attr_type_hint(node, "has_aliases", bool)
169+
170+
def enter_Regfile(self, node: RegfileNode) -> None:
171+
self.test_class.assert_attr_type_hint(node, "parent", Union[AddrmapNode, RegfileNode])
172+
self.test_class.assert_attr_type_hint(node, "inst", comp.Regfile)
173+
self.test_class.assert_attr_type_hint(node, "size", int)
174+
175+
def enter_Addrmap(self, node: AddrmapNode) -> None:
176+
self.test_class.assert_attr_type_hint(node, "parent", Union[AddrmapNode, RootNode])
177+
self.test_class.assert_attr_type_hint(node, "inst", comp.Addrmap)
178+
self.test_class.assert_attr_type_hint(node, "size", int)
179+
180+
def enter_Mem(self, node: MemNode) -> None:
181+
self.test_class.assert_attr_type_hint(node, "parent", AddrmapNode)
182+
self.test_class.assert_attr_type_hint(node, "inst", comp.Mem)
183+
self.test_class.assert_attr_type_hint(node, "size", int)
184+
self.test_class.assert_attr_type_hint(node, "is_sw_writable", bool)
185+
self.test_class.assert_attr_type_hint(node, "is_sw_readable", bool)

0 commit comments

Comments
 (0)