import pytest from typing import Any from compiler.parser import parse from compiler.tokenizer import tokenize from compiler.interpreter import interpret, SymTab, Unit def test_interpreter_addition() -> None: exp = '2 + 3' tokens = tokenize(exp) ast = parse(tokens) assert interpret(ast.block) == 5 def test_interpreter_boolean_coercion() -> None: exp = '1 + (2 < 3)' tokens = tokenize(exp) ast = parse(tokens) assert interpret(ast.block) == 2 def test_interpreter_nested_blocks() -> None: code = ''' { var x = 5; { var y = 10; x + y } } ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 15 def test_interpreter_variable_shadowing() -> None: code = ''' var x = 1; { var x = 2; x } x ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 1 def test_interpreter_assignment_in_outer_scope() -> None: code = ''' var x = 5; { x = x + 1; x } ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 6 def test_interpreter_assignment_operator() -> None: code = 'var x = 0; x = 5; x' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 5 def test_interpreter_operator_precedence() -> None: code = '10 - 4 - 3' # (10-4)-3 = 3 tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 3 def test_interpreter_logical_operators() -> None: code = 'true and false or true' # (true and false) or true = true tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is True def test_interpreter_comparison_equality() -> None: code = '5 > 3 == true' # (5>3) == true → true tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is True def test_interpreter_unary_operators() -> None: code = 'not (5 < 3)' # not(false) → true tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is True def test_interpreter_block_scoping() -> None: code = ''' { var a = 10; { var b = 20; a + b } } ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 30 def test_interpreter_variable_redeclaration() -> None: code = ''' { var x = 1; var x = 2; x } ''' tokens = tokenize(code) ast = parse(tokens) with pytest.raises(Exception): interpret(ast.block) def test_interpreter_while_loop() -> None: code = ''' var counter = 3; while counter > 0 do { counter = counter - 1 }; counter ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 0 def test_interpreter_empty_block() -> None: code = '{}' tokens = tokenize(code) ast = parse(tokens) assert isinstance(interpret(ast.block), Unit) def test_interpreter_block_last_semicolon() -> None: code = ''' { var x = 5; x; } ''' tokens = tokenize(code) ast = parse(tokens) assert isinstance(interpret(ast.block), Unit) def test_interpreter_if_without_else_returns_unit() -> None: code = 'if false then 5' tokens = tokenize(code) ast = parse(tokens) assert isinstance(interpret(ast.block), Unit) def test_interpreter_if_else_true_branch() -> None: code = 'if true then 10 else 20' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 10 def test_interpreter_if_else_false_branch() -> None: code = 'if false then 10 else 20' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 20 def test_interpreter_function_call_raises_error(capsys: pytest.CaptureFixture) -> None: code = 'print_int(5)' tokens = tokenize(code) ast = parse(tokens) assert isinstance(interpret(ast.block), Unit) captured = capsys.readouterr() assert captured.out == '5\n' def test_interpreter_division_modulo_operators() -> None: code = '8 / 3 + 8 % 3' # 2 + 2 = 4 tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 4 def test_interpreter_assignment_to_non_identifier_raises() -> None: code = '5 = 10' tokens = tokenize(code) ast = parse(tokens) with pytest.raises(Exception): interpret(ast.block) def test_interpreter_while_loop_zero_iterations() -> None: code = ''' var x = 3; while x > 5 do { x = x - 1 }; x ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 3 def test_interpreter_nested_if_expression() -> None: code = '(if true then 5 else 3) + 2' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 7 def test_interpreter_unary_not_on_boolean() -> None: code = 'not true' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is False def test_interpreter_unary_negate_non_int_raises() -> None: code = '-true' tokens = tokenize(code) ast = parse(tokens) with pytest.raises(Exception): interpret(ast.block) def test_interpreter_and_short_circuit() -> None: code = ''' var evaluated = false; false and { evaluated = true; true }; evaluated ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is False def test_interpreter_and_evaluates_both() -> None: code = ''' var evaluated = false; true and { evaluated = true; true }; evaluated ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is True def test_interpreter_or_short_circuit() -> None: code = ''' var evaluated = false; true or { evaluated = true; true }; evaluated ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is False def test_interpreter_or_evaluates_both() -> None: code = ''' var evaluated = false; false or { evaluated = true; true }; evaluated ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is True def test_interpreter_unary_negate_int() -> None: code = '-5' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == -5 def test_interpreter_unary_negate_bool_raises() -> None: code = '-true' tokens = tokenize(code) ast = parse(tokens) with pytest.raises(Exception): interpret(ast.block) def test_interpreter_unary_not_on_int_raises() -> None: code = 'not 5' tokens = tokenize(code) ast = parse(tokens) with pytest.raises(Exception): interpret(ast.block) def test_interpreter_binary_operator_precedence() -> None: code = '1 + 2 * 3 == 7' # 1 + (2 * 3) == 7 → true tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is True def test_interpreter_binary_operator_associativity() -> None: code = '10 - 4 - 3' # (10 - 4) - 3 = 3 tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 3 def test_interpreter_binary_operator_equality() -> None: code = '5 == 5 and 3 != 4' # true and true → true tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is True def test_interpreter_binary_operator_comparison() -> None: code = '5 < 10 and 10 >= 10' # true and true → true tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) is True def test_interpreter_binary_operator_division_by_zero_raises() -> None: code = '5 / 0' tokens = tokenize(code) ast = parse(tokens) with pytest.raises(Exception): interpret(ast.block) def test_interpreter_binary_operator_modulo_by_zero_raises() -> None: code = '5 % 0' tokens = tokenize(code) ast = parse(tokens) with pytest.raises(Exception): interpret(ast.block) def test_interpreter_while_loop_multiple_iterations() -> None: code = ''' var x = 3; while x > 0 do { x = x - 1 }; x ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 0 def test_interpreter_block_with_multiple_statements() -> None: code = ''' { var x = 5; var y = 10; x + y } ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 15 def test_interpreter_block_with_trailing_semicolon() -> None: code = ''' { var x = 5; x; } ''' tokens = tokenize(code) ast = parse(tokens) assert isinstance(interpret(ast.block), Unit) def test_interpreter_block_with_nested_blocks() -> None: code = ''' { var x = 5; { var y = 10; x + y } } ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 15 def test_interpreter_variable_declaration_in_block() -> None: code = ''' { var x = 5; x } ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 5 def test_interpreter_variable_redeclaration_in_block_raises() -> None: code = ''' { var x = 5; var x = 10; x } ''' tokens = tokenize(code) ast = parse(tokens) with pytest.raises(Exception): interpret(ast.block) def test_interpreter_assignment_in_block() -> None: code = ''' { var x = 5; x = x + 1; x } ''' tokens = tokenize(code) ast = parse(tokens) assert interpret(ast.block) == 6 def test_interpreter_function_call_print_int() -> None: code = 'print_int(5)' tokens = tokenize(code) ast = parse(tokens) assert isinstance(interpret(ast.block), Unit) def test_interpreter_function_call_print_bool() -> None: code = 'print_bool(true)' tokens = tokenize(code) ast = parse(tokens) assert isinstance(interpret(ast.block), Unit) def test_interpreter_function_call_read_int() -> None: code = 'read_int()' tokens = tokenize(code) ast = parse(tokens) # Mock input for read_int import builtins original_input: Any = builtins.input func: Any = lambda: '42' builtins.input = func assert interpret(ast.block) == 42 builtins.input = original_input