vox-lang 0.4.5

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 variable_declared_under_same_guard_is_available_under_same_guard_later() {
        let input = r#"
            if "number lines" then,
                a number called 'line number' is 1.

            if "number lines" then,
                Print "{line number:6}".
        "#;

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

    #[test]
    fn variable_declared_under_different_guard_is_not_available() {
        let input = r#"
            if "number lines" then,
                a number called 'line number' is 1.

            if "verbose" then,
                Print "{line number:6}".
        "#;

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

    #[test]
    fn guarded_variable_is_available_in_nested_while_for_repeat_blocks() {
        let input = r#"
            if "number lines" then,
                a number called 'line number' is 1.

            if "number lines" then,
                while true,
                    for each item in arguments's all,
                        repeat 1 times,
                            Print "{line number:6}".
        "#;

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

    #[test]
    fn variable_declared_under_same_and_condition_is_available() {
        let input = r#"
            if "number lines" and "verbose" then,
                a number called 'line number' is 1.

            if "number lines" and "verbose" then,
                Print "{line number:6}".
        "#;

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

    #[test]
    fn variable_declared_under_same_not_condition_is_available() {
        let input = r#"
            if not "number lines" then,
                a number called 'line number' is 1.

            if not "number lines" then,
                Print "{line number:6}".
        "#;

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

    #[test]
    fn unknown_variable_inside_function_is_reported() {
        let input = r#"
            To 'show',
                Print "{missing}".

            'show'.
        "#;

        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 top_level_global_variable_is_available_inside_function() {
        let input = r#"
            A text called 'Program Version' is "0.1.3".

            To 'show',
                Print "{Program Version}".

            'show'.
        "#;

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

    #[test]
    fn function_local_variable_is_not_available_at_top_level() {
        let input = r#"
            To 'make',
                a number called temp is 1.

            Print "{temp}".
        "#;

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

    #[test]
    fn branch_local_identifier_named_like_keyword_is_not_false_positive() {
        let input = r#"
            If arguments's count is greater than 1 then,
                a text called arg1 is arguments's first,
                Print the arg1.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .all(|e| !e.message.contains("Unknown identifier 'arg1'") && !e.message.contains("Unknown variable: arg1")),
            "unexpected arg1 errors: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn flag_schema_after_non_schema_code_is_allowed() {
        let input = r#"
            Print "hello".
            a flag called verbose is "-v" or "--verbose", it is a boolean.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer.errors.is_empty(),
            "expected no errors for schema after non-schema code, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn flag_schema_after_explicit_parse_is_rejected() {
        let input = r#"
            a flag called verbose is "-v" or "--verbose", it is a boolean.
            parse flags.
            a flag called debug is "-d" or "--debug", it is a boolean.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Cannot declare new flags after 'parse flags.'")),
            "expected post-parse schema error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn duplicate_parse_flags_statement_is_rejected() {
        let input = r#"
            a flag called verbose is "-v" or "--verbose", it is a boolean.
            parse flags.
            parse flags.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Duplicate 'parse flags.' statement")),
            "expected duplicate-parse error, got: {:?}",
            analyzer.errors
        );
    }

    #[test]
    fn flag_usage_before_explicit_parse_is_rejected() {
        let input = r#"
            a flag called verbose is "-v" or "--verbose", it is a boolean.
            Print "{verbose}".
            parse flags.
        "#;

        let analyzer = analyze_input(input);
        assert!(
            analyzer
                .errors
                .iter()
                .any(|e| e.message.contains("Flag variable 'verbose' is used before flags are parsed")),
            "expected pre-parse usage error, got: {:?}",
            analyzer.errors
        );
    }