Skip to main content

xlsynth_g8r/netlist/
techmap.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Structural technology mapping from `GateFn` (AIG) to gate-level netlists.
4//!
5//! The initial mapper is intentionally simple and deterministic:
6//! - Each AIG `And2` node maps to `NAND2` followed by `INV`.
7//! - Negated operands are materialized via explicit `INV` edge inverters.
8//! - Output negations are materialized via explicit `INV` instances.
9//! - Current scope is limited to direct structural lowering, not general
10//!   cell-covering or optimization across wider cell libraries.
11//!
12//! Output is provided in parsed-netlist data structures so later flows (e.g.
13//! STA) can consume it directly without reparsing text.
14
15use crate::aig::gate::{AigNode, AigOperand, AigRef, GateFn};
16use crate::netlist::parse::{
17    Net, NetIndex, NetRef, NetlistInstance, NetlistModule, NetlistPort, PortDirection,
18};
19use anyhow::{Result, anyhow};
20use std::collections::{HashMap, HashSet};
21use string_interner::symbol::SymbolU32;
22use string_interner::{StringInterner, backend::StringBackend};
23use xlsynth::IrBits;
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct StructuralTechMapOptions {
27    pub module_name: Option<String>,
28    pub nand2_cell_name: String,
29    pub nand2_input_pin_names: [String; 2],
30    pub nand2_output_pin_name: String,
31    pub inv_cell_name: String,
32    pub inv_input_pin_name: String,
33    pub inv_output_pin_name: String,
34}
35
36impl Default for StructuralTechMapOptions {
37    fn default() -> Self {
38        Self {
39            module_name: None,
40            nand2_cell_name: "NAND2".to_string(),
41            nand2_input_pin_names: ["A".to_string(), "B".to_string()],
42            nand2_output_pin_name: "Y".to_string(),
43            inv_cell_name: "INV".to_string(),
44            inv_input_pin_name: "A".to_string(),
45            inv_output_pin_name: "Y".to_string(),
46        }
47    }
48}
49
50#[derive(Debug)]
51pub struct StructuralMappedNetlist {
52    pub module: NetlistModule,
53    pub nets: Vec<Net>,
54    pub interner: StringInterner<StringBackend<SymbolU32>>,
55}
56
57#[derive(Clone, Copy, Debug)]
58enum TechSignal {
59    Net(NetIndex),
60    Literal(bool),
61}
62
63#[derive(Clone, Debug)]
64struct OutputBitPlan {
65    port_name: String,
66    port_net: NetIndex,
67    operand: AigOperand,
68}
69
70fn scalar_bit_name(base: &str, bit_index: usize, bit_count: usize) -> String {
71    if bit_count == 1 {
72        base.to_string()
73    } else {
74        format!("{}_{}", base, bit_index)
75    }
76}
77
78fn literal_netref(value: bool) -> NetRef {
79    NetRef::Literal(IrBits::make_ubits(1, if value { 1 } else { 0 }).expect("1-bit literal"))
80}
81
82fn signal_to_netref(signal: TechSignal) -> NetRef {
83    match signal {
84        TechSignal::Net(idx) => NetRef::Simple(idx),
85        TechSignal::Literal(value) => literal_netref(value),
86    }
87}
88
89/// Verifies that literal propagation has removed constants from mapped cell
90/// inputs before structural lowering starts.
91fn verify_folded_literal_operands(gate_fn: &GateFn) -> Result<()> {
92    for (gate_id, node) in gate_fn.gates.iter().enumerate() {
93        let AigNode::And2 { a, b, .. } = node else {
94            continue;
95        };
96        for (operand_name, operand) in [("a", a), ("b", b)] {
97            if matches!(gate_fn.get(operand.node), AigNode::Literal { .. }) {
98                return Err(anyhow!(
99                    "AIG literal operand reached structural tech mapping at And2 gate {} input {}; constants must be folded before mapping",
100                    gate_id,
101                    operand_name
102                ));
103            }
104        }
105    }
106    Ok(())
107}
108
109fn fresh_internal_net_name(base: String, used_net_names: &mut HashSet<String>) -> String {
110    if used_net_names.insert(base.clone()) {
111        return base;
112    }
113    let mut suffix = 1usize;
114    loop {
115        let candidate = format!("{}__{}", base, suffix);
116        if used_net_names.insert(candidate.clone()) {
117            return candidate;
118        }
119        suffix += 1;
120    }
121}
122
123#[allow(clippy::too_many_arguments)]
124fn ensure_net(
125    interner: &mut StringInterner<StringBackend<SymbolU32>>,
126    nets: &mut Vec<Net>,
127    net_index_by_sym: &mut HashMap<SymbolU32, NetIndex>,
128    port_net_indices: &mut HashSet<NetIndex>,
129    wire_net_indices: &mut Vec<NetIndex>,
130    wire_net_set: &mut HashSet<NetIndex>,
131    name: &str,
132    is_port: bool,
133) -> NetIndex {
134    let sym = interner.get_or_intern(name);
135    let idx = if let Some(existing) = net_index_by_sym.get(&sym).copied() {
136        existing
137    } else {
138        let created = NetIndex(nets.len());
139        nets.push(Net {
140            name: sym,
141            width: None,
142        });
143        net_index_by_sym.insert(sym, created);
144        created
145    };
146
147    if is_port {
148        if port_net_indices.insert(idx) && wire_net_set.remove(&idx) {
149            wire_net_indices.retain(|x| *x != idx);
150        }
151    } else if !port_net_indices.contains(&idx) && wire_net_set.insert(idx) {
152        wire_net_indices.push(idx);
153    }
154
155    idx
156}
157
158fn add_instance(
159    interner: &mut StringInterner<StringBackend<SymbolU32>>,
160    instances: &mut Vec<NetlistInstance>,
161    type_name: &str,
162    instance_name: &str,
163    connections: Vec<(&str, NetRef)>,
164) {
165    let type_sym = interner.get_or_intern(type_name);
166    let instance_sym = interner.get_or_intern(instance_name);
167
168    let mut conn_vec: Vec<(SymbolU32, NetRef)> = connections
169        .into_iter()
170        .map(|(pin_name, netref)| (interner.get_or_intern(pin_name), netref))
171        .collect();
172
173    conn_vec.sort_by(|(a, _), (b, _)| {
174        interner
175            .resolve(*a)
176            .unwrap_or("")
177            .cmp(interner.resolve(*b).unwrap_or(""))
178    });
179
180    instances.push(NetlistInstance {
181        type_name: type_sym,
182        instance_name: instance_sym,
183        connections: conn_vec,
184        inst_lineno: 1,
185        inst_colno: 1,
186    });
187}
188
189fn resolve_node_signal(
190    gate_fn: &GateFn,
191    node_signals: &[Option<NetIndex>],
192    node: AigRef,
193) -> Result<TechSignal> {
194    match gate_fn.get(node) {
195        AigNode::Literal { value, .. } => Ok(TechSignal::Literal(*value)),
196        AigNode::Input { .. } | AigNode::And2 { .. } => {
197            let Some(net_idx) = node_signals.get(node.id).and_then(|x| *x) else {
198                return Err(anyhow!("unmapped node signal for AIG node {}", node.id));
199            };
200            Ok(TechSignal::Net(net_idx))
201        }
202    }
203}
204
205#[allow(clippy::too_many_arguments)]
206fn materialize_operand_signal_for_cell_input(
207    gate_fn: &GateFn,
208    node_signals: &[Option<NetIndex>],
209    operand: AigOperand,
210    edge_inv_counter: &mut usize,
211    options: &StructuralTechMapOptions,
212    interner: &mut StringInterner<StringBackend<SymbolU32>>,
213    nets: &mut Vec<Net>,
214    net_index_by_sym: &mut HashMap<SymbolU32, NetIndex>,
215    port_net_indices: &mut HashSet<NetIndex>,
216    wire_net_indices: &mut Vec<NetIndex>,
217    wire_net_set: &mut HashSet<NetIndex>,
218    used_net_names: &mut HashSet<String>,
219    instances: &mut Vec<NetlistInstance>,
220) -> Result<TechSignal> {
221    let base = resolve_node_signal(gate_fn, node_signals, operand.node)?;
222    if !operand.negated {
223        return Ok(base);
224    }
225
226    match base {
227        TechSignal::Literal(v) => Ok(TechSignal::Literal(!v)),
228        TechSignal::Net(src) => {
229            let inv_id = *edge_inv_counter;
230            *edge_inv_counter += 1;
231
232            let out_name =
233                fresh_internal_net_name(format!("n_inv_edge_{}", inv_id), used_net_names);
234            let out_net = ensure_net(
235                interner,
236                nets,
237                net_index_by_sym,
238                port_net_indices,
239                wire_net_indices,
240                wire_net_set,
241                out_name.as_str(),
242                /* is_port= */ false,
243            );
244            add_instance(
245                interner,
246                instances,
247                options.inv_cell_name.as_str(),
248                format!("u_inv_edge_{}", inv_id).as_str(),
249                vec![
250                    (options.inv_input_pin_name.as_str(), NetRef::Simple(src)),
251                    (
252                        options.inv_output_pin_name.as_str(),
253                        NetRef::Simple(out_net),
254                    ),
255                ],
256            );
257            Ok(TechSignal::Net(out_net))
258        }
259    }
260}
261
262pub fn map_gatefn_to_structural_netlist(
263    gate_fn: &GateFn,
264    options: &StructuralTechMapOptions,
265) -> Result<StructuralMappedNetlist> {
266    gate_fn.check_invariants_with_debug_assert();
267    verify_folded_literal_operands(gate_fn)?;
268
269    let module_name = options
270        .module_name
271        .clone()
272        .unwrap_or_else(|| gate_fn.name.clone());
273
274    let mut interner: StringInterner<StringBackend<SymbolU32>> = StringInterner::new();
275    let module_name_sym = interner.get_or_intern(module_name.as_str());
276
277    let mut nets: Vec<Net> = Vec::new();
278    let mut net_index_by_sym: HashMap<SymbolU32, NetIndex> = HashMap::new();
279    let mut port_net_indices: HashSet<NetIndex> = HashSet::new();
280    let mut wire_net_indices: Vec<NetIndex> = Vec::new();
281    let mut wire_net_set: HashSet<NetIndex> = HashSet::new();
282    let mut ports: Vec<NetlistPort> = Vec::new();
283    let mut output_plans: Vec<OutputBitPlan> = Vec::new();
284    let mut port_names_seen: HashSet<String> = HashSet::new();
285    let mut used_net_names: HashSet<String> = HashSet::new();
286
287    let mut node_signals: Vec<Option<NetIndex>> = vec![None; gate_fn.gates.len()];
288
289    for input in &gate_fn.inputs {
290        let bit_count = input.get_bit_count();
291        for (bit_index, bit) in input.bit_vector.iter_lsb_to_msb().enumerate() {
292            let port_name = scalar_bit_name(input.name.as_str(), bit_index, bit_count);
293            if !port_names_seen.insert(port_name.clone()) {
294                return Err(anyhow!(
295                    "duplicate mapped port name '{}' in input set",
296                    port_name
297                ));
298            }
299            used_net_names.insert(port_name.clone());
300            let port_sym = interner.get_or_intern(port_name.as_str());
301            let net_idx = ensure_net(
302                &mut interner,
303                &mut nets,
304                &mut net_index_by_sym,
305                &mut port_net_indices,
306                &mut wire_net_indices,
307                &mut wire_net_set,
308                port_name.as_str(),
309                /* is_port= */ true,
310            );
311            ports.push(NetlistPort {
312                direction: PortDirection::Input,
313                width: None,
314                name: port_sym,
315            });
316            if bit.node.id >= node_signals.len() {
317                return Err(anyhow!(
318                    "input bit references out-of-range node {}",
319                    bit.node.id
320                ));
321            }
322            node_signals[bit.node.id] = Some(net_idx);
323        }
324    }
325
326    for output in &gate_fn.outputs {
327        let bit_count = output.get_bit_count();
328        for (bit_index, bit) in output.bit_vector.iter_lsb_to_msb().enumerate() {
329            let port_name = scalar_bit_name(output.name.as_str(), bit_index, bit_count);
330            if !port_names_seen.insert(port_name.clone()) {
331                return Err(anyhow!(
332                    "duplicate mapped port name '{}' in output set",
333                    port_name
334                ));
335            }
336            used_net_names.insert(port_name.clone());
337            let port_sym = interner.get_or_intern(port_name.as_str());
338            let net_idx = ensure_net(
339                &mut interner,
340                &mut nets,
341                &mut net_index_by_sym,
342                &mut port_net_indices,
343                &mut wire_net_indices,
344                &mut wire_net_set,
345                port_name.as_str(),
346                /* is_port= */ true,
347            );
348            ports.push(NetlistPort {
349                direction: PortDirection::Output,
350                width: None,
351                name: port_sym,
352            });
353            output_plans.push(OutputBitPlan {
354                port_name,
355                port_net: net_idx,
356                operand: *bit,
357            });
358        }
359    }
360
361    let mut owner_output_name_by_node: HashMap<usize, String> = HashMap::new();
362    for plan in &output_plans {
363        if plan.operand.negated {
364            continue;
365        }
366        if matches!(gate_fn.get(plan.operand.node), AigNode::Literal { .. }) {
367            continue;
368        }
369        owner_output_name_by_node
370            .entry(plan.operand.node.id)
371            .or_insert_with(|| plan.port_name.clone());
372    }
373
374    let mut instances: Vec<NetlistInstance> = Vec::new();
375    let mut edge_inv_counter = 0usize;
376
377    for (gate_id, node) in gate_fn.gates.iter().enumerate() {
378        match node {
379            AigNode::Input { .. } | AigNode::Literal { .. } => {}
380            AigNode::And2 { a, b, .. } => {
381                let a_signal = materialize_operand_signal_for_cell_input(
382                    gate_fn,
383                    node_signals.as_slice(),
384                    *a,
385                    &mut edge_inv_counter,
386                    options,
387                    &mut interner,
388                    &mut nets,
389                    &mut net_index_by_sym,
390                    &mut port_net_indices,
391                    &mut wire_net_indices,
392                    &mut wire_net_set,
393                    &mut used_net_names,
394                    &mut instances,
395                )?;
396                let b_signal = materialize_operand_signal_for_cell_input(
397                    gate_fn,
398                    node_signals.as_slice(),
399                    *b,
400                    &mut edge_inv_counter,
401                    options,
402                    &mut interner,
403                    &mut nets,
404                    &mut net_index_by_sym,
405                    &mut port_net_indices,
406                    &mut wire_net_indices,
407                    &mut wire_net_set,
408                    &mut used_net_names,
409                    &mut instances,
410                )?;
411
412                let nand_out_name =
413                    fresh_internal_net_name(format!("n_nand_g{}", gate_id), &mut used_net_names);
414                let nand_out_net = ensure_net(
415                    &mut interner,
416                    &mut nets,
417                    &mut net_index_by_sym,
418                    &mut port_net_indices,
419                    &mut wire_net_indices,
420                    &mut wire_net_set,
421                    nand_out_name.as_str(),
422                    /* is_port= */ false,
423                );
424                add_instance(
425                    &mut interner,
426                    &mut instances,
427                    options.nand2_cell_name.as_str(),
428                    format!("u_nand_g{}", gate_id).as_str(),
429                    vec![
430                        (
431                            options.nand2_input_pin_names[0].as_str(),
432                            signal_to_netref(a_signal),
433                        ),
434                        (
435                            options.nand2_input_pin_names[1].as_str(),
436                            signal_to_netref(b_signal),
437                        ),
438                        (
439                            options.nand2_output_pin_name.as_str(),
440                            NetRef::Simple(nand_out_net),
441                        ),
442                    ],
443                );
444
445                let node_out_name = owner_output_name_by_node
446                    .get(&gate_id)
447                    .cloned()
448                    .unwrap_or_else(|| {
449                        fresh_internal_net_name(format!("n_g{}", gate_id), &mut used_net_names)
450                    });
451                let node_out_net = ensure_net(
452                    &mut interner,
453                    &mut nets,
454                    &mut net_index_by_sym,
455                    &mut port_net_indices,
456                    &mut wire_net_indices,
457                    &mut wire_net_set,
458                    node_out_name.as_str(),
459                    /* is_port= */ false,
460                );
461                add_instance(
462                    &mut interner,
463                    &mut instances,
464                    options.inv_cell_name.as_str(),
465                    format!("u_inv_g{}", gate_id).as_str(),
466                    vec![
467                        (
468                            options.inv_input_pin_name.as_str(),
469                            NetRef::Simple(nand_out_net),
470                        ),
471                        (
472                            options.inv_output_pin_name.as_str(),
473                            NetRef::Simple(node_out_net),
474                        ),
475                    ],
476                );
477
478                node_signals[gate_id] = Some(node_out_net);
479            }
480        }
481    }
482
483    for (output_index, output) in output_plans.iter().enumerate() {
484        let base_signal =
485            resolve_node_signal(gate_fn, node_signals.as_slice(), output.operand.node)?;
486
487        match (base_signal, output.operand.negated) {
488            (TechSignal::Literal(_), _) => {
489                return Err(anyhow!(
490                    "constant output '{}' is unsupported by structural tech mapping because the STA-compatible cell subset does not include constant drivers",
491                    output.port_name
492                ));
493            }
494            (TechSignal::Net(src), true) => {
495                add_instance(
496                    &mut interner,
497                    &mut instances,
498                    options.inv_cell_name.as_str(),
499                    format!("u_inv_out_neg_{}", output_index).as_str(),
500                    vec![
501                        (options.inv_input_pin_name.as_str(), NetRef::Simple(src)),
502                        (
503                            options.inv_output_pin_name.as_str(),
504                            NetRef::Simple(output.port_net),
505                        ),
506                    ],
507                );
508            }
509            (TechSignal::Net(src), false) => {
510                if src != output.port_net {
511                    // Keep the emitted netlist purely structural: when a
512                    // distinct output port must mirror an existing signal,
513                    // realize that connection through cells instead of a
514                    // continuous assign.
515                    let tmp_name = fresh_internal_net_name(
516                        format!("n_out_buf_{}", output_index),
517                        &mut used_net_names,
518                    );
519                    let tmp_net = ensure_net(
520                        &mut interner,
521                        &mut nets,
522                        &mut net_index_by_sym,
523                        &mut port_net_indices,
524                        &mut wire_net_indices,
525                        &mut wire_net_set,
526                        tmp_name.as_str(),
527                        /* is_port= */ false,
528                    );
529                    add_instance(
530                        &mut interner,
531                        &mut instances,
532                        options.inv_cell_name.as_str(),
533                        format!("u_inv_out_buf0_{}", output_index).as_str(),
534                        vec![
535                            (options.inv_input_pin_name.as_str(), NetRef::Simple(src)),
536                            (
537                                options.inv_output_pin_name.as_str(),
538                                NetRef::Simple(tmp_net),
539                            ),
540                        ],
541                    );
542                    add_instance(
543                        &mut interner,
544                        &mut instances,
545                        options.inv_cell_name.as_str(),
546                        format!("u_inv_out_buf1_{}", output_index).as_str(),
547                        vec![
548                            (options.inv_input_pin_name.as_str(), NetRef::Simple(tmp_net)),
549                            (
550                                options.inv_output_pin_name.as_str(),
551                                NetRef::Simple(output.port_net),
552                            ),
553                        ],
554                    );
555                }
556            }
557        }
558    }
559
560    let module = NetlistModule {
561        name: module_name_sym,
562        net_index_range: 0..nets.len(),
563        ports,
564        wires: wire_net_indices,
565        assigns: vec![],
566        instances,
567    };
568
569    Ok(StructuralMappedNetlist {
570        module,
571        nets,
572        interner,
573    })
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use crate::aig_sim::gate_sim::{Collect, eval};
580    use crate::gate_builder::{GateBuilder, GateBuilderOptions};
581    use crate::liberty_proto::{Cell, Library as LibertyLibrary, Pin, PinDirection};
582    use crate::netlist::gatefn_from_netlist::project_gatefn_from_netlist_and_liberty;
583    use std::collections::HashSet;
584
585    fn net_name(mapped: &StructuralMappedNetlist, idx: NetIndex) -> String {
586        mapped
587            .nets
588            .get(idx.0)
589            .and_then(|n| mapped.interner.resolve(n.name))
590            .unwrap_or("<unknown>")
591            .to_string()
592    }
593
594    fn sym_name(interner: &StringInterner<StringBackend<SymbolU32>>, sym: SymbolU32) -> String {
595        interner.resolve(sym).unwrap_or("<unknown>").to_string()
596    }
597
598    fn make_inv_nand2_liberty() -> LibertyLibrary {
599        LibertyLibrary {
600            cells: vec![
601                Cell {
602                    name: "INV".to_string(),
603                    pins: vec![
604                        Pin {
605                            direction: PinDirection::Input as i32,
606                            name: "A".to_string(),
607                            ..Default::default()
608                        },
609                        Pin {
610                            direction: PinDirection::Output as i32,
611                            name: "Y".to_string(),
612                            function: "!A".to_string(),
613                            ..Default::default()
614                        },
615                    ],
616                    ..Default::default()
617                },
618                Cell {
619                    name: "NAND2".to_string(),
620                    pins: vec![
621                        Pin {
622                            direction: PinDirection::Input as i32,
623                            name: "A".to_string(),
624                            ..Default::default()
625                        },
626                        Pin {
627                            direction: PinDirection::Input as i32,
628                            name: "B".to_string(),
629                            ..Default::default()
630                        },
631                        Pin {
632                            direction: PinDirection::Output as i32,
633                            name: "Y".to_string(),
634                            function: "!(A*B)".to_string(),
635                            ..Default::default()
636                        },
637                    ],
638                    ..Default::default()
639                },
640            ],
641            ..Default::default()
642        }
643    }
644
645    fn assert_projected_equivalent_for_all_inputs(lhs: &GateFn, rhs: &GateFn) {
646        assert_eq!(lhs.inputs.len(), rhs.inputs.len());
647        assert_eq!(lhs.outputs.len(), rhs.outputs.len());
648
649        let total_input_bits: usize = lhs.inputs.iter().map(|i| i.get_bit_count()).sum();
650        assert!(
651            total_input_bits <= 16,
652            "test harness only supports <=16 bits"
653        );
654
655        for input_index in 0..(1usize << total_input_bits) {
656            let mut inputs: Vec<IrBits> = Vec::new();
657            let mut shift = 0usize;
658            for input in &lhs.inputs {
659                let width = input.get_bit_count();
660                let value = if width == 0 {
661                    0usize
662                } else {
663                    (input_index >> shift) & ((1usize << width) - 1)
664                };
665                inputs.push(
666                    IrBits::make_ubits(width, value as u64).expect("input stimulus should fit"),
667                );
668                shift += width;
669            }
670
671            let lhs_eval = eval(lhs, inputs.as_slice(), Collect::None);
672            let rhs_eval = eval(rhs, inputs.as_slice(), Collect::None);
673            assert_eq!(lhs_eval.outputs, rhs_eval.outputs);
674        }
675    }
676
677    fn render_instance_summary(mapped: &StructuralMappedNetlist) -> Vec<String> {
678        let mut lines = Vec::new();
679        for inst in &mapped.module.instances {
680            let type_name = sym_name(&mapped.interner, inst.type_name);
681            let inst_name = sym_name(&mapped.interner, inst.instance_name);
682            let mut conn_parts = Vec::new();
683            for (pin_sym, netref) in &inst.connections {
684                let pin_name = sym_name(&mapped.interner, *pin_sym);
685                let rhs = match netref {
686                    NetRef::Simple(idx) => net_name(mapped, *idx),
687                    NetRef::Literal(bits) => {
688                        let bit = bits.get_bit(0).unwrap_or(false);
689                        if bit {
690                            "1'b1".to_string()
691                        } else {
692                            "1'b0".to_string()
693                        }
694                    }
695                    NetRef::UnknownLiteral(width) => format!("{}'hx", width),
696                    NetRef::BitSelect(idx, bit) => format!("{}[{}]", net_name(mapped, *idx), bit),
697                    NetRef::PartSelect(idx, msb, lsb) => {
698                        format!("{}[{}:{}]", net_name(mapped, *idx), msb, lsb)
699                    }
700                    NetRef::Unconnected => "<unconnected>".to_string(),
701                    NetRef::Concat(_) => "<concat>".to_string(),
702                };
703                conn_parts.push(format!("{}.{}", pin_name, rhs));
704            }
705            lines.push(format!(
706                "{} {} {}",
707                type_name,
708                inst_name,
709                conn_parts.join(" ")
710            ));
711        }
712        lines
713    }
714
715    #[test]
716    fn and_gate_maps_to_one_nand2_and_one_inv() {
717        let mut gb = GateBuilder::new("and_gate".to_string(), GateBuilderOptions::no_opt());
718        let a = gb.add_input("a".to_string(), 1);
719        let b = gb.add_input("b".to_string(), 1);
720        let and_out = gb.add_and_binary(*a.get_lsb(0), *b.get_lsb(0));
721        gb.add_output("y".to_string(), and_out.into());
722        let gate_fn = gb.build();
723
724        let mapped =
725            map_gatefn_to_structural_netlist(&gate_fn, &StructuralTechMapOptions::default())
726                .expect("mapping should succeed");
727
728        assert_eq!(mapped.module.instances.len(), 2);
729        let summary = render_instance_summary(&mapped);
730        assert!(
731            summary
732                .iter()
733                .any(|line| line.starts_with("NAND2 u_nand_g")),
734            "expected NAND2 instance in {:?}",
735            summary
736        );
737        assert!(
738            summary.iter().any(|line| line.starts_with("INV u_inv_g")),
739            "expected INV instance in {:?}",
740            summary
741        );
742    }
743
744    #[test]
745    fn negated_edge_materializes_edge_inverter() {
746        let mut gb = GateBuilder::new("neg_edge".to_string(), GateBuilderOptions::no_opt());
747        let a = gb.add_input("a".to_string(), 1);
748        let b = gb.add_input("b".to_string(), 1);
749        let and_out = gb.add_and_binary(a.get_lsb(0).negate(), *b.get_lsb(0));
750        gb.add_output("y".to_string(), and_out.into());
751        let gate_fn = gb.build();
752
753        let mapped =
754            map_gatefn_to_structural_netlist(&gate_fn, &StructuralTechMapOptions::default())
755                .expect("mapping should succeed");
756
757        let summary = render_instance_summary(&mapped);
758        assert!(
759            summary
760                .iter()
761                .any(|line| line.starts_with("INV u_inv_edge_0")),
762            "expected edge inverter in {:?}",
763            summary
764        );
765        assert_eq!(mapped.module.instances.len(), 3);
766    }
767
768    #[test]
769    fn generated_internal_nets_do_not_reuse_port_names() {
770        let mut gb = GateBuilder::new("collision".to_string(), GateBuilderOptions::no_opt());
771        let a = gb.add_input("n_nand_g3".to_string(), 1);
772        let b = gb.add_input("b".to_string(), 1);
773        let and_out = gb.add_and_binary(*a.get_lsb(0), *b.get_lsb(0));
774        gb.add_output("y".to_string(), and_out.into());
775        let gate_fn = gb.build();
776
777        let mapped =
778            map_gatefn_to_structural_netlist(&gate_fn, &StructuralTechMapOptions::default())
779                .expect("mapping should succeed");
780        let input_port_names: HashSet<String> = mapped
781            .module
782            .ports
783            .iter()
784            .filter(|port| port.direction == PortDirection::Input)
785            .map(|port| sym_name(&mapped.interner, port.name))
786            .collect();
787        let nand = mapped
788            .module
789            .instances
790            .iter()
791            .find(|inst| sym_name(&mapped.interner, inst.instance_name).starts_with("u_nand_g"))
792            .expect("nand instance expected");
793        let nand_output = nand
794            .connections
795            .iter()
796            .find_map(|(pin, netref)| (sym_name(&mapped.interner, *pin) == "Y").then_some(netref))
797            .expect("nand output connection expected");
798        let NetRef::Simple(nand_output_net) = nand_output else {
799            panic!("nand output should be a simple net");
800        };
801        let nand_output_name = net_name(&mapped, *nand_output_net);
802        assert!(!input_port_names.contains(&nand_output_name));
803        assert!(nand_output_name.starts_with("n_nand_g3"));
804    }
805
806    #[test]
807    fn constant_outputs_are_rejected_instead_of_emitting_literal_tied_cells() {
808        let mut gb = GateBuilder::new("const_out".to_string(), GateBuilderOptions::no_opt());
809        gb.add_output("y".to_string(), gb.get_false().into());
810        let gate_fn = gb.build();
811
812        let err = map_gatefn_to_structural_netlist(&gate_fn, &StructuralTechMapOptions::default())
813            .expect_err("constant outputs should be rejected");
814        assert!(
815            err.to_string()
816                .contains("constant output 'y' is unsupported")
817        );
818    }
819
820    #[test]
821    fn literal_fed_gates_are_rejected_before_mapping() {
822        let mut gb = GateBuilder::new("literal_input".to_string(), GateBuilderOptions::no_opt());
823        let a = gb.add_input("a".to_string(), 1);
824        let y = gb.add_and_binary(*a.get_lsb(0), gb.get_true());
825        gb.add_output("y".to_string(), y.into());
826        let gate_fn = gb.build();
827
828        let err = map_gatefn_to_structural_netlist(&gate_fn, &StructuralTechMapOptions::default())
829            .expect_err("literal-fed gates should be rejected");
830        assert!(
831            err.to_string()
832                .contains("constants must be folded before mapping")
833        );
834    }
835
836    #[test]
837    fn mapped_netlist_projects_back_equivalent_gatefn() {
838        let mut gb = GateBuilder::new("and_nand_dual".to_string(), GateBuilderOptions::no_opt());
839        let a = gb.add_input("a".to_string(), 1);
840        let b = gb.add_input("b".to_string(), 1);
841        let and_out = gb.add_and_binary(*a.get_lsb(0), *b.get_lsb(0));
842        let nand_out = gb.add_not(and_out);
843        gb.add_output("y_and".to_string(), and_out.into());
844        gb.add_output("y_nand".to_string(), nand_out.into());
845        let original = gb.build();
846
847        let mapped =
848            map_gatefn_to_structural_netlist(&original, &StructuralTechMapOptions::default())
849                .expect("mapping should succeed");
850
851        let lib = make_inv_nand2_liberty();
852        let dff_cells_identity: HashSet<String> = HashSet::new();
853        let dff_cells_inverted: HashSet<String> = HashSet::new();
854        let projected = project_gatefn_from_netlist_and_liberty(
855            &mapped.module,
856            mapped.nets.as_slice(),
857            &mapped.interner,
858            &lib,
859            &dff_cells_identity,
860            &dff_cells_inverted,
861        )
862        .expect("projected gatefn should build from mapped netlist");
863
864        assert_projected_equivalent_for_all_inputs(&original, &projected);
865    }
866
867    #[test]
868    fn mapping_is_deterministic() {
869        let mut gb = GateBuilder::new("deterministic".to_string(), GateBuilderOptions::no_opt());
870        let a = gb.add_input("a".to_string(), 1);
871        let b = gb.add_input("b".to_string(), 1);
872        let and_out = gb.add_and_binary(a.get_lsb(0).negate(), b.get_lsb(0).negate());
873        gb.add_output("y".to_string(), and_out.into());
874        let gate_fn = gb.build();
875
876        let first =
877            map_gatefn_to_structural_netlist(&gate_fn, &StructuralTechMapOptions::default())
878                .expect("first mapping should succeed");
879        let second =
880            map_gatefn_to_structural_netlist(&gate_fn, &StructuralTechMapOptions::default())
881                .expect("second mapping should succeed");
882
883        let first_summary = render_instance_summary(&first);
884        let second_summary = render_instance_summary(&second);
885        assert_eq!(first_summary, second_summary);
886
887        let first_nets: Vec<String> = first
888            .nets
889            .iter()
890            .map(|n| sym_name(&first.interner, n.name))
891            .collect();
892        let second_nets: Vec<String> = second
893            .nets
894            .iter()
895            .map(|n| sym_name(&second.interner, n.name))
896            .collect();
897        assert_eq!(first_nets, second_nets);
898    }
899}