// SPDX-License-Identifier: AGPL-3.0-or-later
// Commercial license available
// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
// © Code 2020–2026 Miroslav Šotek. All rights reserved.
// ORCID: 0009-0009-3560-0851
// Contact: www.anulum.li | protoscience@anulum.li
// SC-NeuroCore — SystemVerilog emitter for SC IR graphs

//! SystemVerilog emitter for SC IR graphs.
//!
//! Produces synthesizable RTL that instantiates modules from `hdl/`.
//!
//! Generated module interface:
//! - Clock: `clk`
//! - Reset: `rst_n` (active-low)
//! - One port per `sc.input` / `sc.output` operation
//! - Internal wiring for all intermediate values

use crate::ir::graph::*;
use crate::ir::sv_target::{ResourceReport, SvTarget};

/// Emit a synthesizable SystemVerilog module from an SC graph.
///
/// The graph should pass `verify::verify()` before emission.
pub fn emit(graph: &ScGraph) -> Result<String, String> {
    emit_systemverilog_with_target(graph, SvTarget::Generic).map(|(systemverilog, _)| systemverilog)
}

/// Emit a synthesizable SystemVerilog module and a resource estimate for a target.
pub fn emit_systemverilog_with_target(
    graph: &ScGraph,
    target: SvTarget,
) -> Result<(String, ResourceReport), String> {
    let mut sv = String::new();

    // Header
    sv.push_str(&format!(
        "// Auto-generated by SC-NeuroCore IR Compiler v3.0\n\
         // Source graph: {}\n\
         // Do not edit — regenerate from IR source.\n\n",
        graph.name
    ));
    sv.push_str(&target.header_comment());
    sv.push_str("`timescale 1ns / 1ps\n\n");

    // Module declaration
    sv.push_str(&format!("module {} (\n", graph.name));
    sv.push_str("    input wire clk,\n");
    sv.push_str("    input wire rst_n");

    // Collect inputs and outputs for port list
    for op in &graph.ops {
        match op {
            ScOp::Input { name, ty, .. } => {
                let port_width = type_to_width(ty);
                if port_width == 1 {
                    sv.push_str(&format!(",\n    input wire {}", name));
                } else {
                    sv.push_str(&format!(
                        ",\n    input wire [{}:0] {}",
                        port_width - 1,
                        name
                    ));
                }
            }
            ScOp::Output { name, source, .. } => {
                let width = find_value_width(graph, *source);
                if width == 1 {
                    sv.push_str(&format!(",\n    output wire {}", name));
                } else {
                    sv.push_str(&format!(",\n    output wire [{}:0] {}", width - 1, name));
                }
            }
            _ => {}
        }
    }
    sv.push_str("\n);\n\n");

    // Wire declarations for intermediate values
    for op in &graph.ops {
        match op {
            ScOp::Input { .. } | ScOp::Output { .. } => {}
            ScOp::Constant { id, value, .. } => emit_constant(&mut sv, *id, value, &target),
            ScOp::Encode { id, .. } => {
                sv.push_str(&format!("    wire v{};\n", id.0));
            }
            ScOp::BitwiseAnd { id, .. } => {
                sv.push_str(&format!("    wire v{};\n", id.0));
            }
            ScOp::Popcount { id, .. } => {
                sv.push_str(&format!("    logic [63:0] v{};\n", id.0));
            }
            ScOp::LifStep { id, params, .. } => {
                sv.push_str(&format!(
                    "    wire v{}_spike;\n    wire signed [{}:0] v{}_v_out;\n",
                    id.0,
                    params.data_width - 1,
                    id.0
                ));
            }
            ScOp::DenseForward { id, params, .. } => {
                sv.push_str(&format!(
                    "    wire [{}:0] v{}_spikes;\n    wire v{}_running;\n    wire v{}_done;\n",
                    params.n_neurons - 1,
                    id.0,
                    id.0,
                    id.0
                ));
            }
            ScOp::DclsLayer { id, params, .. } => {
                sv.push_str(&format!(
                    "    wire signed [{}:0] v{};\n\
                     \x20   wire signed [31:0] v{}_accumulator_q16_16;\n\
                     \x20   wire v{}_valid;\n\
                     \x20   wire v{}_overflow;\n\
                     \x20   wire v{}_invalid_sigma;\n",
                    params.data_width - 1,
                    id.0,
                    id.0,
                    id.0,
                    id.0,
                    id.0
                ));
            }
            ScOp::BitwiseXor { id, .. } => {
                sv.push_str(&format!("    wire v{};\n", id.0));
            }
            ScOp::Reduce { id, .. } => {
                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
            }
            ScOp::GraphForward { id, n_features, .. } => {
                sv.push_str(&format!(
                    "    wire [{}:0] v{};\n",
                    n_features.saturating_sub(1),
                    id.0
                ));
            }
            ScOp::SoftmaxAttention { id, .. } => {
                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
            }
            ScOp::KuramotoStep { id, .. } => {
                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
            }
            ScOp::Scale { id, .. } | ScOp::Offset { id, .. } | ScOp::DivConst { id, .. } => {
                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
            }
        }
    }
    sv.push('\n');

    let mut inst_idx = 0_u32;

    // Module instantiations
    for op in &graph.ops {
        match op {
            ScOp::Encode { id, prob, seed, .. } => {
                let prob_wire = value_to_wire(graph, *prob);
                sv.push_str(&format!(
                    "    sc_bitstream_encoder #(\n\
                     \x20       .DATA_WIDTH(16),\n\
                     \x20       .SEED_INIT(16'h{:04X})\n\
                     \x20   ) u_enc_{} (\n\
                     \x20       .clk(clk),\n\
                     \x20       .rst_n(rst_n),\n\
                     \x20       .x_value({}),\n\
                     \x20       .t_index(32'd0),\n\
                     \x20       .bit_out(v{})\n\
                     \x20   );\n\n",
                    seed, inst_idx, prob_wire, id.0
                ));
                inst_idx += 1;
            }
            ScOp::BitwiseAnd { id, lhs, rhs } => {
                let lhs_wire = value_to_wire(graph, *lhs);
                let rhs_wire = value_to_wire(graph, *rhs);
                sv.push_str(&format!(
                    "    sc_bitstream_synapse u_syn_{} (\n\
                     \x20       .pre_bit({}),\n\
                     \x20       .w_bit({}),\n\
                     \x20       .post_bit(v{})\n\
                     \x20   );\n\n",
                    inst_idx, lhs_wire, rhs_wire, id.0
                ));
                inst_idx += 1;
            }
            ScOp::LifStep {
                id,
                current,
                leak,
                gain,
                noise,
                params,
            } => {
                let current_wire = value_to_wire(graph, *current);
                let leak_wire = value_to_wire(graph, *leak);
                let gain_wire = value_to_wire(graph, *gain);
                let noise_wire = value_to_wire(graph, *noise);
                emit_target_dsp_attribute(&mut sv, &target);
                sv.push_str(&format!(
                    "    sc_lif_neuron #(\n\
                     \x20       .DATA_WIDTH({}),\n\
                     \x20       .FRACTION({}),\n\
                     \x20       .V_REST({}),\n\
                     \x20       .V_RESET({}),\n\
                     \x20       .V_THRESHOLD({}),\n\
                     \x20       .REFRACTORY_PERIOD({})\n\
                     \x20   ) u_lif_{} (\n\
                     \x20       .clk(clk),\n\
                     \x20       .rst_n(rst_n),\n\
                     \x20       .leak_k({}),\n\
                     \x20       .gain_k({}),\n\
                     \x20       .I_t({}),\n\
                     \x20       .noise_in({}),\n\
                     \x20       .spike_out(v{}_spike),\n\
                     \x20       .v_out(v{}_v_out)\n\
                     \x20   );\n\n",
                    params.data_width,
                    params.fraction,
                    params.v_rest,
                    params.v_reset,
                    params.v_threshold,
                    params.refractory_period,
                    inst_idx,
                    leak_wire,
                    gain_wire,
                    current_wire,
                    noise_wire,
                    id.0,
                    id.0
                ));
                inst_idx += 1;
            }
            ScOp::DenseForward {
                id,
                inputs,
                weights,
                leak,
                gain,
                params,
            } => {
                let inputs_wire = value_to_wire(graph, *inputs);
                let weights_wire = value_to_wire(graph, *weights);
                let leak_wire = value_to_wire(graph, *leak);
                let gain_wire = value_to_wire(graph, *gain);
                emit_dense_fold_plan_comment(&mut sv, &target, params);
                emit_target_dsp_attribute(&mut sv, &target);
                sv.push_str(&format!(
                    "    sc_dense_layer_core #(\n\
                     \x20       .N_INPUTS({}),\n\
                     \x20       .N_NEURONS({}),\n\
                     \x20       .DATA_WIDTH({})\n\
                     \x20   ) u_dense_{} (\n\
                     \x20       .clk(clk),\n\
                     \x20       .rst_n(rst_n),\n\
                     \x20       .start_pulse(1'b1),\n\
                     \x20       .stream_len(32'd{}),\n\
                     \x20       .x_input_fp({}),\n\
                     \x20       .weight_fp({}),\n\
                     \x20       .y_min_fp(16'd0),\n\
                     \x20       .y_max_fp(16'd256),\n\
                     \x20       .cfg_leak({}),\n\
                     \x20       .cfg_gain({}),\n\
                     \x20       .I_t(),\n\
                     \x20       .spikes(v{}_spikes),\n\
                     \x20       .step_valid(),\n\
                     \x20       .run_done(v{}_done),\n\
                     \x20       .running(v{}_running)\n\
                     \x20   );\n\n",
                    params.n_inputs,
                    params.n_neurons,
                    params.data_width,
                    inst_idx,
                    params.stream_length,
                    inputs_wire,
                    weights_wire,
                    leak_wire,
                    gain_wire,
                    id.0,
                    id.0,
                    id.0
                ));
                inst_idx += 1;
            }
            ScOp::DclsLayer {
                id,
                spike,
                weights,
                centre,
                sigma,
                params,
            } => {
                if params.tap_offsets.len() != params.n_taps {
                    return Err(format!(
                        "DclsLayer (v{}) expected {} tap offsets, got {}",
                        id.0,
                        params.n_taps,
                        params.tap_offsets.len()
                    ));
                }
                let spike_wire = value_to_wire(graph, *spike);
                let weights_wire = value_to_wire(graph, *weights);
                let centre_wire = value_to_wire(graph, *centre);
                let sigma_wire = value_to_wire(graph, *sigma);
                let tap_offsets = emit_concat_u32(&params.tap_offsets, params.ptr_width)?;
                emit_target_dsp_attribute(&mut sv, &target);
                sv.push_str(&format!(
                    "    sc_dcls_layer_core #(\n\
                     \x20       .N_TAPS({}),\n\
                     \x20       .DATA_WIDTH({}),\n\
                     \x20       .FRACTION({}),\n\
                     \x20       .DELAY_DEPTH({}),\n\
                     \x20       .PTR_WIDTH({})\n\
                     \x20   ) u_dcls_{} (\n\
                     \x20       .clk(clk),\n\
                     \x20       .rst_n(rst_n),\n\
                     \x20       .in_valid(1'b1),\n\
                     \x20       .spike_in({}),\n\
                     \x20       .tap_offsets({}),\n\
                     \x20       .tap_weights_q88({}),\n\
                     \x20       .centre_q88({}),\n\
                     \x20       .sigma_q88({}),\n\
                     \x20       .out_valid(v{}_valid),\n\
                     \x20       .weighted_sum_q88(v{}),\n\
                     \x20       .accumulator_q16_16(v{}_accumulator_q16_16),\n\
                     \x20       .overflow(v{}_overflow),\n\
                     \x20       .invalid_sigma(v{}_invalid_sigma)\n\
                     \x20   );\n\n",
                    params.n_taps,
                    params.data_width,
                    params.fraction,
                    params.delay_depth,
                    params.ptr_width,
                    inst_idx,
                    spike_wire,
                    tap_offsets,
                    weights_wire,
                    centre_wire,
                    sigma_wire,
                    id.0,
                    id.0,
                    id.0,
                    id.0,
                    id.0
                ));
                inst_idx += 1;
            }
            ScOp::BitwiseXor { id, lhs, rhs } => {
                let lhs_wire = value_to_wire(graph, *lhs);
                let rhs_wire = value_to_wire(graph, *rhs);
                sv.push_str(&format!(
                    "    assign v{} = {} ^ {};\n",
                    id.0, lhs_wire, rhs_wire
                ));
            }
            ScOp::Reduce { id, input, mode } => {
                let in_wire = value_to_wire(graph, *input);
                let label = match mode {
                    ReduceMode::Sum => "reduce_sum",
                    ReduceMode::Max => "reduce_max",
                };
                sv.push_str(&format!(
                    "    // {label}: passthrough for single-element; multi-element requires adder/comparator tree\n\
                     \x20   assign v{id} = {wire};\n",
                    label = label,
                    id = id.0,
                    wire = in_wire,
                ));
            }
            ScOp::GraphForward {
                id,
                features: _,
                adjacency: _,
                n_nodes,
                n_features,
            } => {
                return Err(format!(
                    "GraphForward (v{}, {} nodes × {} features) has no synthesizable RTL implementation yet",
                    id.0, n_nodes, n_features
                ));
            }
            ScOp::SoftmaxAttention { id, dim_k, .. } => {
                return Err(format!(
                    "SoftmaxAttention (v{}, dim_k={}) has no synthesizable RTL implementation yet",
                    id.0, dim_k
                ));
            }
            ScOp::KuramotoStep { id, .. } => {
                return Err(format!(
                    "KuramotoStep (v{}) has no synthesizable RTL implementation yet",
                    id.0
                ));
            }
            ScOp::Output { name, source, .. } => {
                let src_wire = value_to_wire(graph, *source);
                sv.push_str(&format!("    assign {} = {};\n", name, src_wire));
            }
            ScOp::Scale { id, input, factor } => {
                let in_wire = value_to_wire(graph, *input);
                let scale_int = (*factor * 256.0) as i64; // Q8.8
                sv.push_str(&format!(
                    "    assign v{} = ({} * {}) >>> 8;\n",
                    id.0, in_wire, scale_int
                ));
            }
            ScOp::Offset { id, input, offset } => {
                let in_wire = value_to_wire(graph, *input);
                let offset_int = (*offset * 256.0) as i64;
                sv.push_str(&format!(
                    "    assign v{} = {} + {};\n",
                    id.0, in_wire, offset_int
                ));
            }
            ScOp::DivConst { id, input, divisor } => {
                let in_wire = value_to_wire(graph, *input);
                sv.push_str(&format!(
                    "    assign v{} = {} / {};\n",
                    id.0, in_wire, divisor
                ));
            }
            ScOp::Popcount { id, input } => {
                let in_wire = value_to_wire(graph, *input);
                sv.push_str(&format!(
                    "    // Combinatorial popcount for v{id}\n\
                     \x20   always_comb begin\n\
                     \x20       v{id} = 64'd0;\n\
                     \x20       for (integer _pc_i = 0; _pc_i < 64; _pc_i = _pc_i + 1)\n\
                     \x20           v{id} = v{id} + {{63'd0, {wire}[_pc_i]}};\n\
                     \x20   end\n\n",
                    id = id.0,
                    wire = in_wire,
                ));
            }
            _ => {}
        }
    }

    sv.push_str("\nendmodule\n");
    let report = target.estimate_graph(graph);
    Ok((sv, report))
}

