veryl-analyzer 0.22.0

A modern hardware description language
Documentation
use super::*;

#[test]
fn output_array_slice_preserves_element_positions() {
    for feedback in [false, true] {
        let read = if feedback { 1 } else { 2 };
        let code = format!(
            "module Forward(i: input logic, o: output logic[2]) {{
                assign o[0] = i;
                assign o[1] = 0;
            }}
            module Top(o: output logic) {{
                var returned: logic[4];
                inst forward: Forward(i: o, o: returned[1+:2]);
                assign returned[0] = 0;
                assign returned[3] = 0;
                assign o = returned[{read}];
            }}"
        );
        let errors = analyze(&code);
        assert!(
            errors
                .iter()
                .all(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
            "{errors:?}\n{code}"
        );
        assert_eq!(!errors.is_empty(), feedback, "{errors:?}\n{code}");
        assert!(comb_loop_analysis_is_complete(&code), "{code}");
    }
}

#[test]
fn output_multidimensional_slice_preserves_rows_columns_and_bits() {
    for row in 0..4 {
        for column in 0..2 {
            for bit in 0..4 {
                let code = format!(
                    "module Scatter(i: input logic, o: output logic<4>[2, 2]) {{
                        assign o = '{{'{{4'b0, (i as 4) << 1}}, '{{4'b0, 4'b0}}}};
                    }}
                    module Top(o: output logic) {{
                        var returned: logic<4>[4, 2];
                        inst scatter: Scatter(i: o, o: returned[1+:2]);
                        assign returned[0] = '{{default: 4'b0}};
                        assign returned[3] = '{{default: 4'b0}};
                        assign o = returned[{row}][{column}][{bit}];
                    }}"
                );
                let errors = analyze(&code);
                assert!(
                    errors
                        .iter()
                        .all(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
                    "{errors:?}\n{code}"
                );
                assert_eq!(
                    !errors.is_empty(),
                    row == 1 && column == 1 && bit == 1,
                    "{errors:?}\n{code}"
                );
                assert!(comb_loop_analysis_is_complete(&code), "{code}");
            }
        }
    }
}

#[test]
fn output_concatenation_preserves_fragment_order_and_selected_offsets() {
    let reads = [
        "word[0]", "word[1]", "spare[1]", "spare[2]", "word[4]", "word[5]", "word[6]",
    ];
    for wrapper in [false, true] {
        let target = if wrapper { "Wrapper" } else { "Scatter" };
        for driven in 0..7 {
            for (read_bit, read) in reads.iter().enumerate() {
                let code = format!(
                    "module Scatter(i: input logic, o: output logic<7>) {{
                        assign o = (i as 7) << {driven};
                    }}
                    module Wrapper(i: input logic, o: output logic<7>) {{
                        inst scatter: Scatter(i: i, o: {{o[6:4], o[3:2], o[1:0]}});
                    }}
                    module Top(o: output logic) {{
                        var word: logic<7>;
                        var spare: logic<4>;
                        inst scatter: {target}(i: o, o: {{word[6:4], spare[2:1], word[1:0]}});
                        assign word[3:2] = 0;
                        assign spare[3] = 0;
                        assign spare[0] = 0;
                        assign o = {read};
                    }}"
                );
                let errors = analyze(&code);
                assert!(
                    errors
                        .iter()
                        .all(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
                    "{errors:?}\n{code}"
                );
                assert_eq!(!errors.is_empty(), read_bit == driven, "{errors:?}\n{code}");
                assert!(comb_loop_analysis_is_complete(&code), "{code}");
            }
        }
    }
}

