Skip to content

Commit e7b7e76

Browse files
authored
[PyCDE] Auto-name signals from Python variable names (#9856)
Automatically derive signal names from the Python variable they are assigned to. For example, `x = comb.AddOp(a, b)` will auto-name the resulting signal "x" without requiring a manual `.name = "x"` call. A new `debug` parameter on `System()` switches auto-naming from `sv.namehint` attributes to `hw.wire` ops with inner symbols, creating optimization barriers that guarantee named wires appear in the output SystemVerilog. AI-assisted-by: GitHub Copilot (Claude)
1 parent ec39dd5 commit e7b7e76

File tree

14 files changed

+348
-18
lines changed

14 files changed

+348
-18
lines changed

.github/workflows/testPycdeESI.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ jobs:
5252
fetch-depth: 1
5353
submodules: true
5454

55+
- name: Setup venv and install dependencies
56+
run: |
57+
set -o errexit
58+
whoami
59+
apt update
60+
apt install -y python3-venv
61+
python3 -m venv venv
62+
. venv/bin/activate
63+
pip install --upgrade pip
64+
pip install -r frontends/PyCDE/python/requirements.txt
65+
5566
- name: ccache
5667
uses: hendrikmuhs/ccache-action@v1.2
5768
with:
@@ -70,6 +81,7 @@ jobs:
7081
BUILD_SHARED: ${{ matrix.build-shared }}
7182
BUILD_TYPE: ${{ matrix.build-type }}
7283
run: |
84+
. venv/bin/activate
7385
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
7486
mkdir build && cd build
7587
# In order for ccache to be effective, these flags should be kept in sync with nighly.
@@ -95,17 +107,21 @@ jobs:
95107
96108
- name: Test CIRCT
97109
run: |
110+
. venv/bin/activate
98111
ninja -C build check-circt -j$(nproc)
99112
- name: Test PyCDE
100113
run: |
114+
. venv/bin/activate
101115
ninja -C build check-pycde -j$(nproc)
102116
# The PyCDE integration tests exercise the ESI runtime.
103117
- name: Test PyCDE and ESI runtime integration
104118
run: |
119+
. venv/bin/activate
105120
ninja -C build check-pycde-integration -j$(nproc)
106121
107122
- name: Test ESI runtime (pytest)
108123
run: |
124+
. venv/bin/activate
109125
# Install the ESI C++ runtime into the esiaccel Python package tree
110126
# so that cmake-based tests can find headers, libraries, and the
111127
# esiaccelConfig.cmake.

frontends/PyCDE/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ requires = [
55
"wheel",
66
"cmake>=3.23,<4",
77
"ninja>=1.10",
8+
"executing",
89

910
# MLIR build depends.
1011
"numpy",
@@ -42,7 +43,7 @@ name = "pycde"
4243
dynamic = ["version"]
4344
description = "Python CIRCT Design Entry"
4445
authors = [{ name = "John Demme", email = "John.Demme@microsoft.com" }]
45-
dependencies = ['numpy']
46+
dependencies = ['numpy', 'executing']
4647
requires-python = ">=3.10"
4748

4849
[project.urls]
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
numpy
2-
pybind11>=2.9
2+
nanobind>=2.2.0
33
PyYAML
44
cocotb>=1.6.2
55
cocotb-test>=0.2.2
66
jinja2
7+
executing>=2.2.0
8+
psutil

frontends/PyCDE/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ declare_mlir_python_sources(PyCDESources
3434
pycde/instance.py
3535
pycde/seq.py
3636
pycde/signals.py
37+
pycde/tracer.py
3738
pycde/ndarray.py
3839
pycde/esi.py
3940
pycde/fsm.py

frontends/PyCDE/src/pycde/constructs.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .circt import ir
1616
from .circt.support import BackedgeBuilder
1717
from .circt.dialects import msft as raw_msft
18+
from .tracer import get_var_name
1819

1920
import typing
2021
from typing import List, Optional, Union
@@ -77,6 +78,9 @@ def Wire(type: Type, name: str = None):
7778
"""Declare a wire. Used to create backedges. Must assign exactly once. If
7879
'name' is specified, use 'NamedWire' instead."""
7980

81+
if name is None:
82+
name = get_var_name(depth=1, skip_pycde=True)
83+
8084
class WireValue(type._get_value_class(), AssignableSignal):
8185

8286
def __init__(self):
@@ -139,6 +143,9 @@ def Reg(type: Type,
139143
name: str = None) -> Signal:
140144
"""Declare a register. Must assign exactly once."""
141145

146+
if name is None:
147+
name = get_var_name(depth=1, skip_pycde=True)
148+
142149
class RegisterValue(type._get_value_class()):
143150

144151
def assign(self, new_value: Signal):

frontends/PyCDE/src/pycde/signals.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from __future__ import annotations
66

77
from .support import get_user_loc, _obj_to_value_infer_type
8+
from .tracer import get_var_name
89
from .types import (Array, Bit, Bits, Bundle, BundledChannel, Channel,
910
ChannelDirection, ChannelSignaling, Type)
1011

@@ -27,6 +28,29 @@ def _FromCirctValue(value: ir.Value, type: Type | None = None) -> Signal:
2728
return type._get_value_class()(value, type)
2829

2930

31+
def _apply_auto_name(signal: Signal, name: str) -> Signal:
32+
"""Apply an auto-derived name to a signal. In normal mode, sets sv.namehint.
33+
In debug mode, wraps the signal with hw.wire with a symbol to create an
34+
optimization barrier that preserves the name in the output Verilog."""
35+
from .system import System
36+
try:
37+
system = System.current()
38+
is_debug = system.debug
39+
except RuntimeError:
40+
is_debug = False
41+
42+
if is_debug:
43+
from .module import _BlockContext
44+
from .circt.dialects import hw as raw_hw
45+
sym_name = _BlockContext.current().uniquify_symbol(name)
46+
inner_sym = raw_hw.InnerSymAttr.get(ir.StringAttr.get(sym_name))
47+
wire_op = raw_hw.WireOp(signal.value, name=name, inner_sym=inner_sym)
48+
signal.value = wire_op.result
49+
else:
50+
signal.name = name
51+
return signal
52+
53+
3054
class Signal:
3155
"""Root of the PyCDE value (signal, in RTL terms) hierarchy."""
3256

@@ -589,6 +613,9 @@ def __getitem__(self, idx: Union[int, BitVectorSignal]) -> Signal:
589613
v = hw.ArrayGetOp(self.value, idx)
590614
if self.name and isinstance(idx, int):
591615
v.name = self.name + f"__{idx}"
616+
var_name = get_var_name(depth=2)
617+
if var_name is not None:
618+
_apply_auto_name(v, var_name)
592619
return v
593620

594621
@__getitem__.register(slice)
@@ -606,6 +633,9 @@ def __get_item__slice(self, s: slice):
606633
ret = hw.ArraySliceOp(self.value, idxs[0], ret_type)
607634
if self.name is not None:
608635
ret.name = f"{self.name}_{idxs[0]}upto{idxs[1]}"
636+
var_name = get_var_name(depth=2)
637+
if var_name is not None:
638+
_apply_auto_name(ret, var_name)
609639
return ret
610640

611641
def slice(self, low_idx: Union[int, BitVectorSignal],
@@ -701,6 +731,9 @@ def __getattr__(self, attr):
701731
v = hw.StructExtractOp(self.value, attr)
702732
if self.name:
703733
v.name = f"{self.name}__{attr}"
734+
var_name = get_var_name(depth=1)
735+
if var_name is not None:
736+
_apply_auto_name(v, var_name)
704737
return v
705738
raise AttributeError(f"{type(self)} object has no attribute '{attr}'")
706739

@@ -1222,8 +1255,14 @@ def to_circt(arg):
12221255
# Return the wrapped values, if any.
12231256
converted_results = tuple(
12241257
_FromCirctValue(res) for res in created.results)
1225-
return converted_results[0] if len(
1226-
converted_results) == 1 else converted_results
1258+
if len(converted_results) == 1:
1259+
signal = converted_results[0]
1260+
if signal.name is None:
1261+
var_name = get_var_name(depth=1, skip_pycde=True)
1262+
if var_name is not None:
1263+
_apply_auto_name(signal, var_name)
1264+
return signal
1265+
return converted_results
12271266

12281267
return create
12291268

frontends/PyCDE/src/pycde/system.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,19 @@ class System:
4242
"mod", "top_modules", "name", "passed", "_old_system_token", "_op_cache",
4343
"_generate_queue", "output_directory", "files", "mod_files",
4444
"packaging_funcs", "sw_api_langs", "_instance_roots", "_placedb",
45-
"_appid_index", "platform", "core_freq", "plugin_added_passes"
45+
"_appid_index", "platform", "core_freq", "plugin_added_passes", "_debug"
4646
]
4747

4848
def __init__(self,
4949
top_modules: Union[List[Type[Module]], Type[Module]],
5050
name: str = None,
5151
output_directory: str = None,
5252
sw_api_langs: List[str] = None,
53-
core_clock_frequency_hz: Optional[int] = None):
53+
core_clock_frequency_hz: Optional[int] = None,
54+
debug: bool = False):
5455
from .module import Module
5556
self.passed = False
57+
self._debug = debug
5658
self.mod = ir.Module.create()
5759
if isinstance(top_modules, Iterable):
5860
self.top_modules = list(top_modules)
@@ -119,6 +121,13 @@ def _get_ip(self):
119121
def set_debug():
120122
ir._GlobalDebug.flag = True
121123

124+
@property
125+
def debug(self) -> bool:
126+
"""True when debug mode is enabled — auto-naming uses hw.wire instead of
127+
sv.namehint for better Verilog readability at the cost of some
128+
optimizations."""
129+
return self._debug
130+
122131
# TODO: Ideally, we'd be able to run the cf-to-handshake lowering passes in
123132
# pycde. As of now, however, the cf/memref/arith dialects are not registered
124133
# so the assembly can't be loaded. The right way to do this is to have pycde

frontends/PyCDE/src/pycde/testing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def unittestmodule(generate=True,
2424
print_after_passes=False,
2525
emit_outputs=False,
2626
debug=False,
27+
system_debug=False,
2728
**kwargs):
2829
"""
2930
Like @module, but additionally performs system instantiation, generation,
@@ -46,7 +47,9 @@ def testmodule_inner(func_or_class):
4647
# module generator functions
4748
setattr(builtins, mod.__name__, mod)
4849

49-
sys = System([mod], output_directory=f"out_{func_or_class.__name__}")
50+
sys = System([mod],
51+
output_directory=f"out_{func_or_class.__name__}",
52+
debug=system_debug)
5053
if generate:
5154
sys.generate()
5255
if print:
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2+
# See https://llvm.org/LICENSE.txt for license information.
3+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
5+
import ast
6+
import sys
7+
8+
import executing
9+
10+
# PyCDE-internal modules where auto-naming from variable names IS desired.
11+
# Code in any other pycde.* module is considered "internal plumbing" and will
12+
# be skipped when skip_pycde is True.
13+
_PYCDE_AUTONAME_MODULES = frozenset((
14+
"pycde.constructs",
15+
"pycde.bsp",
16+
"pycde.bsp.common",
17+
"pycde.bsp.cosim",
18+
"pycde.bsp.dma",
19+
"pycde.bsp.xrt",
20+
))
21+
22+
23+
def _is_pycde_internal(module_name: str) -> bool:
24+
"""Return True if module_name belongs to pycde but is NOT on the allowlist."""
25+
if module_name == 'pycde' or module_name.startswith('pycde.'):
26+
return module_name not in _PYCDE_AUTONAME_MODULES
27+
return False
28+
29+
30+
def _name_from_target(target: ast.AST):
31+
"""Extract a name string from an AST assignment target node.
32+
33+
Returns a name for simple variable assignments and subscript assignments
34+
with constant or simple-name keys. Returns None for attribute stores,
35+
starred targets, tuple unpacking, or anything else we can't map to a
36+
simple name."""
37+
if isinstance(target, ast.Name):
38+
return target.id if target.id != "_" else None
39+
40+
if isinstance(target, ast.Subscript) and isinstance(target.value, ast.Name):
41+
container = target.value.id
42+
s = target.slice
43+
if isinstance(s, ast.Constant) and isinstance(s.value, (int, str)):
44+
return f"{container}_{s.value}"
45+
if isinstance(s, ast.Name):
46+
return f"{container}_{s.id}"
47+
48+
return None
49+
50+
51+
def get_var_name(depth=1, skip_pycde=False):
52+
"""Determine whether the result of the current call is being assigned to a
53+
simple variable. Returns the variable name as a string, or None.
54+
55+
Uses the ``executing`` library to identify the currently executing AST
56+
node and inspects the enclosing statement for an assignment target.
57+
58+
If skip_pycde is True, returns None when the target frame belongs to pycde
59+
internal code (but NOT for modules on the allowlist like pycde.constructs
60+
and pycde.bsp.*).
61+
62+
Silently returns None on any failure."""
63+
try:
64+
frame = sys._getframe(depth + 1)
65+
66+
if skip_pycde:
67+
module_name = frame.f_globals.get('__name__', '')
68+
if _is_pycde_internal(module_name):
69+
return None
70+
71+
ex = executing.Source.executing(frame)
72+
node = ex.node
73+
if node is None:
74+
return None
75+
for stmt in ex.statements:
76+
if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1:
77+
# Only return a name if the executing node is the direct RHS of the
78+
# assignment — not a sub-expression within a larger expression.
79+
if stmt.value is node:
80+
return _name_from_target(stmt.targets[0])
81+
return None
82+
except Exception:
83+
return None

frontends/PyCDE/test/test_constructs.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
# CHECK: %in = sv.wire sym @in : !hw.inout<i8>
1818
# CHECK: {{%.+}} = sv.read_inout %in {sv.namehint = "in"} : !hw.inout<i8>
1919
# CHECK: sv.assign %in, %In : i8
20-
# CHECK: [[r1:%.+]] = seq.compreg %In, %clk : i8
20+
# CHECK: %r1__reg1 = seq.compreg sym @r1__reg1 %In, %clk : i8
2121
# CHECK: %c0_i8{{.*}} = hw.constant 0 : i8
22-
# CHECK: [[r5:%.+]] = seq.compreg %In, %clk reset %rst, %c0_i8{{.*}} : i8
23-
# CHECK: [[r6:%.+]] = seq.compreg.ce %In, %clk, %InCE : i8
24-
# CHECK: hw.output [[r2]], [[r1]], [[r5]], [[r6]] : i8, i8, i8, i8
22+
# CHECK: %r_rst__reg1 = seq.compreg sym @r_rst__reg1 %In, %clk reset %rst, %c0_i8{{.*}} : i8
23+
# CHECK: %r_ce__reg1 = seq.compreg.ce sym @r_ce__reg1 %In, %clk, %InCE : i8
24+
# CHECK: hw.output [[r2]], %r1__reg1, %r_rst__reg1, %r_ce__reg1 : i8, i8, i8, i8
2525

2626

2727
@unittestmodule()
@@ -91,12 +91,12 @@ def pe(r, c):
9191

9292

9393
# CHECK-LABEL: hw.module @ControlReg_num_asserts2_num_resets1
94-
# CHECK: [[r0:%.+]] = hw.array_get %asserts[%false]
95-
# CHECK: [[r1:%.+]] = hw.array_get %asserts[%true]
94+
# CHECK: [[r0:%.+]] = hw.array_get %asserts[%false] {sv.namehint = "asserts__0"}
95+
# CHECK: [[r1:%.+]] = hw.array_get %asserts[%true] {sv.namehint = "asserts__1"}
9696
# CHECK: [[r2:%.+]] = comb.or bin [[r0]], [[r1]]
97-
# CHECK: [[r3:%.+]] = hw.array_get %resets[%c0_i0]
97+
# CHECK: [[r3:%.+]] = hw.array_get %resets[%c0_i0] {sv.namehint = "resets__0"}
9898
# CHECK: [[r4:%.+]] = comb.or bin [[r3]]
99-
# CHECK: %state = seq.compreg [[r6]], %clk reset %rst, %false{{.*}}
99+
# CHECK: %state = seq.compreg sym @reg__reg1 {{%.+}}, %clk reset %rst, %false{{.*}}
100100
# CHECK: [[r5:%.+]] = comb.mux bin [[r4]], %false{{.*}}, %state
101101
# CHECK: [[r6:%.+]] = comb.mux bin [[r2]], %true{{.*}}, [[r5]]
102102
# CHECK: hw.output %state

0 commit comments

Comments
 (0)