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())
Example #2
0
def test_getting_tail_of_list():
    """`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()))
Example #3
0
def test_calling_atom_raises_exception():
    """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())
Example #4
0
def test_parsing_unclosed_strings():
    """
    Strings that are not closed result in an parse error.
    """

    with assert_raises_regexp(DiyLangError, 'Unclosed string'):
        parse('"hey, close me!')
Example #5
0
def test_parsing_unclosed_strings():
    """
    Strings that are not closed result in an parse error.
    """

    with assert_raises_regexp(DiyLangError, 'Unclosed string'):
        parse('"hey, close me!')
Example #6
0
def test_parse_boolean():
    """Parsing single booleans.

    Booleans are the special symbols #t and #f. In the ASTs they are represented
    by Pythons True and False, respectively. """

    assert_equals(True, parse('#t'))
    assert_equals(False, parse('#f'))
Example #7
0
def test_calling_with_wrong_number_of_arguments():
    """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)
Example #8
0
def test_parse_exception_extra_paren():
    """TEST 1.8: Another exception is raised if the expression is too large.

    The parse function expects to receive only one single expression. Anything
    more than this, should result in the proper exception."""

    with assert_raises_regexp(DiyLangError, 'Expected EOF'):
        parse('(foo (bar x y)))')
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())
Example #10
0
def test_parse_boolean():
    """TEST 1.2: Parsing single booleans.

    Booleans are the special symbols #t and #f. In the ASTs they are
    represented by Python's True and False, respectively."""

    assert_equals(True, parse('#t'))
    assert_equals(False, parse('#f'))
Example #11
0
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_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)
Example #13
0
def test_lambda_arguments_are_lists():
    """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())
Example #14
0
def test_parse_exception_extra_paren():
    """Another exception is raised if the expression is too large.

    The parse function expects to receive only one single expression. Anything
    more than this, should result in the proper exception."""

    with assert_raises_regexp(DiyLangError, 'Expected EOF'):
        parse('(foo (bar x y)))')
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"))
Example #16
0
def test_creating_longer_lists_with_only_cons():
    """`cons` needs to evaluate it's arguments.

    Like all the other special forms and functions in our language, `cons` is
    call-by-value. This means that the arguments must be evaluated before we
    create the list with their values."""

    result = evaluate(parse("(cons 3 (cons (- 4 2) (cons 1 '())))"), Environment())
    assert_equals(parse("(3 2 1)"), result)
Example #17
0
def test_parsing_strings_are_closed_by_first_closing_quotes():
    """
    Strings are delimited by the first and last (unescaped) double quotes.

    Thus, unescaped quotes followed by anything at all should be considered
    invalid and throw an exception.
    """

    with assert_raises_regexp(DiyLangError, 'Expected EOF'):
        parse('"foo" bar"')
Example #18
0
def test_parsing_strings_are_closed_by_first_closing_quotes():
    """
    Strings are delimited by the first and last (unescaped) double quotes.

    Thus, unescaped quotes followed by anything at all should be considered
    invalid and throw an exception.
    """

    with assert_raises_regexp(DiyLangError, 'Expected EOF'):
        parse('"foo" bar"')
Example #19
0
def test_parse_integer():
    """TEST 1.3: Parsing single integer.

    Integers are represented in the ASTs as Python ints.

    Tip: String objects have a handy .isdigit() method.
    """

    assert_equals(42, parse('42'))
    assert_equals(1337, parse('1337'))
Example #20
0
def test_basic_if_statement():
    """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()))
Example #21
0
def test_parse_integer():
    """Parsing single integer.

    Integers are represented in the ASTs as Python ints.

    Tip: String objects have a handy .isdigit() method.
    """

    assert_equals(42, parse('42'))
    assert_equals(1337, parse('1337'))
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)
Example #24
0
def test_creating_longer_lists_with_only_cons():
    """TEST 6.3: `cons` needs to evaluate it's arguments.

    Like all the other special forms and functions in our language, `cons` is
    call-by-value. This means that the arguments must be evaluated before we
    create the list with their values.
    """

    result = evaluate(parse("(cons 3 (cons (- 4 2) (cons 1 '())))"),
                      Environment())
    assert_equals(parse("(3 2 1)"), result)
Example #25
0
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_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()))
Example #27
0
def test_call_to_function_should_evaluate_arguments():
    """Call to function should evaluate all arguments.

    When a function is applied, the arguments should be evaluated before being bound
    to the parameter names."""

    env = Environment()
    closure = evaluate(parse("(lambda (a) (+ a 5))"), env)
    ast = [closure, parse("(if #f 0 (+ 10 10))")]

    assert_equals(25, evaluate(ast, env))
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)
Example #29
0
def test_parse_list_of_symbols():
    """Parsing list of only symbols.

    A list is represented by a number of elements surrounded by parens. Python lists
    are used to represent lists as ASTs.

    Tip: The useful helper function `find_matching_paren` is already provided in
    `parse.py`.
    """

    assert_equals(['foo', 'bar', 'baz'], parse('(foo bar baz)'))
    assert_equals([], parse('()'))
Example #30
0
def test_parse_integer():
    """TEST 1.3: Parsing single integer.

    Integers are represented in the ASTs as Python ints.

    Tip: String objects have a handy .isdigit() method.
    """

    assert_equals(42, parse('42'))
    assert_equals(1337, parse('1337'))
    assert_true(is_integer(parse('42')),
        "Numbers should be represented as integers in the AST")
Example #31
0
def test_parse_list_of_symbols():
    """TEST 1.4: Parsing list of only symbols.

    A list is represented by a number of elements surrounded by parens. Python
    lists are used to represent lists as ASTs.

    Tip: The useful helper function `find_matching_paren` is already provided
    in `parse.py`.
    """

    assert_equals(['foo', 'bar', 'baz'], parse('(foo bar baz)'))
    assert_equals([], parse('()'))
