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 d2b7364529..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()) @@ -122,6 +128,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, top) cells = mod_info.get("cells", {}) mem_cells = [ n for n, c in cells.items() if c.get("type", "") in ("$mem_v2", "$mem") @@ -289,4 +302,56 @@ def scan_yosys_json(data: dict | str | Path) -> list[schema.Memory]: ) out.append(mem) - return out + return dedupe(out) + + +def definition_name(module: str, top: str | None) -> str: + """`array_64x114$top.u0` -> `array_64x114` for top module `top`. + + 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. + """ + if top: + i = module.find(f"${top}.") + if i > 0: + return module[:i] + 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(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..c1f70e0ef7 100644 --- a/flow/scripts/memories/detect_test.py +++ b/flow/scripts/memories/detect_test.py @@ -157,6 +157,44 @@ 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, 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") + + 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 = { + "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, top="top") + def test_rw_port_pins_are_the_module_ports(self): (m,) = detect.scan_yosys_json( {"modules": {"\\array_64x114": _firtool_rw_module()}} 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, )