Repository: cocotb/cocotb

Issue:
Update CoCoTB to allow test to access constants defined in packages
When working with VHDL designs, it's common to have constants defined in packages for use across many source files. At the moment, the only way to access those constants is to create copies in the dut so that cocotb tests can access them. While this work around is functional, it's a bit inelegant. It would be nice to able to get a handle to other objects such as packages. I understand this is possible, but might require modifying the C++ back-end. 



Discussion:
A `packages` attribute could be added to `simulator` as a list of handles to the package instantiations.

For VHPI, here's an example snippet from the language ref manual showing how to get the packages:
```c
/* if not found in the design hierarchy, search for the signal
in the instantiated packages
*/
itr = vhpi_iterator(vhpiPackInsts, NULL);
if (itr) {
  while (hdl = vhpi_scan(itr)) {
    sigHdl = vhpi_handle_by_name(sigName, hdl);
    if (sigHdl) {
      return sigHdl;
    }
  }
}
```

I'm not sure, but my guess for SystemVerilog VPI would be:
```c
iterator = vpi_iterate(vpiPackage, NULL);
```
Hello,

I plan to work on this issue if no one else is doing so already. I propose the following interface:

```python

@test()
def some_test(dut, packages) -> None:
    SOME_CONSTANT = packages["package_name"].SOME_CONSTANT.value

```

The packages parameter can be an optional argument the user can add to their test to retrieve a dictionary with all the packages inside it.
I think making it a function or attribute on the simulator module is probably easier
I was thinking maybe during the `_initialize_testbench` the packages can all be discovered and stored, that way the user wouldn't have to directly use anything in the simulator module. Are you suggesting only have an interface to the packages from the simulator module only?
@andrewandrepowell I agree with Eric. The discovery will have to go through the simulator module anyways. As for pythonland abstraction around that, I'm not a fan of adding it to the test as an optional parameter. In fact I'm for the opposite (#1827). If we want to support alternative test frameworks putting less requirements on it is better.
One more vote for a method on `simulator`. There is already `simulator.get_root_handle`, so probably `simulator.get_packages`.

I think it makes sense to have each package be a `HierarchyObject`, and we should check each GPI `registered_impls` for the mixed-language case.

I'm not sure what `get_packages` should return, though:
1. tuple of package objects
2. "fake" `HierarchyObject` named `packages`, which would allow `packages.pkgName` named attribute access vs iterating over tuple and checking the name
@garmin-mjames, 2 sounds good to me. I don't think we need a `HeirarchyObject`, something simple like `types.SimpleNamespace` would work fine.

If we really wanted to go off the rails here, we could expose this as `import cocotb.vhdl_packages.my_package_name`, and put something in `sys.meta_path` which makes the vhdl modules look like python modules. 
Retuning a `SimpleNamespace` or even just a dictionary of packages sounds good to me.
There is also the problem of GHDL only supporting VPI which does not have a notion of packages. AFAICT, there is no way to inspect packages in GHDL. 
@ktbarrett Yeah it looks like they only support **IEEE 1364-2005** VPI (partially) at the moment.
The SV VPI extension supports `vpiPackage`, but I don't think GHDL supports SV VPI extensions.
GHDL has `Get_Package_Inst`:
https://github.com/ghdl/ghdl/blob/657fcfde5fb93c1311fe5fd2d28146c89852614d/src/grt/grt-avhpi.ads#L427

It might not be too much work to support `vpiPackage` only.
has this finally been implemented?
It has not, and I'm not aware of anyone currently working on it.
I'm interested to this feature too
Also interested.
One way to accomplish this right now is to use the following.

```python
my_pack = cocotb.handle.SimHandle(cocotb.simulator.get_root_handle("my_pack"))
```
@ktbarrett Does that work with VHPI? It separates root instantiation and packages into `vhpi_handle(vhpiRootInst, NULL)` and `vhpi_iterator(vhpiPackInsts, NULL)`
Also desire this feature


Resolving patch:
diff --git a/src/cocotb/__init__.py b/src/cocotb/__init__.py
--- a/src/cocotb/__init__.py
+++ b/src/cocotb/__init__.py
@@ -38,6 +38,7 @@
 import time
 import warnings
 from collections.abc import Coroutine
+from types import SimpleNamespace
 from typing import Dict, List, Optional, Union
 
 import cocotb.handle
@@ -100,6 +101,15 @@ def _setup_logging() -> None:
 This is guaranteed to hold a value at test time.
 """
 
+packages: Optional[SimpleNamespace] = None
+"""A :class:`python:types.SimpleNamespace` of package handles.
+
+This will be populated with handles at test time if packages can be discovered
+via the GPI.
+
+.. versionadded:: 2.0
+"""
+
 LANGUAGE: Optional[str] = os.getenv("TOPLEVEL_LANG")
 """The value of :make:var:`TOPLEVEL_LANG`.
 
@@ -250,6 +260,7 @@ def _initialise_testbench_(argv_):
     # Create the base handle type
 
     _process_plusargs()
+    _process_packages()
 
     # Seed the Python random number generator to make this repeatable
     global RANDOM_SEED
@@ -328,3 +339,29 @@ def _process_plusargs() -> None:
                 plusargs[name] = value
             else:
                 plusargs[option[1:]] = True
+
+
+def _process_packages() -> None:
+    global packages
+
+    pkg_dict = {}
+
+    from cocotb import simulator
+
+    pkgs = simulator.package_iterate()
+    if pkgs is None:
+        packages = SimpleNamespace()
+        return
+
+    for pkg in pkgs:
+        handle = cocotb.handle.SimHandle(pkg)
+        name = handle._name
+
+        # Icarus doesn't support named access to package objects:
+        # https://github.com/steveicarus/iverilog/issues/1038
+        # so we cannot lazily create handles
+        if SIM_NAME == "Icarus Verilog":
+            handle._discover_all()
+        pkg_dict[name] = handle
+
+    packages = SimpleNamespace(**pkg_dict)
diff --git a/src/cocotb/handle.py b/src/cocotb/handle.py
--- a/src/cocotb/handle.py
+++ b/src/cocotb/handle.py
@@ -215,7 +215,8 @@ def _discover_all(self) -> None:
 
     def _child_path(self, name) -> str:
         """Return a string of the path of the child :any:`SimHandle` for a given *name*."""
-        return self._path + "." + name
+        delimiter = "::" if self._type == "GPI_PACKAGE" else "."
+        return self._path + delimiter + name
 
     def _sub_handle_key(self, name):
         """Translate the handle name to a key to use in :any:`_sub_handles` dictionary."""
@@ -864,6 +865,7 @@ def value(self) -> bytes:
     simulator.ENUM: EnumObject,
     simulator.STRING: StringObject,
     simulator.GENARRAY: HierarchyArrayObject,
+    simulator.PACKAGE: HierarchyObject,
 }
 
 


