codecov[bot]: # [Codecov](https://codecov.io/gh/m-labs/nmigen/pull/50?src=pr&el=h1) Report
> Merging [#50](https://codecov.io/gh/m-labs/nmigen/pull/50?src=pr&el=desc) into [master](https://codecov.io/gh/m-labs/nmigen/commit/81ee2db1636a504d2e60fc4649db4afde8b27e4c?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/m-labs/nmigen/pull/50/graphs/tree.svg?width=650&token=gOCJYWfHzR&height=150&src=pr)](https://codecov.io/gh/m-labs/nmigen/pull/50?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master     #50   +/-   ##
======================================
  Coverage    85.5%   85.5%           
======================================
  Files          27      27           
  Lines        3912    3912           
  Branches      764     764           
======================================
  Hits         3345    3345           
  Misses        478     478           
  Partials       89      89
```


| [Impacted Files](https://codecov.io/gh/m-labs/nmigen/pull/50?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [nmigen/hdl/rec.py](https://codecov.io/gh/m-labs/nmigen/pull/50/diff?src=pr&el=tree#diff-bm1pZ2VuL2hkbC9yZWMucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/m-labs/nmigen/pull/50?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/m-labs/nmigen/pull/50?src=pr&el=footer). Last update [81ee2db...a6d531a](https://codecov.io/gh/m-labs/nmigen/pull/50?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/nmigen/hdl/rec.py b/nmigen/hdl/rec.py
index 999c85b..f1ddbd2 100644
--- a/nmigen/hdl/rec.py
+++ b/nmigen/hdl/rec.py
@@ -72,7 +72,7 @@ class Record(Value):
         def concat(a, b):
             if a is None:
                 return b
-            return "{}_{}".format(a, b)
+            return "{}__{}".format(a, b)
 
         self.layout = Layout.wrap(layout)
         self.fields = OrderedDict()

seperator of signal names in records causes ambiguity
When creating a `Record`, the signals inside it are named `recordname_fieldname` currently.

This makes it hard to see the hierarchy when using snake case names for records and fields as well. I.e. having a record named `some_record` with a field named `some_field` the resulting signal is called `some_record_some_field` which could also be the name of a signal in a nested record with depth 3. 

I propose to use another character for separation (for example `.`), which would lead to `some_record.some_field` in that case.

(see: https://github.com/m-labs/nmigen/blob/master/nmigen/hdl/rec.py#L75)
diff --git a/nmigen/test/test_hdl_rec.py b/nmigen/test/test_hdl_rec.py
index 3f68247..65e8bf6 100644
--- a/nmigen/test/test_hdl_rec.py
+++ b/nmigen/test/test_hdl_rec.py
@@ -65,11 +65,11 @@ class RecordTestCase(FHDLTestCase):
             ])
         ])
 
-        self.assertEqual(repr(r), "(rec r stb data (rec r_info a b))")
+        self.assertEqual(repr(r), "(rec r stb data (rec r__info a b))")
         self.assertEqual(len(r),  35)
         self.assertIsInstance(r.stb, Signal)
-        self.assertEqual(r.stb.name, "r_stb")
-        self.assertEqual(r["stb"].name, "r_stb")
+        self.assertEqual(r.stb.name, "r__stb")
+        self.assertEqual(r["stb"].name, "r__stb")
 
     def test_unnamed(self):
         r = [Record([

pip install -e .
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
bitarray==3.3.0
exceptiongroup==1.2.2
iniconfig==2.1.0
-e git+https://github.com/m-labs/nmigen.git@81ee2db1636a504d2e60fc4649db4afde8b27e4c#egg=nmigen
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
pyvcd==0.4.1
tomli==2.2.1

name: nmigen
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - bitarray==3.3.0
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - pyvcd==0.4.1
      - tomli==2.2.1
prefix: /opt/conda/envs/nmigen

nmigen/test/test_hdl_rec.py::RecordTestCase::test_basic
nmigen/test/test_hdl_rec.py::LayoutTestCase::test_fields nmigen/test/test_hdl_rec.py::LayoutTestCase::test_wrong_direction nmigen/test/test_hdl_rec.py::LayoutTestCase::test_wrong_field nmigen/test/test_hdl_rec.py::LayoutTestCase::test_wrong_name nmigen/test/test_hdl_rec.py::LayoutTestCase::test_wrong_name_duplicate nmigen/test/test_hdl_rec.py::LayoutTestCase::test_wrong_shape nmigen/test/test_hdl_rec.py::RecordTestCase::test_unnamed nmigen/test/test_hdl_rec.py::RecordTestCase::test_wrong_field nmigen/test/test_hdl_rec.py::RecordTestCase::test_wrong_field_unnamed
nmigen/hdl/rec.py
nmigen/hdl/rec.py