Example #32
0
def test_define_with_wrong_number_of_arguments():
    """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_call_to_function_should_evaluate_arguments():
    """TEST 5.10: Call to function should evaluate all arguments.

    When a function is applied, the arguments should be evaluated before being bound
    to the parameter names.
    """

    env = Environment()
    closure = evaluate(parse("(lambda (a) (+ a 5))"), env)
    ast = [closure, parse("(if #f 0 (+ 10 10))")]

    assert_equals(25, evaluate(ast, env))
Example #34
0
def test_creating_lists_by_quoting():
    """One way to create lists is by quoting.

    We have already implemented `quote` so this test should already be
    passing.

    The reason we need to use `quote` here is that otherwise the expression would
    be seen as a call to the first element -- `1` in this case, which obviously isn't
    even a function."""

    assert_equals(parse("(1 2 3 #t)"),
                  evaluate(parse("'(1 2 3 #t)"), Environment()))
Example #35
0
def test_creating_lists_by_quoting():
    """TEST 6.1: One way to create lists is by quoting.

    We have already implemented `quote` so this test should already be
    passing.

    The reason we need to use `quote` here is that otherwise the expression
    would be seen as a call to the first element -- `1` in this case, which
    obviously isn't even a function.
    """

    assert_equals(parse("(1 2 3 #t)"),
                  evaluate(parse("'(1 2 3 #t)"), Environment()))
Example #36
0
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)
Example #37
0
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():
    """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)
Example #39
0
def test_parse_list_of_mixed_types():
    """TEST 1.5: Parsing a list containing different types.

    When parsing lists, make sure each of the sub-expressions are also parsed
    properly."""

    assert_equals(['foo', True, 123], parse('(foo #t 123)'))
Example #40
0
def test_lambda_closure_holds_function():
    """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)
Example #41
0
def test_calling_function_recursively():
    """Tests that a named function is included in the environment
    where it is evaluated."""

    env = Environment()
    evaluate(parse("""
        (define my-fn
            ;; A meaningless, but recursive, function
            (lambda (x)
                (if (eq x 0)
                    42
                    (my-fn (- x 1)))))
    """), env)

    assert_equals(42, evaluate(parse("(my-fn 0)"), env))
    assert_equals(42, evaluate(parse("(my-fn 10)"), env))
Example #42
0
def test_parse_single_symbol():
    """TEST 1.1: Parsing a single symbol.

    Symbols are represented by text strings. Parsing a single atom should
    result in an AST consisting of only that symbol."""

    assert_equals('foo', parse('foo'))
Example #43
0
def test_parse_single_symbol():
    """Parsing a single symbol.

    Symbols are represented by text strings. Parsing a single atom should result
    in an AST consisting of only that symbol."""

    assert_equals('foo', parse('foo'))
Example #44
0
def test_parse_list_of_mixed_types():
    """Parsing a list containing different types.

    When parsing lists, make sure each of the sub-expressions are also parsed
    properly."""

    assert_equals(['foo', True, 123], parse('(foo #t 123)'))
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)
Example #46
0
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()))
Example #47
0
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"))
Example #48
0
def test_variable_lookup_after_define():
    """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))
Example #49
0
def test_parse_on_nested_list():
    """Parsing should also handle nested lists properly."""

    program = '(foo (bar ((#t)) x) (baz y))'
    ast = ['foo',
           ['bar', [[True]], 'x'],
           ['baz', 'y']]
    assert_equals(ast, parse(program))
def test_calling_function_recursively():
    """TEST 5.20: Tests that a named function is included in the environment where
    it is evaluated.
    """

    env = Environment()
    evaluate(parse("""
        (define my-fn
            ;; A meaningless, but recursive, function
            (lambda (x)
                (if (eq x 0)
                    42
                    (my-fn (- x 1)))))
    """), env)

    assert_equals(42, evaluate(parse("(my-fn 0)"), env))
    assert_equals(42, evaluate(parse("(my-fn 10)"), env))
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"))
Example #52
0
def test_parsing_strings_with_escaped_double_quotes():
    """
    We should be able to create strings with "-characters by escaping them.
    """

    ast = parse('"Say \\"what\\" one more time!"')

    assert_is_instance(ast, String)
    assert_equals('Say \\"what\\" one more time!', ast.val)
Example #53
0
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))
Example #54
0
def test_parse_with_extra_whitespace():
    """Excess whitespace should be removed."""

    program = """

       (program    with   much        whitespace)
    """
    expected_ast = ['program', 'with', 'much', 'whitespace']
    assert_equals(expected_ast, parse(program))
Example #55
0
def test_calling_lambda_directly():
    """It should be possible to define and call functions directly.

    A lambda definition in the call position of an AST should be evaluated, and then
    evaluated as before."""

    ast = parse("((lambda (x) x) 42)")
    result = evaluate(ast, Environment())
    assert_equals(42, 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 cumbersome 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()))
Example #57
0
def test_parsing_strings_with_escaped_double_quotes():
    """
    We should be able to create strings with "-characters by escaping them.
    """

    ast = parse('"Say \\"what\\" one more time!"')

    assert_is_instance(ast, String)
    assert_equals('Say \\"what\\" one more time!', ast.val)
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_calling_lambda_directly():
    """TEST 5.13: It should be possible to define and call functions directly.

    A lambda definition in the call position of an AST should be evaluated, and then
    evaluated as before.
    """

    ast = parse("((lambda (x) x) 42)")
    result = evaluate(ast, Environment())
    assert_equals(42, result)