fn type_to_width(ty: &ScType) -> usize {
    ty.bit_width()
}

fn find_value_width(graph: &ScGraph, id: ValueId) -> usize {
    for op in &graph.ops {
        if op.result_id() == id {
            return match op {
                ScOp::Input { ty, .. } => type_to_width(ty),
                ScOp::Constant { ty, .. } => type_to_width(ty),
                ScOp::Encode { .. } | ScOp::BitwiseAnd { .. } | ScOp::BitwiseXor { .. } => 1,
                ScOp::Popcount { .. } | ScOp::Reduce { .. } => 64,
                ScOp::LifStep { params, .. } => params.data_width as usize,
                ScOp::DenseForward { params, .. } => params.n_neurons,
                ScOp::DclsLayer { params, .. } => params.data_width as usize,
                ScOp::GraphForward { n_features, .. } => *n_features,
                ScOp::SoftmaxAttention { .. }
                | ScOp::KuramotoStep { .. }
                | ScOp::Scale { .. }
                | ScOp::Offset { .. }
                | ScOp::DivConst { .. } => 64,
                ScOp::Output { source, .. } => find_value_width(graph, *source),
            };
        }
    }
    16
}

fn value_to_wire(graph: &ScGraph, id: ValueId) -> String {
    for op in &graph.ops {
        if op.result_id() == id {
            return match op {
                ScOp::Input { name, .. } => name.clone(),
                ScOp::Constant { id, .. } => format!("c{}", id.0),
                ScOp::LifStep { id, .. } => format!("v{}_spike", id.0),
                ScOp::DenseForward { id, .. } => format!("v{}_spikes", id.0),
                _ => format!("v{}", id.0),
            };
        }
    }
    format!("v{}", id.0)
}

