Redundant logic computes something already known, or something nobody reads. Two gates with identical inputs compute the same value twice. A gate whose output never reaches an output computes nothing at all. And some logic is redundant only in context: it looks useful gate by gate, but no input combination can observe it.
Removal is safe exactly when the visible function never changes, and safe is a claim that needs a proof, not a glance. This lesson runs three redundancies through the same passes. One falls to plain opt. Two survive it and fall only to ABC, which removes them through AIG structural hashing and truth-table rewriting over small cuts. That rewriting is itself local in scope; the real contrast is that Yosys opt_expr carries no absorption or reconvergence rule, while ABC compares the truth tables of small cuts.
Pattern | Rewrite |
|---|---|
OR or AND of a signal with itself | The signal |
OR or AND of a signal with its own inverse |
|
Two gates with identical inputs | One gate driving both loads |
Undriven net, unused gate output | Delete the net and the gate |
The first script builds the easy case: two identical AND gates feeding one OR. After elaboration the recorded netlist holds two AND cells and one OR cell. The script then runs the four small passes one by one, each with its log captured, before the blanket opt. The assertions pin the outcome: exactly one AND cell remains and no OR cell.
read_verilog <<EOV
module dupred(input wire a, input wire b, output wire y);
wire t1 = a & b;
wire t2 = a & b;
assign y = t1 | t2;
endmodule
EOV
hierarchy -check -top dupred
proc
write_json net_dup_proc.json
tee -o log_expr.txt opt_expr
tee -o log_merge.txt opt_merge
tee -o log_muxtree.txt opt_muxtree
tee -o log_clean.txt opt_clean
opt
check
write_json net_dup_opt.json
select -assert-count 1 t:$and
select -assert-none t:$or
log PASS: duplicate gates merge and the self-OR folds awayy = a | (a & b) and y = (a & b) | (a & ~b). Both equal a on all four input pairs. Will plain opt remove either of them, the way it removes the duplicate above?No. The recorded netlists after opt still hold 1 AND and 1 OR for the absorption term and 2 ANDs and 1 OR for the reconvergent one. No rule in opt_expr covers either shape, so opt leaves both netlists untouched; ABC's structural hashing plus truth-table rewriting over small cuts removes them during mapping, and the mapped areas below confirm it.
The table names each pass's job on this exact design; the recorded lines below are the passes reporting back, one action line per pass. Read both in run order.
Pass | What it did on this design |
|---|---|
opt_expr | Ran its folding over the module; nothing to fold here |
opt_merge | United the identical AND pair so the self-OR could fold |
opt_muxtree | Found no muxes; nothing to prune |
opt_clean | Deleted the dangling wires |
recorded action lines, one per pass
opt_expr: Optimizing module dupred.
opt_merge: Removed a total of 1 cells.
opt_muxtree: Removed 0 multiplexer ports.
opt_clean: Removed 0 unused cells and 3 unused wires.On the duplicate design the division of labour is visible in the recorded counts: elaboration gives 2 AND cells and 1 OR cell, and after the passes 1 AND cell remains with no OR cell. The merge pass unites the duplicates; the expression pass folds the OR of a signal with itself.
The next two scripts run the harder pair. y = a | (a & b) is redundant by absorption: when a is 1 the answer is 1 whatever b says, and when a is 0 both terms are 0. y = (a & b) | (a & ~b) is redundant by reconvergence: a fans out to both AND gates and meets again at the OR, and case-splitting on a shows the output always equals a. Both arguments quantify over input combinations, and opt_expr carries no rule for either shape, so opt leaves both netlists untouched; ABC's hashing and rewriting, which compares truth tables of small cuts, removes them during mapping.
read_verilog <<EOV
module absred(input wire a, input wire b, output wire y);
assign y = a | (a & b);
endmodule
EOV
hierarchy -check -top absred
proc
tee -o stat_abs_proc.txt stat
write_json net_abs_proc.json
opt
check
tee -o stat_abs_opt.txt stat
write_json net_abs_opt.json
select -assert-count 1 t:$and
select -assert-count 1 t:$or
techmap
tee -o abc_abs.txt abc -liberty course.lib -constr typical.constr -D 1000
tee -o stat_abs.txt stat -liberty course.lib
write_json net_abs_map.json
select -assert-none t:$and t:$or t:$not
check
log PASS: opt keeps the absorption term while abc maps it awayread_verilog <<EOV
module recreconv(input wire a, input wire b, output wire y);
assign y = (a & b) | (a & ~b);
endmodule
EOV
hierarchy -check -top recreconv
proc
tee -o stat_rec_proc.txt stat
write_json net_rec_proc.json
opt
check
tee -o stat_rec_opt.txt stat
write_json net_rec_opt.json
select -assert-count 2 t:$and
select -assert-count 1 t:$or
techmap
tee -o abc_rec.txt abc -liberty course.lib -constr typical.constr -D 1000
tee -o stat_rec.txt stat -liberty course.lib
write_json net_rec_map.json
select -assert-none t:$and t:$or t:$not
check
log PASS: opt keeps the reconvergent redundancy while abc maps it awayDesign and flow | Generic netlist after opt | Mapped cover (ABC, course.lib) |
|---|---|---|
Absorption, opt then ABC | 1 AND plus 1 OR: opt changed nothing | 1 x BUF, area 2 |
Reconvergence, opt then ABC | 2 AND plus 1 OR: opt changed nothing | 1 x BUF, area 2 |
Read the middle column first: it is the recorded evidence that generic optimisation changed nothing on either harder design. Then read the right column: mapping covers each output with a buffer, 1 x BUF in both recorded histograms, because once ABC sees that each output equals the wire a, a buffer carrying a to the output is the whole cover.
Three simplifications that sound aggressive need one proof: the duplicate pair folded to one AND, and both harder outputs replaced by the wire a. The script below builds the original three-output design beside the simplified one, joins them with a miter, and asks the solver to prove the trigger can never fire. Proving happens before mapping on purpose: library cells carry no SAT model.
read_verilog <<EOV
module gold(input wire a, input wire b, output wire y_dup, output wire y_abs, output wire y_rec);
wire t1 = a & b;
wire t2 = a & b;
assign y_dup = t1 | t2;
assign y_abs = a | (a & b);
assign y_rec = (a & b) | (a & ~b);
endmodule
module gate(input wire a, input wire b, output wire y_dup, output wire y_abs, output wire y_rec);
wire t = a & b;
assign y_dup = t;
assign y_abs = a;
assign y_rec = a;
endmodule
EOV
hierarchy -check
proc
opt
miter -equiv -flatten -make_outputs gold gate m
hierarchy -top m
sat -prove trigger 0 -verify m
log PASS: simplified outputs equal the originals on every inputThe PASS line covers all four input pairs on all three outputs at once: the miter flags any input where any pair of outputs differs, and the solver proves no such input exists. Sampling a few vectors could miss the one that differs; the proof quantifies over all of them. That is the difference between testing a rewrite and knowing it.
Now change one thing about the setup: run the duplicate design through opt_expr and opt_clean but skip opt_merge. If merging were doing real work here, skipping it should leave gates behind. The script folds and cleans without merging and records the surviving netlist.
read_verilog <<EOV
module dupred(input wire a, input wire b, output wire y);
wire t1 = a & b;
wire t2 = a & b;
assign y = t1 | t2;
endmodule
EOV
hierarchy -check -top dupred
proc
tee -o stat_nomerge_proc.txt stat
write_json net_nomerge_proc.json
opt_expr
opt_clean
check
tee -o stat_nomerge.txt stat
write_json net_nomerge.json
select -assert-count 2 t:$and
select -assert-count 1 t:$or
log PASS: folding without merging leaves the duplicate pair in placeWithout the merge pass the recorded netlist keeps 2 AND cells and 1 OR cell, against a single AND cell and no OR with the full pass. Expression folding alone cannot see that the two AND cells are identical; only the merge pass unites them so the self-OR can fold. The general lesson is that each redundancy belongs to the pass that can see it, and the recorded stage reports tell you which one did the work.
Redundancy at RTL is not always redundancy at the gate level. Sub-expressions written twice in RTL merge only if every input matches exactly after elaboration; one width or signedness difference prevents the merge, and the duplicate survives to mapping. Check the proc-stage netlist before assuming the merge happened.
One AND gate: the recorded counts move from 2 AND cells to 1 with the OR gone. The merge pass unites the duplicates and the expression pass folds the OR of a signal with itself. Without the merge pass the pair survives, as the break-it run shows.
opt but maps to a buffer. Which tool did the real work, and what proves the answer stayed right?ABC did the work during mapping, reaching 1 x BUF at area 2. The miter plus sat proof shows the simplified outputs equal the originals on every input.
Every number and picture on this page comes from five Yosys 0.69+post runs recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): a duplicate design with per-pass opt logs, absorption and reconvergent designs through proc, opt and abc -liberty course.lib, a duplicate design folded without the merge pass, and a miter with sat -prove over all three simplifications. course.lib is fictional, so areas are teaching units.