vj-sim 0.1.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! Test-only source-scan helper (Task 3, DT-006 review fix). Both this
//! task's own `dt006_no_thread_spawn_or_rollback_code_outside_determinism_module`
//! (`sequential.rs`) and Task 22's crate-wide `notify_change_has_exactly_two_
//! call_sites_in_the_crate` scan need "scan production code only, not
//! `#[cfg(test)]` bodies" for the identical reason (a legitimate test-only
//! occurrence of the scanned-for substring must not trip the gate) — this is
//! the one shared implementation, not two independent brace-matchers.

/// For each occurrence of the literal marker `#[cfg(test)]` in `src`, finds
/// the next `{` after it, then scans forward tracking brace depth (`+1` per
/// `{`, `-1` per `}`) until depth returns to `0`, and removes that whole
/// `#[cfg(test)] .. { .. }` span from the string (repeats until no
/// `#[cfg(test)]` marker remains). Returns the remaining "production-only"
/// source text.
///
/// Byte-index slicing is safe throughout: every slice boundary used here
/// (`find`'s match start/end, and the position immediately after an ASCII
/// `{`/`}`) is a UTF-8 char boundary because the marker and both brace
/// characters are single-byte ASCII.
pub(crate) fn strip_cfg_test_blocks(src: &str) -> String {
    const MARKER: &str = "#[cfg(test)]";
    let mut result = String::new();
    let mut rest = src;
    loop {
        let Some(marker_pos) = rest.find(MARKER) else {
            result.push_str(rest);
            break;
        };
        result.push_str(&rest[..marker_pos]);
        let after_marker = &rest[marker_pos + MARKER.len()..];
        let Some(brace_pos) = after_marker.find('{') else {
            // No block follows this marker at all (malformed/edge input) —
            // drop just the marker text and keep scanning the remainder.
            rest = after_marker;
            continue;
        };
        let bytes = after_marker.as_bytes();
        let mut depth: i32 = 0;
        let mut i = brace_pos;
        let mut end = after_marker.len();
        while i < bytes.len() {
            match bytes[i] {
                b'{' => depth += 1,
                b'}' => {
                    depth -= 1;
                    if depth == 0 {
                        end = i + 1;
                        break;
                    }
                }
                _ => {}
            }
            i += 1;
        }
        rest = &after_marker[end..];
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn strips_a_single_cfg_test_mod_block_entirely() {
        let src = "fn production() {}\n\n#[cfg(test)]\nmod tests {\n    fn f() { thread::spawn(|| {}); }\n}\n";
        let stripped = strip_cfg_test_blocks(src);
        assert!(!stripped.contains("thread::spawn"));
        assert!(stripped.contains("fn production()"));
    }

    #[test]
    fn leaves_production_code_outside_any_cfg_test_block_untouched() {
        let src = "fn production() { thread::spawn(|| {}); }\n";
        let stripped = strip_cfg_test_blocks(src);
        assert!(stripped.contains("thread::spawn"));
    }

    #[test]
    fn handles_nested_braces_inside_the_stripped_block() {
        let src = "#[cfg(test)]\nmod tests {\n    fn f() { if true { thread::spawn(|| {}); } }\n}\nfn after() {}\n";
        let stripped = strip_cfg_test_blocks(src);
        assert!(!stripped.contains("thread::spawn"));
        assert!(stripped.contains("fn after()"));
    }
}