diff --git a/doc/source/coverage.rst b/doc/source/coverage.rst index 1dbd00d..44df822 100644 --- a/doc/source/coverage.rst +++ b/doc/source/coverage.rst @@ -332,6 +332,69 @@ compose the coverpoint cross. self.cp1X2 = vsc.cross([self.cp1, self.cp2]) +Nested Coverpoint Crosses +.......................... + +PyVSC supports nested crosses, where a cross can be defined using previously +defined crosses as target elements. This enables modular coverage definition +and prevents redundant code when building multi-dimensional coverage matrices. + +When a cross is used as a target element in another cross, its component +coverpoints are automatically flattened into the resulting cross. This approach +is similar to SystemVerilog's cross-of-crosses functionality. + +.. code-block:: python3 + + @vsc.covergroup + class nested_cross_cg(object): + def __init__(self): + self.with_sample(dict( + a = vsc.uint8_t(), + b = vsc.uint8_t(), + c = vsc.uint8_t() + )) + + self.cp_a = vsc.coverpoint(self.a, bins={"a_bins": vsc.bin_array([4], [0, 255])}) + self.cp_b = vsc.coverpoint(self.b, bins={"b_bins": vsc.bin_array([4], [0, 255])}) + self.cp_c = vsc.coverpoint(self.c, bins={"c_bins": vsc.bin_array([4], [0, 255])}) + + # Standard cross of two coverpoints (2D) + self.cross_ab = vsc.cross([self.cp_a, self.cp_b]) + + # Nested cross: cross of a cross and a coverpoint (3D) + # Automatically flattened to cross([cp_a, cp_b, cp_c]) + self.cross_abc = vsc.cross([self.cross_ab, self.cp_c]) + +You can also create a cross from multiple crosses: + +.. code-block:: python3 + + @vsc.covergroup + class multi_cross_cg(object): + def __init__(self): + self.with_sample(dict( + a = vsc.uint8_t(), + b = vsc.uint8_t(), + c = vsc.uint8_t(), + d = vsc.uint8_t() + )) + + self.cp_a = vsc.coverpoint(self.a, bins={"a_bins": vsc.bin_array([2], [0, 255])}) + self.cp_b = vsc.coverpoint(self.b, bins={"b_bins": vsc.bin_array([2], [0, 255])}) + self.cp_c = vsc.coverpoint(self.c, bins={"c_bins": vsc.bin_array([2], [0, 255])}) + self.cp_d = vsc.coverpoint(self.d, bins={"d_bins": vsc.bin_array([2], [0, 255])}) + + # Two separate 2D crosses + self.cross_ab = vsc.cross([self.cp_a, self.cp_b]) + self.cross_cd = vsc.cross([self.cp_c, self.cp_d]) + + # Cross of two crosses creates a 4D coverage matrix + # Automatically flattened to cross([cp_a, cp_b, cp_c, cp_d]) + self.cross_abcd = vsc.cross([self.cross_ab, self.cross_cd]) + +Nested crosses can be arbitrarily deep, with each level being automatically +flattened to its constituent coverpoints. + Coverpoint Cross Ignore Bins ............................ diff --git a/src/vsc/coverage.py b/src/vsc/coverage.py index 313d822..58d7c9a 100644 --- a/src/vsc/coverage.py +++ b/src/vsc/coverage.py @@ -905,10 +905,31 @@ def __init__(self, name=None, iff=None, ignore_bins=None): - for t in target_l: - if not isinstance(t, coverpoint): - raise Exception("Cross target \"" + str(t) + "\" is not a coverpoint") - self.target_l = target_l + # Recursively flatten nested crosses by extracting their coverpoints + def flatten_targets(targets): + """Recursively flatten a list of coverpoints and crosses. + + Args: + targets: List of coverpoint and/or cross objects + + Returns: + List of coverpoint objects (flat list with all crosses expanded) + + Raises: + Exception: If any target is neither a coverpoint nor a cross + """ + flattened = [] + for t in targets: + if isinstance(t, cross): + # Recursively flatten cross objects + flattened.extend(flatten_targets(t.target_l)) + elif isinstance(t, coverpoint): + flattened.append(t) + else: + raise Exception("Cross target \"" + str(t) + "\" is not a coverpoint or cross") + return flattened + + self.target_l = flatten_targets(target_l) self.bins = bins self.options = Options() diff --git a/ve/unit/test_coverage_cross.py b/ve/unit/test_coverage_cross.py index 1796635..05d854c 100644 --- a/ve/unit/test_coverage_cross.py +++ b/ve/unit/test_coverage_cross.py @@ -54,3 +54,162 @@ def __init__(self, v1,v2): self.assertEqual(report.covergroups[0].coverpoints[1].coverage, 100) self.assertEqual(report.covergroups[0].crosses[0].coverage, 50) + def test_nested_cross_basic(self): + """Test basic nested cross: cross of (cross + coverpoint)""" + + @vsc.covergroup + class nested_cross_cg(object): + def __init__(self): + self.with_sample(dict( + a = vsc.uint8_t(), + b = vsc.uint8_t(), + c = vsc.uint8_t() + )) + + self.cp_a = vsc.coverpoint(self.a, bins={"a_bins": vsc.bin_array([4], [0, 255])}) + self.cp_b = vsc.coverpoint(self.b, bins={"b_bins": vsc.bin_array([4], [0, 255])}) + self.cp_c = vsc.coverpoint(self.c, bins={"c_bins": vsc.bin_array([4], [0, 255])}) + + # Standard cross + self.cross_ab = vsc.cross([self.cp_a, self.cp_b]) + + # Nested cross: cross of a cross and a coverpoint + # This should create a 3D matrix (A x B x C) + self.cross_abc = vsc.cross([self.cross_ab, self.cp_c]) + + cg = nested_cross_cg() + + # Sample various combinations (positional args match with_sample order) + cg.sample(10, 20, 30) + cg.sample(100, 150, 200) + cg.sample(200, 100, 50) + + # Verify that the nested cross was created correctly + # The cross_abc should have 3 coverpoints (a, b, c) flattened + self.assertEqual(len(cg.cross_abc.target_l), 3) + self.assertEqual(cg.cross_abc.target_l[0], cg.cp_a) + self.assertEqual(cg.cross_abc.target_l[1], cg.cp_b) + self.assertEqual(cg.cross_abc.target_l[2], cg.cp_c) + + def test_nested_cross_two_crosses(self): + """Test cross of two crosses""" + + @vsc.covergroup + class two_cross_cg(object): + def __init__(self): + self.with_sample(dict( + a = vsc.uint8_t(), + b = vsc.uint8_t(), + c = vsc.uint8_t(), + d = vsc.uint8_t() + )) + + self.cp_a = vsc.coverpoint(self.a, bins={"a_bins": vsc.bin_array([2], [0, 255])}) + self.cp_b = vsc.coverpoint(self.b, bins={"b_bins": vsc.bin_array([2], [0, 255])}) + self.cp_c = vsc.coverpoint(self.c, bins={"c_bins": vsc.bin_array([2], [0, 255])}) + self.cp_d = vsc.coverpoint(self.d, bins={"d_bins": vsc.bin_array([2], [0, 255])}) + + # Two crosses + self.cross_ab = vsc.cross([self.cp_a, self.cp_b]) + self.cross_cd = vsc.cross([self.cp_c, self.cp_d]) + + # Cross of crosses: creates 4D matrix (A x B x C x D) + self.cross_abcd = vsc.cross([self.cross_ab, self.cross_cd]) + + cg = two_cross_cg() + + # Sample various combinations + cg.sample(10, 20, 30, 40) + cg.sample(200, 220, 230, 240) + + # Verify that the nested cross flattened correctly + self.assertEqual(len(cg.cross_abcd.target_l), 4) + self.assertEqual(cg.cross_abcd.target_l[0], cg.cp_a) + self.assertEqual(cg.cross_abcd.target_l[1], cg.cp_b) + self.assertEqual(cg.cross_abcd.target_l[2], cg.cp_c) + self.assertEqual(cg.cross_abcd.target_l[3], cg.cp_d) + + def test_nested_cross_deep(self): + """Test deeply nested crosses (3+ levels)""" + + @vsc.covergroup + class deep_cross_cg(object): + def __init__(self): + self.with_sample(dict( + a = vsc.uint8_t(), + b = vsc.uint8_t(), + c = vsc.uint8_t(), + d = vsc.uint8_t() + )) + + self.cp_a = vsc.coverpoint(self.a, bins={"a_bins": vsc.bin_array([2], [0, 255])}) + self.cp_b = vsc.coverpoint(self.b, bins={"b_bins": vsc.bin_array([2], [0, 255])}) + self.cp_c = vsc.coverpoint(self.c, bins={"c_bins": vsc.bin_array([2], [0, 255])}) + self.cp_d = vsc.coverpoint(self.d, bins={"d_bins": vsc.bin_array([2], [0, 255])}) + + # Build nested crosses progressively + self.cross_ab = vsc.cross([self.cp_a, self.cp_b]) + self.cross_abc = vsc.cross([self.cross_ab, self.cp_c]) + self.cross_abcd = vsc.cross([self.cross_abc, self.cp_d]) + + cg = deep_cross_cg() + + # Sample + cg.sample(10, 20, 30, 40) + + # Verify deep flattening + self.assertEqual(len(cg.cross_abcd.target_l), 4) + self.assertEqual(cg.cross_abcd.target_l[0], cg.cp_a) + self.assertEqual(cg.cross_abcd.target_l[1], cg.cp_b) + self.assertEqual(cg.cross_abcd.target_l[2], cg.cp_c) + self.assertEqual(cg.cross_abcd.target_l[3], cg.cp_d) + + def test_nested_cross_with_options(self): + """Test nested cross with options""" + + @vsc.covergroup + class nested_cross_options_cg(object): + def __init__(self): + self.with_sample(dict( + a = vsc.uint8_t(), + b = vsc.uint8_t(), + c = vsc.uint8_t() + )) + + self.cp_a = vsc.coverpoint(self.a, bins={"a_bins": vsc.bin_array([2], [0, 255])}) + self.cp_b = vsc.coverpoint(self.b, bins={"b_bins": vsc.bin_array([2], [0, 255])}) + self.cp_c = vsc.coverpoint(self.c, bins={"c_bins": vsc.bin_array([2], [0, 255])}) + + self.cross_ab = vsc.cross([self.cp_a, self.cp_b]) + + # Nested cross with options + self.cross_abc_nested = vsc.cross( + [self.cross_ab, self.cp_c], + options=dict(at_least=2) + ) + + # Non-nested cross with same options for comparison + self.cross_abc_flat = vsc.cross( + [self.cp_a, self.cp_b, self.cp_c], + options=dict(at_least=2) + ) + + cg = nested_cross_options_cg() + cg.sample(10, 20, 30) + + # Verify structure of nested cross + self.assertEqual(len(cg.cross_abc_nested.target_l), 3) + # Verify options were set correctly on nested cross + self.assertIsNotNone(cg.cross_abc_nested.options) + self.assertEqual(cg.cross_abc_nested.options.at_least, 2) + + # Verify structure of flat cross + self.assertEqual(len(cg.cross_abc_flat.target_l), 3) + # Verify options were set correctly on flat cross + self.assertIsNotNone(cg.cross_abc_flat.options) + self.assertEqual(cg.cross_abc_flat.options.at_least, 2) + + # Verify both crosses have same structure (proving flattening works correctly) + for i in range(3): + self.assertEqual(cg.cross_abc_nested.target_l[i], cg.cross_abc_flat.target_l[i]) +