From 3c4d1bc0c2732f0bd3551869dc15f816aeacc962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 07:42:52 +0200 Subject: [PATCH 1/2] memories: a slang-uniquified memory module is named for its definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit slang's --keep-hierarchy elaborates one module per instance and names it `$`. The detector named a memory after its module, so a memory module instanced twice produced two entries, two liberty views of one array, and two uniquified names in blackboxes.txt. read_slang's --blackboxed-module matches the definition's name only and ignores a name it does not know, so nothing was blackboxed: every array was elaborated, flattened into its parent, and synthesis stopped at SYNTH_MEMORY_MAX_BITS. asap7/tage_update (#4595) instances each of its SRAM definitions 8 to 16 times and hits this. The detector now cuts a module name at the first `$` when an identifier precedes it (yosys's own `$paramod\...` names have nothing before it and stay whole) and collapses the copies into one entry. Copies that disagree in shape or pins are an error: one liberty view per definition cannot serve a definition parameterised on its memory. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/detect.py | 59 +++++++++++++++++++++++++++- flow/scripts/memories/detect_test.py | 29 ++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/flow/scripts/memories/detect.py b/flow/scripts/memories/detect.py index d2b7364529..33e0a8d20f 100644 --- a/flow/scripts/memories/detect.py +++ b/flow/scripts/memories/detect.py @@ -122,6 +122,13 @@ def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]: for mod_name, mod_info in modules.items(): clean_mod_name = mod_name[1:] if mod_name.startswith("\\") else mod_name + # slang's --keep-hierarchy elaborates one module per instance and + # names it `$`; the flow blackboxes by + # definition (`--blackboxed-module ` is what the + # frontend honours, and the black box it imports has the + # definition's type), so the memory is named for the definition + # and its copies collapse into one entry below. + clean_mod_name = definition_name(clean_mod_name) cells = mod_info.get("cells", {}) mem_cells = [ n for n, c in cells.items() if c.get("type", "") in ("$mem_v2", "$mem") @@ -289,4 +296,54 @@ def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]: ) out.append(mem) - return out + return dedupe(out) + + +def definition_name(module: str) -> str: + """`array_64x114$top.u0` -> `array_64x114`; anything else unchanged. + + Only a slang uniquified name, an identifier before the first `$`, + is cut; yosys's own `$paramod\\...` names have nothing before it and + stay whole. + """ + head, sep, _ = module.partition("$") + if sep and head and (head[0].isalpha() or head[0] == "_"): + return head + return module + + +def dedupe(memories: list[schema.Memory]) -> list[schema.Memory]: + """One entry per name; copies must agree on shape and pins. + + The copies are one definition elaborated per instance, so they agree + unless the definition is parameterised on the memory's shape -- and + then one liberty view per definition cannot serve them, which is an + error rather than a silent pick. + """ + + def shape(m): + return ( + m.rows, + m.bits, + m.read_ports, + m.write_ports, + m.rw_ports, + m.mask_lanes, + m.comb_read_ports, + m.port_convention, + tuple((p.name, p.direction, p.width) for p in m.pins), + ) + + by_name: dict[str, schema.Memory] = {} + for m in memories: + first = by_name.get(m.name) + if first is None: + by_name[m.name] = m + elif shape(first) != shape(m): + raise ValueError( + f"memory {m.name}: two elaborations differ in shape " + f"({first.rows}x{first.bits} against {m.rows}x{m.bits}, or in " + "ports); a definition parameterised on its memory cannot be " + "one macro" + ) + return list(by_name.values()) diff --git a/flow/scripts/memories/detect_test.py b/flow/scripts/memories/detect_test.py index 9965ea9f4f..dd715ef16c 100644 --- a/flow/scripts/memories/detect_test.py +++ b/flow/scripts/memories/detect_test.py @@ -157,6 +157,35 @@ def test_one_memory_per_module_is_named_after_the_module(self): mems = detect.scan_yosys_json(yosys_json) self.assertEqual(sorted(m.name for m in mems), ["array_256x66", "array_64x114"]) + def test_slang_uniquified_copies_collapse_to_the_definition(self): + # slang --keep-hierarchy names a module per instance; the flow + # blackboxes the definition, so that is the memory's name and its + # copies are one entry + yosys_json = { + "modules": { + "\\array_64x114$top.t0.u0": _firtool_rw_module(), + "\\array_64x114$top.t0.u1": _firtool_rw_module(), + "\\array_256x66$top.t1.u0": _firtool_rw_module(256, 66, 1), + } + } + mems = detect.scan_yosys_json(yosys_json) + self.assertEqual(sorted(m.name for m in mems), ["array_256x66", "array_64x114"]) + by = {m.name: m for m in mems} + self.assertEqual(by["array_64x114"].behavioral_model["module"], "array_64x114") + self.assertEqual( + detect.definition_name("$paramod\\foo\\W=8"), "$paramod\\foo\\W=8" + ) + + def test_copies_of_one_definition_must_agree(self): + yosys_json = { + "modules": { + "\\array_64x114$top.t0.u0": _firtool_rw_module(), + "\\array_64x114$top.t0.u1": _firtool_rw_module(64, 114, 1), + } + } + with self.assertRaises(ValueError): + detect.scan_yosys_json(yosys_json) + def test_rw_port_pins_are_the_module_ports(self): (m,) = detect.scan_yosys_json( {"modules": {"\\array_64x114": _firtool_rw_module()}} From 430744ec246376c6dc2105ac2262faa58ad9869d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 10:29:54 +0200 Subject: [PATCH 2/2] memories: cut slang's name where the instance path starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review: cutting at the first `$` breaks a definition whose own name has a `$` (legal in a Verilog identifier): read_slang names an instance of `my$ram` under top `top2` `my$ram$top2.u2`, and the first-`$` cut made it `my`, a module that does not exist. The instance path starts at the top module, so the name is now cut where `$.` begins; gen_memories takes the top as --top, which the Makefile passes as DESIGN_NAME. An escaped definition starting with a digit (`\1d_ram`, which read_slang blackboxes as `1d_ram`) is cut the same way. Copies of one definition are compared on their whole pins, port and function included, not on name, direction and width only. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/Makefile | 1 + flow/scripts/memories/detect.py | 32 +++++++++++++++++---------- flow/scripts/memories/detect_test.py | 19 +++++++++++----- flow/scripts/memories/gen_memories.py | 9 +++++++- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/flow/Makefile b/flow/Makefile index 212474ba84..ce6a8ef3c9 100644 --- a/flow/Makefile +++ b/flow/Makefile @@ -274,6 +274,7 @@ do-auto-memories: $(RESULTS_DIR)/memories_inferred.json mkdir -p $(RESULTS_DIR) $(PYTHON_EXE) $(SCRIPTS_DIR)/memories/gen_memories.py \ --platform $(PLATFORM) \ + --top $(DESIGN_NAME) \ --yosys-json $(RESULTS_DIR)/memories_inferred.json \ --out-dir $(RESULTS_DIR)/memories \ --json $(RESULTS_DIR)/memories.json \ diff --git a/flow/scripts/memories/detect.py b/flow/scripts/memories/detect.py index 33e0a8d20f..2343350429 100644 --- a/flow/scripts/memories/detect.py +++ b/flow/scripts/memories/detect.py @@ -112,8 +112,14 @@ def _clock_enable_bits(val, ports: int) -> str: return val -def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]: - """Extract memory modules from a Yosys netlist JSON output ($mem_v2 cells).""" +def scan_yosys_json( + data: dict | str | Path, top: str | None = None +) -> list[schema.Memory]: + """Extract memory modules from a Yosys netlist JSON output ($mem_v2 cells). + + `top` is the design's top module; slang's per-instance module names + are cut back to their definition with it (see definition_name). + """ if isinstance(data, (str, Path)): data = json.loads(Path(data).read_text()) @@ -128,7 +134,7 @@ def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]: # frontend honours, and the black box it imports has the # definition's type), so the memory is named for the definition # and its copies collapse into one entry below. - clean_mod_name = definition_name(clean_mod_name) + clean_mod_name = definition_name(clean_mod_name, top) cells = mod_info.get("cells", {}) mem_cells = [ n for n, c in cells.items() if c.get("type", "") in ("$mem_v2", "$mem") @@ -299,16 +305,18 @@ def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]: return dedupe(out) -def definition_name(module: str) -> str: - """`array_64x114$top.u0` -> `array_64x114`; anything else unchanged. +def definition_name(module: str, top: str | None) -> str: + """`array_64x114$top.u0` -> `array_64x114` for top module `top`. - Only a slang uniquified name, an identifier before the first `$`, - is cut; yosys's own `$paramod\\...` names have nothing before it and - stay whole. + slang's instance path starts at the top module, so the name is cut + where `$.` begins, not at the first `$`: a definition may have a + `$` of its own (`my$ram$top.u0` is `my$ram`). Any other name, yosys's + own `$paramod\\...` names among them, is returned unchanged. """ - head, sep, _ = module.partition("$") - if sep and head and (head[0].isalpha() or head[0] == "_"): - return head + if top: + i = module.find(f"${top}.") + if i > 0: + return module[:i] return module @@ -331,7 +339,7 @@ def shape(m): m.mask_lanes, m.comb_read_ports, m.port_convention, - tuple((p.name, p.direction, p.width) for p in m.pins), + tuple(m.pins), ) by_name: dict[str, schema.Memory] = {} diff --git a/flow/scripts/memories/detect_test.py b/flow/scripts/memories/detect_test.py index dd715ef16c..c1f70e0ef7 100644 --- a/flow/scripts/memories/detect_test.py +++ b/flow/scripts/memories/detect_test.py @@ -168,13 +168,22 @@ def test_slang_uniquified_copies_collapse_to_the_definition(self): "\\array_256x66$top.t1.u0": _firtool_rw_module(256, 66, 1), } } - mems = detect.scan_yosys_json(yosys_json) + mems = detect.scan_yosys_json(yosys_json, top="top") self.assertEqual(sorted(m.name for m in mems), ["array_256x66", "array_64x114"]) by = {m.name: m for m in mems} self.assertEqual(by["array_64x114"].behavioral_model["module"], "array_64x114") - self.assertEqual( - detect.definition_name("$paramod\\foo\\W=8"), "$paramod\\foo\\W=8" - ) + + def test_definition_name_is_cut_where_the_instance_path_starts(self): + name = detect.definition_name + # read_slang names modules this way: a definition with a `$` of its + # own, an escaped one starting with a digit, a single instance + self.assertEqual(name("my$ram$top2.u2", "top2"), "my$ram") + self.assertEqual(name("1d_ram$top2.u0", "top2"), "1d_ram") + self.assertEqual(name("ram$top.u0", "top"), "ram") + # not a slang instance name: unchanged + self.assertEqual(name("$paramod\\foo\\W=8", "top"), "$paramod\\foo\\W=8") + self.assertEqual(name("my$ram", "top"), "my$ram") + self.assertEqual(name("ram$top.u0", None), "ram$top.u0") def test_copies_of_one_definition_must_agree(self): yosys_json = { @@ -184,7 +193,7 @@ def test_copies_of_one_definition_must_agree(self): } } with self.assertRaises(ValueError): - detect.scan_yosys_json(yosys_json) + detect.scan_yosys_json(yosys_json, top="top") def test_rw_port_pins_are_the_module_ports(self): (m,) = detect.scan_yosys_json( diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py index 9d179d7908..476a9d32fc 100644 --- a/flow/scripts/memories/gen_memories.py +++ b/flow/scripts/memories/gen_memories.py @@ -40,12 +40,13 @@ def run( platform: str, out_dir: Path, json_path: Path, + top: str | None = None, ) -> int: if platform != "asap7": sys.stderr.write(f"gen_memories: unsupported platform {platform}\n") return 1 - found = detect.scan_yosys_json(yosys_json) + found = detect.scan_yosys_json(yosys_json, top) idiomatic.apply(found) @@ -114,6 +115,11 @@ def main(argv: list[str] | None = None) -> int: type=Path, help="Path of the memories.json inventory to write.", ) + p.add_argument( + "--top", + help="The design's top module, which slang's per-instance module " + "names carry.", + ) args = p.parse_args(argv) return run( args.yosys_json, @@ -121,6 +127,7 @@ def main(argv: list[str] | None = None) -> int: args.platform, args.out_dir, args.json, + args.top, )