From cdbad67c64e3ab1db14565a5b8236e05b3754948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 07:46:58 +0200 Subject: [PATCH 1/3] AUTO_MEMORIES: stop when a module to blackbox is not a black box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit read_slang's --blackboxed-module ignores a name it does not know. When a name in blackboxes.txt is not a module definition -- slang names a module per instance, `$`, and a detector that reports those names hands read_slang names it never matches -- the memory's behavioral body is synthesized, and the flow says so only later, as a memory over SYNTH_MEMORY_MAX_BITS with no hint of why. After read_slang, every name in blackboxes.txt must now be a module with the blackbox attribute; synthesis stops otherwise, naming the modules. Tested with a design that instances one memory module twice: the definition name passes, the per-instance names and an unknown name stop. The test is skipped without a yosys that has read_slang. The yosys Verilog frontend is not checked: slang is the supported frontend for AUTO_MEMORIES. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/check_blackboxed.tcl | 24 +++++ flow/scripts/synth_preamble.tcl | 5 ++ flow/test/test_auto_memories_blackboxed.py | 100 +++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 flow/scripts/memories/check_blackboxed.tcl create mode 100644 flow/test/test_auto_memories_blackboxed.py diff --git a/flow/scripts/memories/check_blackboxed.tcl b/flow/scripts/memories/check_blackboxed.tcl new file mode 100644 index 0000000000..fb32514c09 --- /dev/null +++ b/flow/scripts/memories/check_blackboxed.tcl @@ -0,0 +1,24 @@ +# AUTO_MEMORIES: stop when a module synthesis was told to blackbox is not +# a black box of the design. +# +# read_slang's --blackboxed-module ignores a name it does not know. A +# name in blackboxes.txt that is not the module's definition -- a slang +# per-instance name, a typo, a module the sources no longer have -- leaves +# the memory's behavioral body in place, and the flow says so only much +# later, as a memory over SYNTH_MEMORY_MAX_BITS with no hint of why. Run +# after the sources are read; `names` is the blackboxes.txt list. +proc auto_memories_check_blackboxed { names } { + set blackboxed [split [string trim \ + [tee -q -s result.string select -list-mod =A:blackbox]] "\n"] + set missing {} + foreach m $names { + if { [lsearch -exact $blackboxed $m] < 0 } { + lappend missing $m + } + } + if { [llength $missing] > 0 } { + error "AUTO_MEMORIES: not a black box of the design after reading the\ + sources, so its behavioral body would be synthesized:\ + [join $missing {, }]" + } +} diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 53353ee46a..0a8d1fd2f5 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -47,6 +47,8 @@ proc read_checkpoint { file } { # their behavioral body during synthesis. The list is produced by # scripts/memories/gen_memories.py before canonicalization; see # docs/user/AutoMemories.md. +source $::env(SCRIPTS_DIR)/memories/check_blackboxed.tcl + proc auto_memories_blackboxes { } { if { ![env_var_equals AUTO_MEMORIES 1] } { return {} @@ -123,6 +125,7 @@ proc read_design_sources { } { lappend slang_args {*}$::env(SYNTH_SLANG_ARGS) yosys read_slang {*}$slang_args + auto_memories_check_blackboxed [auto_memories_blackboxes] # Workaround for yosys-slang#119 setattr -unset init @@ -173,6 +176,8 @@ proc read_design_sources { } { set auto_blackboxes [auto_memories_blackboxes] if { [llength $auto_blackboxes] > 0 } { hierarchy -top $::env(DESIGN_NAME) + # Not checked with auto_memories_check_blackboxed: slang is the + # supported frontend for AUTO_MEMORIES. foreach m $auto_blackboxes { blackbox $m } diff --git a/flow/test/test_auto_memories_blackboxed.py b/flow/test/test_auto_memories_blackboxed.py new file mode 100644 index 0000000000..697dad7c85 --- /dev/null +++ b/flow/test/test_auto_memories_blackboxed.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""AUTO_MEMORIES stops synthesis when a module it asks read_slang to +blackbox is not a black box of the design. + +read_slang's --blackboxed-module ignores a name it does not know, and +slang names a module per instance, `$`. Only +the definition's name blackboxes the memory; any other name must stop +the flow right after the sources are read, naming the module. + +Skipped when no yosys with read_slang is available: YOSYS_EXE or yosys on +PATH, with the plugin from SLANG_PLUGIN_PATH when read_slang is not built +in. +""" + +import os +import shutil +import subprocess +import tempfile +import unittest + +CHECK_TCL = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "scripts", + "memories", + "check_blackboxed.tcl", +) + +DESIGN = """\ +module ram (input clk, input we, input [5:0] addr, input [15:0] d, + output logic [15:0] q); + logic [15:0] mem [0:63]; + always_ff @(posedge clk) begin + if (we) mem[addr] <= d; + q <= mem[addr]; + end +endmodule +module top (input clk, input we, input [5:0] addr, input [15:0] d, + output [15:0] q0, q1); + ram u0 (.clk, .we, .addr, .d, .q(q0)); + ram u1 (.clk, .we, .addr, .d, .q(q1)); +endmodule +""" + + +def yosys_command(): + yosys = os.environ.get("YOSYS_EXE") or shutil.which("yosys") + if not yosys: + return None + cmd = [yosys, "-q"] + plugin = os.environ.get("SLANG_PLUGIN_PATH") + if plugin: + cmd += ["-m", plugin] + probe = subprocess.run(cmd + ["-p", "help read_slang"], capture_output=True) + return cmd if probe.returncode == 0 else None + + +class TestAutoMemoriesBlackboxed(unittest.TestCase): + def run_check(self, names): + cmd = yosys_command() + if cmd is None: + self.skipTest("no yosys with read_slang") + with tempfile.TemporaryDirectory() as tmp: + design = os.path.join(tmp, "top.sv") + with open(design, "w") as f: + f.write(DESIGN) + braced = [f"{{{n}}}" for n in names] + blackboxed = " ".join(f"--blackboxed-module {n}" for n in braced) + script = os.path.join(tmp, "check.tcl") + with open(script, "w") as f: + f.write( + "yosys -import\n" + f"source {{{CHECK_TCL}}}\n" + f"yosys read_slang --keep-hierarchy --top top {blackboxed}" + f" {{{design}}}\n" + f"auto_memories_check_blackboxed [list {' '.join(braced)}]\n" + ) + result = subprocess.run( + cmd + ["-c", script], capture_output=True, text=True + ) + return result.returncode, result.stdout + result.stderr + + def test_definition_name_is_blackboxed(self): + returncode, output = self.run_check(["ram"]) + self.assertEqual(returncode, 0, output) + + def test_per_instance_name_stops_the_flow(self): + returncode, output = self.run_check(["ram$top.u0", "ram$top.u1"]) + self.assertNotEqual(returncode, 0, output) + self.assertIn("not a black box of the design", output) + self.assertIn("ram$top.u0, ram$top.u1", output) + + def test_unknown_name_stops_the_flow(self): + returncode, output = self.run_check(["no_such_module"]) + self.assertNotEqual(returncode, 0, output) + self.assertIn("no_such_module", output) + + +if __name__ == "__main__": + unittest.main() From 101c1ff6b105054cd499a5d10ab1988697e8d770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 10:30:20 +0200 Subject: [PATCH 2/3] AUTO_MEMORIES: an escaped black box matches its unescaped name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit read_slang blackboxes the escaped module `\1d_ram` given `--blackboxed-module 1d_ram`, and yosys then lists it as `\1d_ram`; the check compared the two as they stood and stopped a design whose memory was blackboxed. The listed names now lose yosys's leading backslash before the comparison. The test design gains an escaped memory module. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/check_blackboxed.tcl | 9 +++++++-- flow/test/test_auto_memories_blackboxed.py | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/flow/scripts/memories/check_blackboxed.tcl b/flow/scripts/memories/check_blackboxed.tcl index fb32514c09..ef1157e7a0 100644 --- a/flow/scripts/memories/check_blackboxed.tcl +++ b/flow/scripts/memories/check_blackboxed.tcl @@ -8,8 +8,13 @@ # later, as a memory over SYNTH_MEMORY_MAX_BITS with no hint of why. Run # after the sources are read; `names` is the blackboxes.txt list. proc auto_memories_check_blackboxed { names } { - set blackboxed [split [string trim \ - [tee -q -s result.string select -list-mod =A:blackbox]] "\n"] + # yosys lists an escaped module with its leading backslash (`\1d_ram`); + # blackboxes.txt names it as read_slang takes it (`1d_ram`). + set blackboxed {} + foreach m [split [string trim \ + [tee -q -s result.string select -list-mod =A:blackbox]] "\n"] { + lappend blackboxed [regsub {^\\} $m {}] + } set missing {} foreach m $names { if { [lsearch -exact $blackboxed $m] < 0 } { diff --git a/flow/test/test_auto_memories_blackboxed.py b/flow/test/test_auto_memories_blackboxed.py index 697dad7c85..9d235e67b0 100644 --- a/flow/test/test_auto_memories_blackboxed.py +++ b/flow/test/test_auto_memories_blackboxed.py @@ -35,10 +35,19 @@ q <= mem[addr]; end endmodule +module \\1d_ram (input clk, input we, input [5:0] addr, input [15:0] d, + output logic [15:0] q); + logic [15:0] mem [0:63]; + always_ff @(posedge clk) begin + if (we) mem[addr] <= d; + q <= mem[addr]; + end +endmodule module top (input clk, input we, input [5:0] addr, input [15:0] d, - output [15:0] q0, q1); + output [15:0] q0, q1, q2); ram u0 (.clk, .we, .addr, .d, .q(q0)); ram u1 (.clk, .we, .addr, .d, .q(q1)); + \\1d_ram u2 (.clk, .we, .addr, .d, .q(q2)); endmodule """ @@ -84,6 +93,11 @@ def test_definition_name_is_blackboxed(self): returncode, output = self.run_check(["ram"]) self.assertEqual(returncode, 0, output) + def test_escaped_definition_name_is_blackboxed(self): + # read_slang takes `\1d_ram` as `1d_ram`; yosys lists it escaped + returncode, output = self.run_check(["ram", "1d_ram"]) + self.assertEqual(returncode, 0, output) + def test_per_instance_name_stops_the_flow(self): returncode, output = self.run_check(["ram$top.u0", "ram$top.u1"]) self.assertNotEqual(returncode, 0, output) From 43f95a263bc73474d4447981619998ad7126d052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 10:36:36 +0200 Subject: [PATCH 3/3] AUTO_MEMORIES: nothing to check without names; trim each listed module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review: return before the select when blackboxes.txt is empty, and trim each line of the module list, skipping empty ones. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/check_blackboxed.tcl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flow/scripts/memories/check_blackboxed.tcl b/flow/scripts/memories/check_blackboxed.tcl index ef1157e7a0..182adb2385 100644 --- a/flow/scripts/memories/check_blackboxed.tcl +++ b/flow/scripts/memories/check_blackboxed.tcl @@ -8,12 +8,17 @@ # later, as a memory over SYNTH_MEMORY_MAX_BITS with no hint of why. Run # after the sources are read; `names` is the blackboxes.txt list. proc auto_memories_check_blackboxed { names } { + if { [llength $names] == 0 } { + return + } # yosys lists an escaped module with its leading backslash (`\1d_ram`); # blackboxes.txt names it as read_slang takes it (`1d_ram`). set blackboxed {} - foreach m [split [string trim \ - [tee -q -s result.string select -list-mod =A:blackbox]] "\n"] { - lappend blackboxed [regsub {^\\} $m {}] + foreach m [split [tee -q -s result.string select -list-mod =A:blackbox] "\n"] { + set m [string trim $m] + if { $m ne "" } { + lappend blackboxed [regsub {^\\} $m {}] + } } set missing {} foreach m $names {