vox-lang 0.4.4

A systems level compiler for Vox (sentence based code)
    use super::*;
    use crate::lexer::Lexer;
    use crate::parser::Parser;

    fn analyze_input(input: &str) -> Analyzer {
        let mut lexer = Lexer::new(input);
        let tokens = lexer.tokenize();
        let mut parser = Parser::new(tokens);
        let mut program = parser.parse().expect("input should parse");
        let mut analyzer = Analyzer::new().with_source("test.en", input);
        analyzer.analyze(&mut program);
        analyzer
    }

    #[test]
    fn append_requires_buffer_source_when_destination_is_buffer() {
        let input = r#"
            a buffer called dst is "hello".
            a number called n is 7.
            append n to dst.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Buffer append requires")),
            "expected buffer-append type error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn quoted_condition_unknown_variable_inside_function_is_reported() {
        let input = r#"
            To mutate,
                if 'missing' then,
                    Print "ok".

            'mutate'.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Unknown variable: missing")),
            "expected unknown-variable error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn quoted_condition_top_level_global_inside_function_is_allowed() {
        let input = r#"
            a boolean called counter is true.

            To bump,
                if 'counter' then,
                    Print "ok".

            'bump'.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .all(|e| !e.message.contains("Unknown variable: counter")),
            "unexpected unknown-variable errors: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn copy_requires_buffers_for_both_operands() {
        let input = r#"
            a buffer called dst is "hello".
            a number called n is 7.
            copy n to dst.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Copy source must be a buffer")),
            "expected copy-source type error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn clear_requires_buffer_operand() {
        let input = r#"
            a number called n is 7.
            clear n.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Clear target must be a buffer")),
            "expected clear-target type error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn append_allows_format_string_when_destination_is_buffer() {
        let input = r#"
            a number called n is 7.
            a buffer called dst is "".
            append "N={n:04}" to dst.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .all(|e| !e.message.contains("Buffer append requires")),
            "unexpected buffer-append error(s): {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn copy_allows_format_string_source() {
        let input = r#"
            a number called n is 7.
            a buffer called dst is "".
            copy "N={n:04}" to dst.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .all(|e| !e.message.contains("Copy source must be a buffer") && !e.message.contains("Copy source must be a buffer or format/literal text")),
            "unexpected copy-source error(s): {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn file_open_rejects_float_path_literal() {
        let input = r#"
            open a file for reading called source at 1.5.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Open path must be either a text path")),
            "expected open-path type error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn file_open_rejects_boolean_path_literal() {
        let input = r#"
            open a file for reading called source at true.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Open path must be either a text path")),
            "expected open-path type error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn file_open_rejects_fd_literal_out_of_range() {
        let input = r#"
            open a file for reading called source at -1.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("File descriptor out of range")),
            "expected fd-range error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn file_open_accepts_string_path_and_fd_literal() {
        let input = r#"
            open a file for reading called source at "./data.txt".
            open a file for writing called output at 1.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .all(|e| !e.message.contains("Open path must be either a text path") && !e.message.contains("File descriptor out of range")),
            "unexpected open-path errors: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn treating_rejects_mismatched_match_and_replacement_types() {
        let input = r#"
            print each filename from arguments's all treating "-" as 0.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Treating match and replacement must be the same type")),
            "expected treating type mismatch error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn treating_allows_same_type_substitution() {
        let input = r#"
            print each filename from arguments's all treating "-" as "/dev/stdin".
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .all(|e| !e.message.contains("Treating match and replacement must be the same type")),
            "unexpected treating type mismatch error(s): {:?}",
            analyzer.errors
        );
    }