fn emit_concat_u32(values: &[u32], width: u32) -> Result<String, String> {
    if width == 0 {
        return Err("packed unsigned concatenation width must be positive".to_string());
    }
    let max_value = if width >= 32 {
        u32::MAX
    } else {
        (1_u32 << width) - 1
    };
    let mut fields = Vec::with_capacity(values.len());
    for value in values.iter().rev() {
        if *value > max_value {
            return Err(format!(
                "packed unsigned value {} exceeds {}-bit field",
                value, width
            ));
        }
        fields.push(format!("{}'d{}", width, value));
    }
    Ok(format!("{{{}}}", fields.join(", ")))
}

fn emit_target_dsp_attribute(sv: &mut String, target: &SvTarget) {
    if let Some(attribute) = target.dsp_attribute() {
        sv.push_str("    ");
        sv.push_str(attribute);
        sv.push('\n');
    }
}

fn emit_dense_fold_plan_comment(sv: &mut String, target: &SvTarget, params: &DenseParams) {
    let Some(plan) = target.dense_fold_plan(params.n_inputs, params.n_neurons) else {
        return;
    };
    if !plan.fold_required {
        return;
    }
    sv.push_str(&format!(
        "    // Dense fold plan: unfurled_macs={}, dsp_budget={}, dsp_per_cycle={}, output_parallelism={}, input_parallelism={}, compute_cycles={}\n",
        plan.mac_count,
        plan.dsp_budget,
        plan.dsp_per_cycle,
        plan.output_parallelism,
        plan.input_parallelism,
        plan.compute_cycles
    ));
}

