---
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
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
|
||||
Reference in New Issue
Block a user