Test patch:
diff --git a/tests/designs/package_module/Makefile b/tests/designs/package_module/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/designs/package_module/Makefile
@@ -0,0 +1,26 @@
+# Copyright cocotb contributors
+# Licensed under the Revised BSD License, see LICENSE for details.
+# SPDX-License-Identifier: BSD-3-Clause
+
+TOPLEVEL_LANG ?= verilog
+
+TOPLEVEL := cocotb_package
+
+ifneq ($(TOPLEVEL_LANG),verilog)
+
+all:
+	@echo "Skipping test due to TOPLEVEL_LANG=$(TOPLEVEL_LANG) not being verilog"
+clean::
+
+else
+
+PWD=$(shell pwd)
+
+COCOTB?=$(PWD)/../../..
+
+VERILOG_SOURCES = $(COCOTB)/tests/designs/package_module/cocotb_package_pkg.sv \
+                  $(COCOTB)/tests/designs/package_module/cocotb_package.sv
+
+include $(shell cocotb-config --makefiles)/Makefile.sim
+
+endif
diff --git a/tests/designs/package_module/cocotb_package.sv b/tests/designs/package_module/cocotb_package.sv
new file mode 100644
--- /dev/null
+++ b/tests/designs/package_module/cocotb_package.sv
@@ -0,0 +1,11 @@
+// Copyright cocotb contributors
+// Licensed under the Revised BSD License, see LICENSE for details.
+// SPDX-License-Identifier: BSD-3-Clause
+
+module cocotb_package;
+    // Necessary for Xcelium and Riviera in order for compiled packages to be visible
+    import cocotb_package_pkg_1::*;
+    import cocotb_package_pkg_2::*;
+
+    parameter int seven_int = 7;
+endmodule
diff --git a/tests/designs/package_module/cocotb_package_pkg.sv b/tests/designs/package_module/cocotb_package_pkg.sv
new file mode 100644
--- /dev/null
+++ b/tests/designs/package_module/cocotb_package_pkg.sv
@@ -0,0 +1,14 @@
+// Copyright cocotb contributors
+// Licensed under the Revised BSD License, see LICENSE for details.
+// SPDX-License-Identifier: BSD-3-Clause
+
+package cocotb_package_pkg_1;
+    parameter int five_int = 5;
+    parameter logic [31:0] eight_logic = 8;
+endpackage
+
+package cocotb_package_pkg_2;
+    parameter int eleven_int = 11;
+endpackage
+
+parameter int unit_four_int = 4;
diff --git a/tests/test_cases/test_package/Makefile b/tests/test_cases/test_package/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/test_cases/test_package/Makefile
@@ -0,0 +1,36 @@
+# Copyright cocotb contributors
+# Licensed under the Revised BSD License, see LICENSE for details.
+# SPDX-License-Identifier: BSD-3-Clause
+
+ifeq ($(SIM),)
+USING_ICARUS := 1
+endif
+
+ifeq ($(shell echo $(SIM) | tr A-Z a-z),icarus)
+USING_ICARUS := 1
+endif
+
+ifneq ($(USING_ICARUS),)
+ICARUS_MIN := 12.0
+ICARUS_VERSION := $(shell iverilog -V 2>/dev/null | head -n1 | cut -d ' ' -f 4)
+MIN_VERSION := $(shell printf "%s\n%s\n" "$(ICARUS_MIN)" "$(ICARUS_VERSION)" | sort -g | head -1)
+ifneq ($(MIN_VERSION),$(ICARUS_MIN))
+SKIP := 1
+endif
+endif
+
+ifeq ($(SKIP),)
+
+include ../../designs/package_module/Makefile
+
+MODULE = test_package
+
+else
+
+all:
+	@echo "Skipping test_array since icarus < v12.0 doesn't support vpiInstance iteration"
+
+clean::
+# nothing to clean, just define target in this branch
+
+endif
diff --git a/tests/test_cases/test_package/test_package.py b/tests/test_cases/test_package/test_package.py
new file mode 100644
--- /dev/null
+++ b/tests/test_cases/test_package/test_package.py
@@ -0,0 +1,60 @@
+# Copyright cocotb contributors
+# Licensed under the Revised BSD License, see LICENSE for details.
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""
+A set of tests that demonstrate package access
+"""
+
+import logging
+
+import cocotb
+from cocotb.result import TestSuccess
+
+
+@cocotb.test()
+async def test_params(dut):
+    """Test package parameter access"""
+    tlog = logging.getLogger("cocotb.test")
+
+    tlog.info("Checking Parameters:")
+    assert dut.seven_int.value == 7
+    pkg1 = cocotb.packages.cocotb_package_pkg_1
+    assert pkg1.five_int.value == 5
+    assert pkg1.eight_logic.value == 8
+    pkg2 = cocotb.packages.cocotb_package_pkg_2
+    assert pkg2.eleven_int.value == 11
+
+
+@cocotb.test()
+async def test_stringification(dut):
+    """Test package stringification"""
+    tlog = logging.getLogger("cocotb.test")
+
+    tlog.info("Checking Strings:")
+    pkg1 = cocotb.packages.cocotb_package_pkg_1
+    assert str(pkg1).startswith("HierarchyObject(cocotb_package_pkg_1")
+    assert str(pkg1.five_int) == "IntegerObject(cocotb_package_pkg_1::five_int)"
+    assert str(pkg1.eight_logic) == "IntegerObject(cocotb_package_pkg_1::eight_logic)"
+    pkg2 = cocotb.packages.cocotb_package_pkg_2
+    assert str(pkg2).startswith("HierarchyObject(cocotb_package_pkg_2")
+    assert str(pkg2.eleven_int) == "IntegerObject(cocotb_package_pkg_2::eleven_int)"
+
+
+@cocotb.test()
+async def test_dollar_unit(dut):
+    """Test $unit scope"""
+    tlog = logging.getLogger("cocotb.test")
+
+    if cocotb.SIM_NAME.lower().startswith("riviera"):
+        tlog.info("Riviera does not support $unit access via vpiInstance")
+        raise TestSuccess
+
+    tlog.info("Checking $unit:")
+    # Is $unit even a package?  Xcelium says yes and 37.10 detail 5 would also suggest yes
+    pkgs = vars(cocotb.packages).keys()
+    f = filter(lambda x: "unit" in x, pkgs)
+    unit = list(f)[0]
+    tlog.info(f"Found $unit as {unit}")
+    unit_pkg = getattr(cocotb.packages, unit)
+    assert unit_pkg.unit_four_int.value == 4