fn emit_ram_style_attribute(sv: &mut String, target: &SvTarget, bits: u64) {
    if let Some(style) = target.ram_style_for_bits(bits) {
        sv.push_str(&format!("    (* ram_style = \"{}\" *)\n", style));
    }
}

fn emit_constant(sv: &mut String, id: ValueId, value: &ScConst, target: &SvTarget) {
    match value {
        ScConst::F64(v) => {
            let fp = (*v * 256.0) as i64; // Q8.8
            sv.push_str(&format!(
                "    localparam signed [15:0] c{} = 16'sd{};\n",
                id.0, fp
            ));
        }
        ScConst::I64(v) => {
            sv.push_str(&format!(
                "    localparam signed [15:0] c{} = 16'sd{};\n",
                id.0, v
            ));
        }
        ScConst::U64(v) => {
            sv.push_str(&format!("    localparam [31:0] c{} = 32'd{};\n", id.0, v));
        }
        ScConst::F64Vec(vec) => {
            let width = vec.len().saturating_mul(16);
            if width == 0 {
                sv.push_str(&format!("    wire [0:0] c{};\n", id.0));
                return;
            }
            emit_ram_style_attribute(sv, target, width as u64);
            sv.push_str(&format!("    wire [{}:0] c{};\n", width - 1, id.0));
            for (i, v) in vec.iter().enumerate() {
                let fp = (*v * 256.0) as i64;
                sv.push_str(&format!(
                    "    assign c{}[{} +: 16] = 16'sd{};\n",
                    id.0,
                    i * 16,
                    fp
                ));
            }
        }
        ScConst::I64Vec(vec) => {
            let width = vec.len().saturating_mul(16);
            if width == 0 {
                sv.push_str(&format!("    wire [0:0] c{};\n", id.0));
                return;
            }
            emit_ram_style_attribute(sv, target, width as u64);
            sv.push_str(&format!("    wire [{}:0] c{};\n", width - 1, id.0));
            for (i, v) in vec.iter().enumerate() {
                sv.push_str(&format!(
                    "    assign c{}[{} +: 16] = 16'sd{};\n",
                    id.0,
                    i * 16,
                    v
                ));
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ir::builder::ScGraphBuilder;
    use crate::ir::sv_target::{SkuKind, SvTarget};

    #[test]
    fn dcls_layer_emits_core_with_q88_contract_ports() {
        let mut builder = ScGraphBuilder::new("dcls_contract");
        let spike = builder.input("spike_in", ScType::Bool);
        let weights = builder.constant(
            ScConst::I64Vec(vec![256, 128, -64]),
            ScType::Vec {
                element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
                count: 3,
            },
        );
        let centre = builder.constant(ScConst::I64(256), ScType::FixedPoint { width: 16, frac: 8 });
        let sigma = builder.constant(ScConst::I64(512), ScType::FixedPoint { width: 16, frac: 8 });
        let result = builder.dcls_layer(
            spike,
            weights,
            centre,
            sigma,
            DclsParams {
                n_taps: 3,
                data_width: 16,
                fraction: 8,
                delay_depth: 31,
                ptr_width: 5,
                tap_offsets: vec![0, 1, 2],
            },
        );
        builder.output("weighted_sum", result);

        let sv = emit(&builder.build()).expect("DCLS layer should emit synthesizable RTL");
        assert!(sv.contains("sc_dcls_layer_core"));
        assert!(sv.contains(".tap_offsets({5'd2, 5'd1, 5'd0})"));
        assert!(sv.contains(".accumulator_q16_16(v4_accumulator_q16_16)"));
        assert!(sv.contains(".overflow(v4_overflow)"));
        assert!(sv.contains(".invalid_sigma(v4_invalid_sigma)"));
        assert!(sv.contains("assign weighted_sum = v4;"));
    }

    #[test]
    fn ultrascale_plus_target_emits_dsp48e2_metadata_and_resource_report() {
        let mut builder = ScGraphBuilder::new("ultrascale_dense");
        let inputs = builder.input(
            "inputs",
            ScType::Vec {
                element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
                count: 4,
            },
        );
        let weights = builder.constant(
            ScConst::I64Vec(vec![128; 12]),
            ScType::Vec {
                element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
                count: 12,
            },
        );
        let leak = builder.constant(ScConst::I64(16), ScType::FixedPoint { width: 16, frac: 8 });
        let gain = builder.constant(ScConst::I64(1), ScType::FixedPoint { width: 16, frac: 8 });
        let result = builder.dense_forward(
            inputs,
            weights,
            leak,
            gain,
            DenseParams {
                n_inputs: 4,
                n_neurons: 3,
                ..DenseParams::default()
            },
        );
        builder.output("spikes", result);

        let (sv, report) = emit_systemverilog_with_target(
            &builder.build(),
            SvTarget::zynq_ultrascale_plus(SkuKind::Zu3eg, 250),
        )
        .expect("UltraScale+ target emission should succeed");

        assert!(sv.contains("Target: Zynq UltraScale+ MPSoC ZU3EG"));
        assert!(sv.contains("sc_target_dsp = \"DSP48E2\""));
        assert!(sv.contains("(* ram_style = \"distributed\" *)"));
        assert_eq!(report.device_part, "xczu3eg-sbva484-1-e");
        assert!(report.dsp_estimated >= 12);
        assert!(report.fits_dsp_budget);
    }

    #[test]
    fn ultrascale_plus_over_budget_dense_emits_fold_plan_comment() {
        let mut builder = ScGraphBuilder::new("ultrascale_fold_dense");
        let inputs = builder.input(
            "inputs",
            ScType::Vec {
                element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
                count: 64,
            },
        );
        let weights = builder.constant(
            ScConst::I64Vec(vec![128; 64 * 32]),
            ScType::Vec {
                element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
                count: 64 * 32,
            },
        );
        let leak = builder.constant(ScConst::I64(16), ScType::FixedPoint { width: 16, frac: 8 });
        let gain = builder.constant(ScConst::I64(1), ScType::FixedPoint { width: 16, frac: 8 });
        let result = builder.dense_forward(
            inputs,
            weights,
            leak,
            gain,
            DenseParams {
                n_inputs: 64,
                n_neurons: 32,
                ..DenseParams::default()
            },
        );
        builder.output("spikes", result);

        let (sv, report) = emit_systemverilog_with_target(
            &builder.build(),
            SvTarget::zynq_ultrascale_plus(SkuKind::Zu3eg, 250),
        )
        .expect("UltraScale+ target emission should produce fold-plan metadata");

        assert!(sv.contains("Dense fold plan: unfurled_macs=2048"));
        assert!(sv.contains("dsp_per_cycle=320"));
        assert!(sv.contains("compute_cycles=7"));
        assert!(report.dense_fold_plan.is_some());
    }
}
