From 7e4dd4ffdca76bd233b2d91b284e776aa93d9468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 6 Oct 2026 18:06:01 +0200 Subject: [PATCH 1/6] AUTO_MEMORIES: a macro's blackbox costs its generated view's area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With SYNTH_MINIMUM_KEEP_SIZE, synthesis decides which modules to keep with keep_hierarchy -min_cost, which needs a cost for every instanced blackbox. convert_liberty_areas gives Liberty cells one, as a gate equivalent of their area, but an AUTO_MEMORIES macro is a blackbox made from the RTL's own module and has none: any AUTO_MEMORIES design with a keep threshold stopped with "Missing cost information on instanced blackbox". asap7/riscv32i is such a design once its register file is a macro (AUTO_MEMORIES_REGFILES). Each AUTO_MEMORIES blackbox now gets the gate equivalent of the area its generated .lib states. The view is not read in the module's place: its pins need not be the module's (FakeRAM names its own). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 53353ee46a..904bcc1824 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -248,4 +248,30 @@ proc convert_liberty_areas { } { set gate_eq [expr int($area / $found_cell_area)] rtlil::set_attr -mod -uint $box gate_cost_equivalent $gate_eq } + + # An AUTO_MEMORIES macro is a blackbox from the RTL's own module, not a + # Liberty cell: it costs what its generated view's area says, or + # keep_hierarchy -min_cost refuses it ("Missing cost information on + # instanced blackbox"). Its view is not read in its place, as its pins + # need not be the module's (FakeRAM uses its own names). + foreach m [auto_memories_blackboxes] { + set lib "$::env(RESULTS_DIR)/memories/$m.lib" + if { ![file exists $lib] } { + error "AUTO_MEMORIES macro $m has no $lib" + } + set fh [open $lib r] + set text [read $fh] + close $fh + # the cell group of and its area; the brace characters spelled as + # hex escapes, which keep the brace count of this body balanced + set pat "cell\\s*\\(\\s*\"?[string map {$ \\$} $m]\"?\\s*\\)\\s*\\\x7b\[^\x7d\]*?area\\s*:\\s*(\[0-9.eE+-\]+)" + if { ![regexp $pat $text -> area] } { + error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" + } + if { [catch {rtlil::set_attr -mod -uint $m gate_cost_equivalent \ + [expr { int($area / $found_cell_area) }]}] } { + # not in the design: removed as unused, or never instantiated + continue + } + } } From 96c50087eacecb06283d9acf6c0609d1e265d758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Thu, 8 Oct 2026 07:52:42 +0200 Subject: [PATCH 2/6] AUTO_MEMORIES: tclfmt the blackbox cost loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 904bcc1824..68a47d23be 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -268,8 +268,12 @@ proc convert_liberty_areas { } { if { ![regexp $pat $text -> area] } { error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" } - if { [catch {rtlil::set_attr -mod -uint $m gate_cost_equivalent \ - [expr { int($area / $found_cell_area) }]}] } { + if { + [catch { + rtlil::set_attr -mod -uint $m gate_cost_equivalent \ + [expr { int($area / $found_cell_area) }] + }] + } { # not in the design: removed as unused, or never instantiated continue } From 1098c5576d8ff95dc60cacca723470dfa91c58eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Thu, 8 Oct 2026 07:52:49 +0200 Subject: [PATCH 3/6] AUTO_MEMORIES: escape the module name in the blackbox area pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only `$` was escaped; yosys and slang module names also carry `\`, `[` and `.`, each a regular-expression metacharacter. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 68a47d23be..a82ae3c985 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -263,8 +263,11 @@ proc convert_liberty_areas { } { set text [read $fh] close $fh # the cell group of and its area; the brace characters spelled as - # hex escapes, which keep the brace count of this body balanced - set pat "cell\\s*\\(\\s*\"?[string map {$ \\$} $m]\"?\\s*\\)\\s*\\\x7b\[^\x7d\]*?area\\s*:\\s*(\[0-9.eE+-\]+)" + # hex escapes, which keep the brace count of this body balanced. The + # module name is escaped whole: yosys and slang names carry `$`, `\`, + # `[` and `.`, each a regular-expression metacharacter. + set name [regsub -all {[][\x7b\x7d()*+?.\\^$|]} $m {\\&}] + set pat "cell\\s*\\(\\s*\"?$name\"?\\s*\\)\\s*\\\x7b\[^\x7d\]*?area\\s*:\\s*(\[0-9.eE+-\]+)" if { ![regexp $pat $text -> area] } { error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" } From 8411e9c426f67e9c634c66737488246825805686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 07:44:30 +0200 Subject: [PATCH 4/6] AUTO_MEMORIES: a listed module present but not blackboxed is an error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cost loop caught every set_attr failure as "not in the design", so a listed module whose name did not match its definition went unnoticed. slang's --keep-hierarchy names each elaborated module `$` and --blackboxed-module matches only the definition, so such a name is in the design with its body elaborated: set_attr succeeded on it and the memory became flip-flops. A listed module absent from the design is skipped (slang imports a blackboxed module only where it is instanced); one present but not a blackbox stops synthesis by name; set_attr is no longer caught. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index a82ae3c985..98bd58368b 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -254,7 +254,18 @@ proc convert_liberty_areas { } { # keep_hierarchy -min_cost refuses it ("Missing cost information on # instanced blackbox"). Its view is not read in its place, as its pins # need not be the module's (FakeRAM uses its own names). + set modules [tee -q -s result.string select -list-mod =*] foreach m [auto_memories_blackboxes] { + if { [lsearch -exact $modules $m] < 0 } { + # slang imports a blackboxed module only where it is instanced + continue + } + if { ![rtlil::has_attr -mod $m blackbox] } { + # e.g. slang's per-instance `$`, which + # --blackboxed-module does not match: the body was elaborated + error "AUTO_MEMORIES macro $m is in the design but was not\ + blackboxed; its name does not match the module's definition" + } set lib "$::env(RESULTS_DIR)/memories/$m.lib" if { ![file exists $lib] } { error "AUTO_MEMORIES macro $m has no $lib" @@ -271,14 +282,7 @@ proc convert_liberty_areas { } { if { ![regexp $pat $text -> area] } { error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" } - if { - [catch { - rtlil::set_attr -mod -uint $m gate_cost_equivalent \ - [expr { int($area / $found_cell_area) }] - }] - } { - # not in the design: removed as unused, or never instantiated - continue - } + rtlil::set_attr -mod -uint $m gate_cost_equivalent \ + [expr { int($area / $found_cell_area) }] } } From a3305f118bfc404097b24ce280968a6bd824d13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 07:45:25 +0200 Subject: [PATCH 5/6] AUTO_MEMORIES: blackboxes.txt carries each macro's area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cost loop read a macro's area out of its .lib with a regular expression that matched only because FakeRAM writes `area :` first in the cell group. FakeRAM knows the area when it writes the view, so it now writes it beside the module name, ` ` in um^2 per line, and synthesis reads it from there. A line of any other shape is an error. gen_memories_test.py checks the area against the one the .lib states. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/AutoMemories.md | 2 +- flow/scripts/memories/gen_memories.py | 5 ++- flow/scripts/memories/gen_memories_test.py | 19 ++++++++- flow/scripts/synth_preamble.tcl | 47 ++++++++++++---------- tools/FakeRAM2.0/orfs_asap7/generate.py | 9 +++-- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/docs/user/AutoMemories.md b/docs/user/AutoMemories.md index 8bc25c1d77..0ae2672e7c 100644 --- a/docs/user/AutoMemories.md +++ b/docs/user/AutoMemories.md @@ -25,7 +25,7 @@ into `$(RESULTS_DIR)/memories_inferred.json`, and | `$(RESULTS_DIR)/memories/.lib` | Generated Liberty view per converted memory. | | `$(RESULTS_DIR)/memories/_pre_layout.lib` | Ideal-clock (zero clock-tree insertion) variant for pre-CTS consumers that select lib files themselves. The Makefile flow uses `.lib` throughout. | | `$(RESULTS_DIR)/memories/.lef` | Abstract LEF per converted memory. | -| `$(RESULTS_DIR)/memories/blackboxes.txt` | Names of the converted modules — what synthesis blackboxes. | +| `$(RESULTS_DIR)/memories/blackboxes.txt` | ` ` per converted module — what synthesis blackboxes, and the area in um² its `.lib` states, which `SYNTH_MINIMUM_KEEP_SIZE` costs it by. | Synthesis (canonicalization) blackboxes the converted modules so the liberty view wins over their behavioral bodies; floorplan through final diff --git a/flow/scripts/memories/gen_memories.py b/flow/scripts/memories/gen_memories.py index 9d179d7908..e6bfd2a2bc 100644 --- a/flow/scripts/memories/gen_memories.py +++ b/flow/scripts/memories/gen_memories.py @@ -10,8 +10,9 @@ /.lib Liberty view per converted memory /_pre_layout.lib ideal-clock variant for pre-CTS consumers /.lef abstract LEF per converted memory - /blackboxes.txt converted module names, one per line — - what synthesis blackboxes + /blackboxes.txt ` ` per line, area in um^2 as + the .lib states it — what synthesis + blackboxes and the cost it gives each Everything downstream consumes these files; nothing else is passed between the generator and the flow. diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index ffd02593d9..74a5929a0f 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import json +import re import sys import tempfile import unittest @@ -118,14 +119,28 @@ def test_flow_contract(self): "mem_128x32_pre_layout.lib", ], ) - self.assertEqual((mems_dir / "blackboxes.txt").read_text(), "mem_128x32\n") + # one ` ` line; the area is the one the .lib states, + # which synthesis costs the blackbox by + [line] = (mems_dir / "blackboxes.txt").read_text().splitlines() + name, area = line.split() + self.assertEqual(name, "mem_128x32") + lib_area = re.search( + r"^\s*area\s*:\s*([0-9.]+)\s*;", + (mems_dir / "mem_128x32.lib").read_text(), + re.M, + ).group(1) + self.assertEqual(float(area), float(lib_area)) + self.assertGreater(float(area), 0) def test_additional_memories_forces_conversion(self): force = Path(self.tmp.name) / "force.memories" force.write_text(FORCE_TAGS) code, d = self.run_generator(memories_files=[force]) self.assertEqual(code, 0) - blackboxes = (d / "memories" / "blackboxes.txt").read_text().split() + blackboxes = [ + line.split()[0] + for line in (d / "memories" / "blackboxes.txt").read_text().splitlines() + ] self.assertEqual(sorted(blackboxes), ["cache_tags", "mem_128x32"]) self.assertTrue((d / "memories" / "cache_tags.lib").exists()) self.assertTrue((d / "memories" / "cache_tags.lef").exists()) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 98bd58368b..06a121c25e 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -44,10 +44,11 @@ proc read_checkpoint { file } { } # AUTO_MEMORIES: memory modules whose generated liberty view replaces -# their behavioral body during synthesis. The list is produced by -# scripts/memories/gen_memories.py before canonicalization; see +# their behavioral body during synthesis, each with that view's area in +# um^2. The list is produced by scripts/memories/gen_memories.py before +# canonicalization, one ` ` per line; see # docs/user/AutoMemories.md. -proc auto_memories_blackboxes { } { +proc auto_memories_areas { } { if { ![env_var_equals AUTO_MEMORIES 1] } { return {} } @@ -59,7 +60,25 @@ proc auto_memories_blackboxes { } { set fh [open $f r] set content [string map {\r ""} [read $fh]] close $fh - return [regexp -all -inline {\S+} $content] + set areas [dict create] + foreach line [split $content "\n"] { + if { [string trim $line] == "" } { + continue + } + set fields [regexp -all -inline {\S+} $line] + if { + [llength $fields] != 2 + || ![string is double -strict [lindex $fields 1]] + } { + error "$f: expected ` `, got `$line`" + } + dict set areas {*}$fields + } + return $areas +} + +proc auto_memories_blackboxes { } { + return [dict keys [auto_memories_areas]] } proc read_design_sources { } { @@ -250,12 +269,12 @@ proc convert_liberty_areas { } { } # An AUTO_MEMORIES macro is a blackbox from the RTL's own module, not a - # Liberty cell: it costs what its generated view's area says, or + # Liberty cell: it costs its generated view's area, or # keep_hierarchy -min_cost refuses it ("Missing cost information on # instanced blackbox"). Its view is not read in its place, as its pins # need not be the module's (FakeRAM uses its own names). set modules [tee -q -s result.string select -list-mod =*] - foreach m [auto_memories_blackboxes] { + dict for {m area} [auto_memories_areas] { if { [lsearch -exact $modules $m] < 0 } { # slang imports a blackboxed module only where it is instanced continue @@ -266,22 +285,6 @@ proc convert_liberty_areas { } { error "AUTO_MEMORIES macro $m is in the design but was not\ blackboxed; its name does not match the module's definition" } - set lib "$::env(RESULTS_DIR)/memories/$m.lib" - if { ![file exists $lib] } { - error "AUTO_MEMORIES macro $m has no $lib" - } - set fh [open $lib r] - set text [read $fh] - close $fh - # the cell group of and its area; the brace characters spelled as - # hex escapes, which keep the brace count of this body balanced. The - # module name is escaped whole: yosys and slang names carry `$`, `\`, - # `[` and `.`, each a regular-expression metacharacter. - set name [regsub -all {[][\x7b\x7d()*+?.\\^$|]} $m {\\&}] - set pat "cell\\s*\\(\\s*\"?$name\"?\\s*\\)\\s*\\\x7b\[^\x7d\]*?area\\s*:\\s*(\[0-9.eE+-\]+)" - if { ![regexp $pat $text -> area] } { - error "AUTO_MEMORIES macro $m: no area for cell $m in $lib" - } rtlil::set_attr -mod -uint $m gate_cost_equivalent \ [expr { int($area / $found_cell_area) }] } diff --git a/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index be30f8f927..42ac55a8a0 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -54,6 +54,7 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): process = Process(ASAP7_PROCESS_CONFIG) timing_data = TimingData(ASAP7_TIMING_CONFIG) + blackboxes = [] for m in converted: name = m["name"] bits = m.get("bits", 32) @@ -82,7 +83,9 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): with open(lef_path, "w") as f: LefExporter(ram).export(f) - (out_dir / "blackboxes.txt").write_text( - "".join(f"{m['name']}\n" for m in converted) - ) + # the area the .lib states, beside the name: synthesis costs the + # blackbox by it + blackboxes.append(f"{name} {ram.physical.get_area(False):.6f}\n") + + (out_dir / "blackboxes.txt").write_text("".join(blackboxes)) return 0 From b59c1d1cfccfa56705b4398c00322d9779c8a446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 10:41:04 +0200 Subject: [PATCH 6/6] AUTO_MEMORIES: an escaped macro module gets its cost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cost loop looked each blackboxes.txt name up in the module list as a Tcl list. yosys lists an escaped module with its leading backslash, `\1d_ram`, which list parsing reads as an escape, and blackboxes.txt names it as read_slang takes it, `1d_ram`; the lookup missed, the module was taken for absent and skipped, and keep_hierarchy -min_cost stopped with "Missing cost information on instanced blackbox \1d_ram". The list is now read one name per line, without the backslash. Checked with read_slang on a design with `ram` and `\1d_ram` blackboxed: before, the error above; now both get their cost and keep_hierarchy -min_cost runs. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_preamble.tcl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 06a121c25e..cfacc639dd 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -273,7 +273,16 @@ proc convert_liberty_areas { } { # keep_hierarchy -min_cost refuses it ("Missing cost information on # instanced blackbox"). Its view is not read in its place, as its pins # need not be the module's (FakeRAM uses its own names). - set modules [tee -q -s result.string select -list-mod =*] + # One name per line, not a Tcl list: yosys lists an escaped module with + # its leading backslash (`\1d_ram`), which list parsing reads as an + # escape, and blackboxes.txt names it as read_slang takes it (`1d_ram`). + set modules {} + foreach line [split [tee -q -s result.string select -list-mod =*] "\n"] { + set line [string trim $line] + if { $line ne "" } { + lappend modules [regsub {^\\} $line {}] + } + } dict for {m area} [auto_memories_areas] { if { [lsearch -exact $modules $m] < 0 } { # slang imports a blackboxed module only where it is instanced