Skip to content

Commit 7da01e5

Browse files
committed
syn: Add combinational mapper unit tests
Assisted-by: Claude (various models) Signed-off-by: Martin Povišer <povik@cutebit.org>
1 parent 67d6bb8 commit 7da01e5

5 files changed

Lines changed: 393 additions & 1 deletion

File tree

src/syn/include/syn/synthesis.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,15 @@ class Synthesis : public sta::dbStaState
117117
// Free-function entry points re-declared here so tests don't need to
118118
// pull in the private flow/ headers. The Synthesis methods of the same
119119
// names forward to these.
120+
void bitblast(Graph& g, bool blast_arith = true);
120121
void mapSequentials(Graph& g,
121122
sta::Network* network,
122123
utl::Logger* logger,
123124
const Synthesis& syn);
125+
void mapCombinationals(Graph& g,
126+
sta::Network* network,
127+
utl::Logger* logger,
128+
const Synthesis& syn);
124129
void abcRoundtrip(Graph& g, const std::string& commands, utl::Logger* logger);
125130
void livenessOpt(Graph& g,
126131
utl::Logger* logger,

src/syn/test/BUILD

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ cc_test(
7676
],
7777
)
7878

79+
cc_test(
80+
name = "cm_test",
81+
srcs = [
82+
"cm_test.cc",
83+
"equivalence_check.h",
84+
],
85+
data = ["cm_test_cells.lib"],
86+
features = ["-layering_check"],
87+
deps = [
88+
"//src/syn",
89+
"//src/syn/src/ir",
90+
"//src/tst",
91+
"@googletest//:gtest",
92+
],
93+
)
94+
7995
cc_test(
8096
name = "abc_test",
8197
srcs = ["abc_test.cc"],

src/syn/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ target_link_libraries(liveness_test GTest::gtest syn_flow syn_ir utl_lib)
3232
gtest_discover_tests(liveness_test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
3333
add_dependencies(build_and_test liveness_test)
3434

35-
foreach(_t abc_test sm_test)
35+
foreach(_t abc_test sm_test cm_test)
3636
add_executable(${_t} ${_t}.cc)
3737
target_link_libraries(${_t} ${_syn_test_libs} syn_lib syn_flow syn_ir tst)
3838
gtest_discover_tests(${_t} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})

src/syn/test/cm_test.cc

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
// Copyright (c) 2026, The OpenROAD Authors
3+
4+
#include <memory>
5+
#include <sstream>
6+
#include <string>
7+
#include <string_view>
8+
9+
#include "equivalence_check.h"
10+
#include "gtest/gtest.h"
11+
#include "syn/ir/Graph.h"
12+
#include "syn/ir/Instance.h"
13+
#include "syn/synthesis.h"
14+
#include "tst/fixture.h"
15+
16+
// ABC ships its own main() that wins over gtest_main when linked in.
17+
// Provide our own to force the gtest entry point.
18+
int main(int argc, char** argv)
19+
{
20+
testing::InitGoogleTest(&argc, argv);
21+
return RUN_ALL_TESTS();
22+
}
23+
24+
namespace syn {
25+
26+
struct CmTestCase
27+
{
28+
std::string_view name;
29+
std::string_view ir;
30+
// Best observed post-mapping area + 20% headroom.
31+
float area_threshold;
32+
};
33+
34+
// Net IDs 0/1/2 are the constant tie-off slots; instance IDs start at %3.
35+
static const CmTestCase kCmTestCases[] = {
36+
{.name = "SingleAnd",
37+
.ir = R"(
38+
%3:1 = input "a"
39+
%4:1 = input "b"
40+
%5:1 = and %3 %4
41+
%6:0 = output "y" %5
42+
)",
43+
.area_threshold = 3.6f},
44+
45+
{.name = "NandFromAndThenNot",
46+
.ir = R"(
47+
%3:1 = input "a"
48+
%4:1 = input "b"
49+
%5:1 = and %3 %4
50+
%6:1 = not %5
51+
%7:0 = output "y" %6
52+
)",
53+
.area_threshold = 2.4f},
54+
55+
{.name = "NorFromOrThenNot",
56+
.ir = R"(
57+
%3:1 = input "a"
58+
%4:1 = input "b"
59+
%5:1 = or %3 %4
60+
%6:1 = not %5
61+
%7:0 = output "y" %6
62+
)",
63+
.area_threshold = 2.4f},
64+
65+
{.name = "Andnot",
66+
.ir = R"(
67+
%3:1 = input "a"
68+
%4:1 = input "b"
69+
%5:1 = andnot %3 %4
70+
%6:0 = output "y" %5
71+
)",
72+
.area_threshold = 3.6f},
73+
74+
{.name = "AoiPattern",
75+
.ir = R"(
76+
%3:1 = input "a"
77+
%4:1 = input "b"
78+
%5:1 = input "c"
79+
%6:1 = or %4 %5
80+
%7:1 = and %3 %6
81+
%8:1 = not %7
82+
%9:0 = output "y" %8
83+
)",
84+
.area_threshold = 4.8f},
85+
86+
{.name = "OrOfAnds",
87+
.ir = R"(
88+
%3:1 = input "a"
89+
%4:1 = input "b"
90+
%5:1 = input "c"
91+
%6:1 = input "d"
92+
%7:1 = and %3 %4
93+
%8:1 = and %5 %6
94+
%9:1 = or %7 %8
95+
%10:0 = output "y" %9
96+
)",
97+
.area_threshold = 7.2f},
98+
99+
{.name = "SharedFanin",
100+
.ir = R"(
101+
%3:1 = input "a"
102+
%4:1 = input "b"
103+
%5:1 = input "c"
104+
%6:1 = and %3 %4
105+
%7:1 = and %3 %5
106+
%8:0 = output "y1" %6
107+
%9:0 = output "y2" %7
108+
)",
109+
.area_threshold = 7.2f},
110+
111+
{.name = "FourInputAndTree",
112+
.ir = R"(
113+
%3:1 = input "a"
114+
%4:1 = input "b"
115+
%5:1 = input "c"
116+
%6:1 = input "d"
117+
%7:1 = and %3 %4
118+
%8:1 = and %5 %6
119+
%9:1 = and %7 %8
120+
%10:0 = output "y" %9
121+
)",
122+
.area_threshold = 7.2f},
123+
124+
// y = (a & !s) | (b & s)
125+
{.name = "MuxFromAig",
126+
.ir = R"(
127+
%3:1 = input "a"
128+
%4:1 = input "b"
129+
%5:1 = input "s"
130+
%6:1 = andnot %3 %5
131+
%7:1 = and %4 %5
132+
%8:1 = or %6 %7
133+
%9:0 = output "y" %8
134+
)",
135+
.area_threshold = 8.4f},
136+
137+
{.name = "ConstantOutput",
138+
.ir = R"(
139+
%3:0 = output "y0" 0
140+
%4:0 = output "y1" 1
141+
)",
142+
.area_threshold = 2.4f},
143+
144+
{.name = "InverterOnly",
145+
.ir = R"(
146+
%3:1 = input "a"
147+
%4:1 = not %3
148+
%5:0 = output "y" %4
149+
)",
150+
.area_threshold = 1.2f},
151+
152+
// 12-bit adder: bitblast lowers `adc` to Han-Carlson AIG. Output is
153+
// the 12-bit sum, dropping the carry-out at bit 12.
154+
{.name = "Adder12",
155+
.ir = R"(
156+
%3:12 = input "a"
157+
%15:12 = input "b"
158+
%27:13 = adc %3:12 %15:12 0
159+
%40:0 = output "y" %27+0:12
160+
)",
161+
.area_threshold = 280.8f},
162+
163+
// Truncated 6x6 unsigned multiplier (syn::Mul outputWidth = a.width()).
164+
// bitblast lowers it to a Wallace tree feeding a Han-Carlson adder.
165+
{.name = "Multiplier6",
166+
.ir = R"(
167+
%3:6 = input "a"
168+
%9:6 = input "b"
169+
%15:6 = mul %3:6 %9:6
170+
%21:0 = output "y" %15:6
171+
)",
172+
.area_threshold = 265.2f},
173+
};
174+
175+
class CmTest : public tst::Fixture,
176+
public ::testing::WithParamInterface<CmTestCase>
177+
{
178+
protected:
179+
void SetUp() override
180+
{
181+
Fixture::SetUp();
182+
readLiberty(getFilePath("_main/src/syn/test/cm_test_cells.lib"));
183+
synthesis_ = std::make_unique<Synthesis>(getDb(),
184+
getSta(),
185+
/*resizer=*/nullptr,
186+
getLogger());
187+
}
188+
189+
std::unique_ptr<Synthesis> synthesis_;
190+
};
191+
192+
TEST_P(CmTest, MapsBelowAreaAndIsEquivalent)
193+
{
194+
const CmTestCase& tc = GetParam();
195+
196+
// bitblast lowers xor/adc/mul/… to AIG; mapper only handles And/Andnot/Or.
197+
std::istringstream gold_is{std::string(tc.ir)};
198+
std::unique_ptr<Graph> gold = Graph::parse(gold_is);
199+
bitblast(*gold);
200+
gold->normalize();
201+
202+
std::istringstream gate_is{std::string(tc.ir)};
203+
std::unique_ptr<Graph> gate = Graph::parse(gate_is);
204+
bitblast(*gate);
205+
206+
mapCombinationals(*gate, getSta()->network(), getLogger(), *synthesis_);
207+
208+
// Surviving instances must be Targets, plus IR scaffolding and the
209+
// naming-only Nots that cm.cc:integrate() leaves behind.
210+
float area = 0.0f;
211+
gate->forEachInstance([&](const Instance* inst) {
212+
if (inst->is<Input>() || inst->is<Output>() || inst->is<Name>()
213+
|| inst->is<Not>() || inst->is<TieLow>() || inst->is<TieHigh>()
214+
|| inst->is<TieX>()) {
215+
return;
216+
}
217+
if (!inst->is<Target>()) {
218+
std::ostringstream os;
219+
gate->dumpInstance(os, inst);
220+
ADD_FAILURE() << "Non-Target instance survived combinational mapping in "
221+
"case "
222+
<< tc.name << ": " << os.str();
223+
return;
224+
}
225+
if (auto* t = inst->try_as<Target>()) {
226+
area += t->cell()->area();
227+
}
228+
});
229+
230+
EXPECT_LE(area, tc.area_threshold)
231+
<< "Mapped area " << area << " exceeds threshold " << tc.area_threshold
232+
<< " (best observed + 20% headroom) for case " << tc.name;
233+
234+
EXPECT_TRUE(equivalenceCheck(
235+
*gold, *gate, /*inputsDefined=*/true, /*allowRefinement=*/false))
236+
<< "Combinational mapping changed the function for case " << tc.name;
237+
}
238+
239+
INSTANTIATE_TEST_SUITE_P(Cm,
240+
CmTest,
241+
::testing::ValuesIn(kCmTestCases),
242+
[](const ::testing::TestParamInfo<CmTestCase>& info) {
243+
return std::string(info.param.name);
244+
});
245+
246+
} // namespace syn

0 commit comments

Comments
 (0)