#[test]
fn modport_output_slice_preserves_element_positions() {
    for multidimensional in [false, true] {
        for offset in [0, 1] {
            for feedback in [false, true] {
                let (shape, prefix) = if multidimensional {
                    ("[1, 4]", "[0]")
                } else {
                    ("[4]", "")
                };
                let read = if feedback { offset } else { offset + 1 };
                let assignments = (0..4)
                    .map(|index| {
                        let request = if index == offset {
                            format!("bus{prefix}[{read}].response")
                        } else {
                            "0".to_owned()
                        };
                        let response = if index < offset || index >= offset + 2 {
                            format!("assign bus{prefix}[{index}].response = 0;")
                        } else {
                            String::new()
                        };
                        format!("assign bus{prefix}[{index}].request = {request}; {response}")
                    })
                    .collect::<Vec<_>>()
                    .join("\n");
                let code = format!(
                    "interface Bus {{
                        var request: logic;
                        var response: logic;
                        modport target {{request: input, response: output}}
                    }}
                    module Targets(bus: modport Bus::target[2]) {{
                        assign bus[0].response = bus[0].request;
                        assign bus[1].response = bus[1].request;
                    }}
                    module Top {{
                        inst bus: Bus{shape};
                        inst targets: Targets(bus: bus{prefix}[{offset}+:2]);
                        {assignments}
                    }}"
                );
                let errors = analyze(&code);
                assert!(
                    errors
                        .iter()
                        .all(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
                    "{errors:?}\n{code}"
                );
                assert_eq!(!errors.is_empty(), feedback, "{errors:?}\n{code}");
                assert!(comb_loop_analysis_is_complete(&code), "{code}");
            }
        }
    }
}

#[test]
fn output_fragments_project_the_corresponding_input_expression() {
    for driven in 0..4 {
        for read in 0..4 {
            let code = format!(
                "module Forward(i: input logic<4>, o: output logic<4>) {{ assign o = i; }}
                module Top(o: output logic) {{
                    var returned: logic<4>;
                    inst forward: Forward(i: (o as 4) << {driven}, o: {{returned[1:0], returned[3:2]}});
                    assign o = returned[{read}];
                }}"
            );
            let errors = analyze(&code);
            assert!(
                errors
                    .iter()
                    .all(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
                "{errors:?}\n{code}"
            );
            assert_eq!(
                !errors.is_empty(),
                read == (driven + 2) % 4,
                "{errors:?}\n{code}"
            );
            assert!(comb_loop_analysis_is_complete(&code), "{code}");
        }
    }
}

#[test]
fn output_fragments_retain_repeated_dependencies() {
    for array in [false, true] {
        let (shape, repeated, connection) = if array {
            ("logic[4]", "'{i repeat 4}", "returned[1+:4]")
        } else {
            ("logic<4>", "{i repeat 4}", "{returned[4:3], returned[2:1]}")
        };
        let parent_shape = if array { "logic[6]" } else { "logic<6>" };
        for read in 0..6 {
            let code = format!(
                "module Fanout(i: input logic, o: output {shape}) {{ assign o = {repeated}; }}
                module Top(o: output logic) {{
                    var returned: {parent_shape};
                    inst fanout: Fanout(i: o, o: {connection});
                    assign returned[0] = 0;
                    assign returned[5] = 0;
                    assign o = returned[{read}];
                }}"
            );
            let errors = analyze(&code);
            assert!(
                errors
                    .iter()
                    .all(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
                "{errors:?}\n{code}"
            );
            assert_eq!(
                !errors.is_empty(),
                (1..5).contains(&read),
                "{errors:?}\n{code}"
            );
            assert!(comb_loop_analysis_is_complete(&code), "{code}");
        }
    }
}

#[test]
fn output_array_slice_keeps_independent_lanes_linear() {
    use crate::comb_loop_detect::{analysis_size, reset_analysis_size};

    for count in [64, 256] {
        let code = format!(
            "module Forward(i: input logic[{count}], o: output logic[{count}]) {{ assign o = i; }}
            module Top(i: input logic[{count}], o: output logic[{count}]) {{
                inst forward: Forward(i: i, o: o[0+:{count}]);
            }}"
        );
        reset_analysis_size();
        let errors = analyze(&code);
        assert!(errors.is_empty(), "{errors:?}");
        let (atoms, nodes, edges) = analysis_size();
        assert!(atoms <= count + 4, "{count}: {atoms} atoms");
        assert!(nodes <= 8 * count + 8, "{count}: {nodes} nodes");
        assert!(edges <= 8 * count + 8, "{count}: {edges} edges");
        assert!(comb_loop_analysis_is_complete(&code), "{code}");
    }
}