def test_lookup_deeply_nested_var(): """TEST 4.4: Extending overwrites old bindings to the same variable name.""" env = Environment({"a": 1}).extend({"b": 2}).extend({"c": 3}). \ extend({"foo": 100}) assert_equals(100, env.lookup("foo"))
def test_lookup_from_inner_env(): """The `extend` function returns a new environment extended with more bindings.""" env = Environment({"foo": 42}) env = env.extend({"bar": True}) assert_equals(42, env.lookup("foo")) assert_equals(True, env.lookup("bar"))
def test_calling_atom_raises_exception(): """TEST 5.15: A function call to a non-function should result in an error.""" with assert_raises_regexp(DiyLangError, "not a function"): evaluate(parse("(#t 'foo 'bar)"), Environment()) with assert_raises_regexp(DiyLangError, "not a function"): evaluate(parse("(42)"), Environment())
def test_simple_lookup(): """TEST 4.1: An environment should store variables and provide lookup. Tip: Implement this in the lookup() method in types.py. """ env = Environment({"var": 42}) assert_equals(42, env.lookup("var"))
def test_define_with_nonsymbol_as_variable(): """TEST 4.13: Defines should evaluate the argument before storing it in the environment. """ env = Environment() evaluate(parse("(define x (+ 1 41))"), env) assert_equals(42, env.lookup("x"))
def test_extend_returns_new_environment(): """TEST 4.5: The extend method should create a new environment, leaving the old one unchanged.""" env = Environment({"foo": 1}) extended = env.extend({"foo": 2}) assert_equals(1, env.lookup("foo")) assert_equals(2, extended.lookup("foo"))
def test_define_should_evaluate_the_argument(): """TEST 4.13: Defines should evaluate the argument before storing it in the environment. """ env = Environment() evaluate(parse("(define x (+ 1 41))"), env) assert_equals(42, env.lookup("x"))
def test_extend_returns_new_environment(): """The extend method should create a new environment, leaving the old one unchanged.""" env = Environment({"foo": 1}) extended = env.extend({"foo": 2}) assert_equals(1, env.lookup("foo")) assert_equals(2, extended.lookup("foo"))
def test_lambda_arguments_are_lists(): """TEST 5.4: The parameters of a `lambda` should be a list.""" closure = evaluate(parse("(lambda (x y) (+ x y))"), Environment()) assert_true(is_list(closure.params)) with assert_raises(DiyLangError): evaluate(parse("(lambda not-a-list (body of fn))"), Environment())
def test_getting_tail_of_list(): """TEST 6.7: `tail` returns the tail of the list. The tail is the list retained after removing the first element. """ assert_equals([2, 3], evaluate(parse("(tail '(1 2 3))"), Environment())) assert_equals([], evaluate(parse("(tail '(1))"), Environment()))
def test_creating_list_with_cons_does_not_modify_initial_list(): """TEST 6.2.1: The `cons` functions prepends an element to the front of a list without modifying the intial list.""" env = Environment({"initial_list": [1, 2, 3]}) result = evaluate(parse("(cons 0 initial_list)"), env) assert_equals(parse("(0 1 2 3)"), result) assert_equals([1, 2, 3], env.lookup("initial_list"))
def test_lookup_on_missing_raises_exception(): """When looking up an undefined symbol, an error should be raised. The error message should contain the relevant symbol, and inform that it has not been defined.""" with assert_raises_regexp(DiyLangError, "my-missing-var"): empty_env = Environment() empty_env.lookup("my-missing-var")
def test_evaluating_quote(): """TEST 2.3: When a call is done to the `quote` form, the argument should be returned without being evaluated. (quote foo) -> foo """ assert_equals("foo", evaluate(["quote", "foo"], Environment())) assert_equals([1, 2, False], evaluate(["quote", [1, 2, False]], Environment())) assert_equals([], evaluate(["quote", []], Environment()))
def test_redefine_variables_illegal(): """Variables can only be defined once. Setting a variable in an environment where it is already defined should result in an appropriate error. """ env = Environment({"foo": 1}) with assert_raises_regexp(DiyLangError, "already defined"): env.set("foo", 2)
def test_redefine_variables_illegal(): """TEST 4.7: Variables can only be defined once. Setting a variable in an environment where it is already defined should result in an appropriate error. """ env = Environment({"foo": 1}) with assert_raises_regexp(DiyLangError, "already defined"): env.set("foo", 2)
def test_lookup_on_missing_raises_exception(): """TEST 4.2: When looking up an undefined symbol, an error should be raised. The error message should contain the relevant symbol, and inform that it has not been defined. """ with assert_raises_regexp(DiyLangError, "my-missing-var"): empty_env = Environment() empty_env.lookup("my-missing-var")
def test_lookup_from_inner_env(): """TEST 4.3: The `extend` function returns a new environment extended with more bindings. Tip: The Dictionary class has a convenient .update method. """ env = Environment({"foo": 42}) env = env.extend({"bar": True}) assert_equals(42, env.lookup("foo")) assert_equals(True, env.lookup("bar"))
def test_make_sure_arguments_are_evaluated_in_correct_environment(): """Test 5.19: Function arguments should be evaluated in correct environment Function arguments should be evaluated in the environment where the function is called, and not in the environment captured by the function. """ env = Environment({'x': 3}) evaluate(parse("(define foo (lambda (x) x))"), env) env = env.extend({'x': 4}) assert_equals(evaluate(parse("(foo (+ x 1))"), env), 5)
def test_basic_if_statement(): """TEST 3.2: If statements are the basic control structures. The `if` should first evaluate its first argument. If this evaluates to true, then the second argument is evaluated and returned. Otherwise the third and last argument is evaluated and returned instead. """ assert_equals(42, evaluate(parse("(if #t 42 1000)"), Environment())) assert_equals(1000, evaluate(parse("(if #f 42 1000)"), Environment())) assert_equals(True, evaluate(parse("(if #t #t #f)"), Environment()))
def test_math_operators_only_work_on_numbers(): """TEST 2.7: The math functions should only allow numbers as arguments.""" with assert_raises(DiyLangError): evaluate(parse("(+ 1 'foo)"), Environment()) with assert_raises(DiyLangError): evaluate(parse("(- 1 'foo)"), Environment()) with assert_raises(DiyLangError): evaluate(parse("(/ 1 'foo)"), Environment()) with assert_raises(DiyLangError): evaluate(parse("(mod 1 'foo)"), Environment())
def test_make_sure_arguments_are_evaluated_in_correct_environment(): """Test 5.19: Function arguments should be evaluated in correct environment Function arguments should be evaluated in the environment where the function is called, and not in the environment captured by the function. """ env = Environment({'x': 3}) res = evaluate(parse("(define foo (lambda (x) x))"), env) env = env.extend({'x': 4}) assert_equals(evaluate(parse("(foo (+ x 1))"), env), 5)
def test_define(): """Test of simple define statement. The `define` form is used to define new bindings in the environment. A `define` call should result in a change in the environment. What you return from evaluating the definition is not important (although it affects what is printed in the REPL).""" env = Environment() evaluate(parse("(define x 1000)"), env) assert_equals(1000, env.lookup("x"))
def test_define(): """TEST 4.10: Test of simple define statement. The `define` form is used to define new bindings in the environment. A `define` call should result in a change in the environment. What you return from evaluating the definition is not important (although it affects what is printed in the REPL). """ env = Environment() evaluate(parse("(define x 1000)"), env) assert_equals(1000, env.lookup("x"))
def test_evaluating_atom_function(): """TEST 2.4: The `atom` form is used to determine whether an expression is an atom. Atoms are expressions that are not list, i.e. integers, booleans or symbols. Remember that the argument to `atom` must be evaluated before the check is done. """ assert_equals(True, evaluate(["atom", True], Environment())) assert_equals(True, evaluate(["atom", False], Environment())) assert_equals(True, evaluate(["atom", 42], Environment())) assert_equals(True, evaluate(["atom", ["quote", "foo"]], Environment())) assert_equals(False, evaluate(["atom", ["quote", [1, 2]]], Environment()))
def test_evaluating_call_to_closure(): """TEST 5.7: The first case we'll handle is when the AST is a list with an actual closure as the first element. In this first test, we'll start with a closure with no arguments and no free variables. All we need to do is to evaluate and return the function body. """ closure = evaluate(parse("(lambda () (+ 1 2))"), Environment()) ast = [closure] result = evaluate(ast, Environment()) assert_equals(3, result)
def test_define_with_wrong_number_of_arguments(): """TEST 4.11: Defines should have exactly two arguments, or raise an error. This type of check could benefit the other forms we implement as well, and you might want to add them elsewhere. It quickly get tiresome to test for this however, so the tests won't require you to. """ with assert_raises_regexp(DiyLangError, "Wrong number of arguments"): evaluate(parse("(define x)"), Environment()) with assert_raises_regexp(DiyLangError, "Wrong number of arguments"): evaluate(parse("(define x 1 2)"), Environment())
def test_calling_very_simple_function_in_environment(): """A call to a symbol corresponds to a call to its value in the environment. When a symbol is the first element of the AST list, it is resolved to its value in the environment (which should be a function closure). An AST with the variables replaced with its value should then be evaluated instead.""" env = Environment() evaluate(parse("(define add (lambda (x y) (+ x y)))"), env) assert_is_instance(env.lookup("add"), Closure) result = evaluate(parse("(add 1 2)"), env) assert_equals(3, result)
def test_evaluating_call_to_closure_with_free_variables(): """TEST 5.11: The body should be evaluated in the environment from the closure. The function's free variables, i.e. those not specified as part of the parameter list, should be looked up in the environment from where the function was defined. This is the environment included in the closure. Make sure this environment is used when evaluating the body. """ closure = evaluate(parse("(lambda (x) (+ x y))"), Environment({"y": 1})) ast = [closure, 0] result = evaluate(ast, Environment({"y": 2})) assert_equals(1, result)
def test_calling_very_simple_function_in_environment(): """TEST 5.12: A call to a symbol corresponds to a call to its value in the environment. When a symbol is the first element of the AST list, it is resolved to its value in the environment (which should be a function closure). An AST with the variables replaced with its value should then be evaluated instead. """ env = Environment() evaluate(parse("(define add (lambda (x y) (+ x y)))"), env) assert_is_instance(env.lookup("add"), Closure) result = evaluate(parse("(add 1 2)"), env) assert_equals(3, result)
def test_evaluating_eq_function(): """TEST 2.5: The `eq` form is used to check whether two expressions are the same atom.""" assert_equals(True, evaluate(["eq", 1, 1], Environment())) assert_equals(False, evaluate(["eq", 1, 2], Environment())) # From this point, the ASTs might sometimes be too long or cummbersome to # write down explicitly, and we'll use `parse` to make them for us. # Remember, if you need to have a look at exactly what is passed to `evaluate`, # just add a print statement in the test (or in `evaluate`). assert_equals(True, evaluate(parse("(eq 'foo 'foo)"), Environment())) assert_equals(False, evaluate(parse("(eq 'foo 'bar)"), Environment())) # Lists are never equal, because lists are not atoms assert_equals(False, evaluate(parse("(eq '(1 2 3) '(1 2 3))"), Environment()))
def test_lambda_closure_holds_function(): """TEST 5.3: The closure contains the parameter list and function body too.""" closure = evaluate(parse("(lambda (x y) (+ x y))"), Environment()) assert_equals(["x", "y"], closure.params) assert_equals(["+", "x", "y"], closure.body)
def test_lookup_missing_variable(): """TEST 4.9: Referencing undefined variables should raise an appropriate exception. This test should already be working if you implemented the environment correctly. """ with assert_raises_regexp(DiyLangError, "my-var"): evaluate("my-var", Environment())
def test_calling_with_wrong_number_of_arguments(): """TEST 5.17: Functions should raise exceptions when called with wrong number of arguments.""" env = Environment() evaluate(parse("(define fn (lambda (p1 p2) 'whatever))"), env) error_msg = "wrong number of arguments, expected 2 got 3" with assert_raises_regexp(DiyLangError, error_msg): evaluate(parse("(fn 1 2 3)"), env)
def test_variable_lookup_after_define(): """TEST 4.14: Test define and lookup variable in same environment. This test should already be working when the above ones are passing. """ env = Environment() evaluate(parse("(define foo (+ 2 2))"), env) assert_equals(4, evaluate("foo", env))
def test_lambda_evaluates_to_closure(): """TEST 5.1: The lambda form should evaluate to a Closure Tip: You'll find the Closure class ready in types.py, just finish the constructor """ ast = ["lambda", [], 42] closure = evaluate(ast, Environment()) assert_is_instance(closure, Closure)
def test_evaluating_symbol(): """TEST 4.8: Symbols (other than #t and #f) are treated as variable references. When evaluating a symbol, the corresponding value should be looked up in the environment. """ env = Environment({"foo": 42}) assert_equals(42, evaluate("foo", env))
def test_if_with_sub_expressions(): """TEST 3.4: A final test with a more complex if expression. This test should already be passing if the above ones are.""" ast = parse(""" (if (> 1 2) (- 1000 1) (+ 40 (- 3 1))) """) assert_equals(42, evaluate(ast, Environment()))
def test_lookup_deeply_nested_var(): """Extending overwrites old bindings to the same variable name.""" env = Environment({"a": 1}).extend({"b": 2}).extend({"c": 3}).extend({"foo": 100}) assert_equals(100, env.lookup("foo"))
def test_set_changes_environment_in_place(): """When calling `set` the environment should be updated""" env = Environment() env.set("foo", 2) assert_equals(2, env.lookup("foo"))
def test_simple_lookup(): """An environment should store variables and provide lookup.""" env = Environment({"var": 42}) assert_equals(42, env.lookup("var"))