scheme_grader.py (plain text)


"""Automatic grading script for the Scheme project.

Expects the following files in the current directory.

scheme.py
scheme_reader.py
scheme_primitives.py, scheme_tokens.py, scheme_test.py
buffer.py, ucb.py
autograder.py
"""

__version__ = '1.4'

from autograder import test, run_tests, check_func, check_doctest, test_eval

try:
    import scheme, scheme_reader
except (SyntaxError, IndentationError) as e:
    import traceback
    print('Unfortunately, the autograder cannot run because ' +
          'your program contains a syntax error:')
    traceback.print_exc(limit=1)
    exit(1)

import sys
from ucb import main
import scheme_primitives
from scheme import read_line, Pair, nil, LambdaProcedure, create_global_frame

@test('1')
def problem_1(grades):
    tests1 = [
        ('3', 3),
        ('-123', -123),
        ('1.25', 1.25),
        ('true', True),
        (')', 'Error'),
        ("'x", pairify(['quote', 'x'])),
        ('(quote x)', pairify(['quote', 'x'])),
        ("'(a b)", pairify(['quote', ['a', 'b']])),
        ("'(a (b c))", pairify(['quote', ['a', ['b', 'c']]])),
        ("(a (b 'c))", pairify(['a', ['b', ['quote', 'c']]])),
        ("(a (b '(c d)))", pairify(['a', ['b', ['quote', ['c', 'd']]]])),
        ("')", 'Error')
    ]
    if check_func(catch_syntax_error(read_line), tests1, comp=scheme_equal):
        return True

@test('2')
def problem_2(grades):
    tests1 = [
        ('(a . b)', Pair('a', 'b')),
        ('(a)', Pair('a', nil)),
        ('(a b . c)', Pair('a', Pair('b', 'c')))
    ]
    tests2 = [
        ('(a b . c d)', 'Error'),
        ('((a b) (c (d (e f))))',
            pairify([['a', 'b'], ['c', ['d', ['e', 'f']]]])),
        ('(a . (b . (c . (d . ()))))',
            pairify(['a', 'b', 'c', 'd'])),
        ('(. . 2)', 'Error'),
        ('(2 . 3 4 . 5)', 'Error'),
        ('(2 (3 . 4) 5)', Pair(2, Pair(Pair(3, 4), Pair(5, nil)))),
    ]
    if check_func(catch_syntax_error(read_line), tests1, comp=scheme_equal):
        return True
    if check_func(catch_syntax_error(read_line), tests2, comp=scheme_equal):
        return True

@test('3')
def problem_3(grades):
    return check_doctest('apply_primitive', scheme)


@test('4')
def problem_4(grades):
    tests1 = [
        ('(+ 2 3)', 5),
        ('(+ 2 3 4 5 6 7)', 27),
        ('(+ 2)', 2),
        ('(+)', 0),
        ('(* (+ 3 2) (+ 1 7))', 40),
        ('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))', 57),
    ]
    tests2 = [
        ('(odd? 13)', True),
        ('(car (list 1 2 3 4))', 1),
        ('hello', 'SchemeError'),
        ('(car car)', 'SchemeError'),
        ('(odd? 1 2 3)', 'SchemeError'),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True

@test('5')
@test('A5')
def problem_A5(grades):
    tests1 = [
        ('(define size 2) size', 2),
        ('(define size 2) (* 5 size)', 10),
        ('(define pi 3.14159) (define radius 10) (* pi (* radius radius))',
                                  314.159),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True

@test('6')
@test('B6')
def problem_B6(grades):
    tests1 = [
        ("(list 'a 'b)", pairify(['a', 'b'])),
        ("(define a 1) (list a 'b)", pairify([1, 'b'])),
        ("(car '(a b c))", 'a'),
        ("(car (car '((a))))", 'a'),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True

@test('7')
def problem_7(grades):
    tests1 = [
        ('(begin (+ 2 3) (+ 5 6))', 11),
        ('(begin (display 3) (newline) (+ 2 3))', 5),
    ]
    tests2 = [
        ('(begin (define x 3) x)', 3),
        ('(define 0 1)', 'SchemeError'),
        ("(begin 30 'hello)", 'hello'),
        ("(begin (define x 3) (cons x '(y z)))", pairify([3, 'y', 'z'])),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True

@test('8')
def problem_8(grades):
    tests1 = [
        ('(lambda (x y) (+ x y))',
         LambdaProcedure(pairify(['x', 'y']),
                         pairify(['+', 'x', 'y']),
                         create_global_frame()))
    ]
    tests2 = [
        ('(lambda (x) (+ x) (+ x x))',
         LambdaProcedure(pairify(['x']),
                         pairify(['begin', ['+', 'x'], ['+', 'x', 'x']]),
                         create_global_frame())),
        ('(begin (define x (lambda () 2)) x)',
         LambdaProcedure(nil,
                         2,
                         create_global_frame())),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True

@test('9')
@test('A9')
def problem_A9(grades):
    tests1 = [
        ('(begin (define (f x y) (+ x y)) f)',
            LambdaProcedure(pairify(['x', 'y']),
                            pairify(['+', 'x', 'y']),
                            create_global_frame())),
        ('(begin (define (f) (+ 2 2)) f)',
            LambdaProcedure(nil,
                            pairify(['+', 2, 2]),
                            create_global_frame())),
        ('(begin (define (f x) (* x x)) f)',
            LambdaProcedure(pairify(['x']),
                            pairify(['*', 'x', 'x']),
                            create_global_frame())),
        ('(begin (define (f x) 1 2) f)',
            LambdaProcedure(pairify(['x']),
                            pairify(['begin', 1, 2]),
                            create_global_frame())),
        ('(define (0 x) (* x x))', 'SchemeError'),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True

@test('10')
def problem_10(grades):
    gf = create_global_frame()

    formals, vals = read_line('(a b c)'), read_line('(1 2 3)')
    call_frame = gf.make_call_frame(formals, vals)
    doctest = [
        (set(call_frame.bindings), {'a', 'b', 'c'}),
        (len(call_frame.bindings), 3),
        (call_frame.bindings['a'], 1),
        (call_frame.bindings['b'], 2),
        (call_frame.bindings['c'], 3),
        (call_frame.parent, gf),
        ]
    if check_func(lambda x: x, doctest, comp=scheme_equal):
        return True

    formals = read_line('(a b c)')
    args = read_line('(2 #t a)')
    lf = gf.make_call_frame(formals, args)
    tests1 = [
        (lf.bindings['a'], 2),
        (lf.bindings['b'], True),
        (lf.bindings['c'], 'a'),
        (lf.parent, gf),
    ]
    if check_func(lambda x: x, tests1, comp=scheme_equal):
        return True

    formals = read_line('(a)')
    args = read_line('(seven)')
    lf2 = lf.make_call_frame(formals, args)
    tests2 = [
        (lf.bindings['a'], 2),
        (lf.parent, gf),
    ]
    if check_func(lambda x: x, tests2, comp=scheme_equal):
        return True

@test('11')
@test('B11')
def problem_B11(grades):
    # Note: Doesn't check well-formed but unrequired list matching.
    # E.g., (lambda (a . b) 2) and (lambda x 2)
    tests1 = [
        ('(lambda (x y z) x)',
            LambdaProcedure(pairify(['x', 'y', 'z']),
                            'x',
                            create_global_frame())),
        ('(lambda (0 y z) x)', 'SchemeError'),
        ('(lambda (x y nil) x)', 'SchemeError'),
        ('(lambda (x y (and z)) x)', 'SchemeError'),
        ('(lambda (x #t z) x)', 'SchemeError'),
    ]
    tests2 = [
        ("(lambda (h e l l o) 'world)", 'SchemeError'),
        ("(lambda (c s 6 1 a) 'yay)", 'SchemeError'),
        ]

    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True

@test('12')
def problem_12(grades):
    tests1 = [
        ('(define (square x) (* x x)) (square 21)',
            441),
        ('(define square (lambda (x) (* x x))) (square (square 21))',
            194481),
    ]
    tests2 = [
        ("""(define square (lambda (x) (* x x)))
                 (define (sum-of-squares x y)
                   (+ (square x) (square y)))
                 (sum-of-squares 3 4)""",
            25) ,
        ("""(define double (lambda (x) (* 2 x)))
                 (define compose (lambda (f g) (lambda (x) (f (g x)))))
                 (define apply-twice (lambda (f) (compose f f)))
                 ((apply-twice double) 5)""",
            20),
        ("""(define (outer x y)
                  (define (inner z x)
                    (list x y z))
                  (inner x 10))
                 (outer 1 2)""",
            pairify([10, 2, 1])),
        ("""(define (outer-func x y)
                  (define (inner z x)
                    (list x y z))
                  inner)
                 ((outer-func 1 2)  1 10)""",
            pairify([10, 2, 1])),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True

@test('13')
@test('A13')
def problem_A13(grades):
    tests1 = [
        ('(if #t 1 0)', 1),
        ('(if #f 1 0)', 0),
        ('(if 1 1 0)', 1),
        ("(if 'a 1 0)", 1),
        ('(if (cons 1 2) 1 0)', 1),
        ('(if #t 1)', 1),
        ('(if #f 1)', scheme.okay),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True

@test('14')
@test('B14')
def problem_B14(grades):
    tests1 = [
        ('(and)', True),
        ('(and 1 #f)', False),
        ('(and 2 1)', 1),
        ('(and #f 5)', False),
        ('(and 3 2 #f)', False),
        ('(and 3 2 1)', 1),
        ('(and 3 #f 5)', False),
    ]
    tests2 = [
        ('(or)', False),
        ('(or 1)', 1),
        ('(or #f)', False),
        ("(or 0 1 2 'a)", 0),
        ('(or #f #f)', False),
        ("(or 'a #f)", 'a'),
        ("(or (< 2 3) (> 2 3) 2 'a)", True),
        ('(or (< 2 3) 2)', True),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True

@test('15')
@test('A15')
def problem_A15(grades):
    tests1 = [
        ("""(cond ((> 2 3) 5)
                  ((> 2 4) 6)
                  ((< 2 5) 7)
                  (else 8))""",
           7),
        ("""(cond ((> 2 3) 5)
                  ((> 2 4) 6)
                  ((< 2 5) 7))""",
           7),
        ("""(cond ((> 2 3) 5)
                  ((> 2 4) 6)
                  (else 8))""",
           8),
        ("""(cond ((> 2 3) 4 5)
                  ((> 2 4) 5 6)
                  ((< 2 5) 6 7)
                  (else 7 8))""",
           7),
        ("""(cond ((> 2 3) (display 'oops) (newline))
                  (else 9))""",
           9),
        ("""(cond ((< 2 1))
                   ((> 3 2))
                   (else 5))""",
            True),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True

@test('16')
@test('A16')
def problem_A16(grades):
    tests1 = [
        ("""(define (square x) (* x x))
                 (define (f x y)
                  (let ((a (+ 1 (* x y)))
                        (b (- 1 y)))
                    (+ (* x (square a))
                       (* y b)
                       (* a b))))
                 (f 3 4)""",
            456),
    ]
    tests2 = [
        ("""(define x 3)
                 (define y 4)

                 (let ((x (+ y 2))
                      (y (+ x 1)))
                  (cons x y))""",
            Pair(6, 4)),
        ("""(let ((x 'hello)) x)""",
            'hello'),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True

@test('17')
@test('B17')
def problem_B17(grades):
    tests1 = [
        ("""(define f (mu (x) (+ x y)))
                 (define g (lambda (x y) (f (+ x x))))
                 (g 3 7)""",
            13),
        ("""(define g (mu () x))
                 (define (high f x)
                   (f))
                 (high g 2)""",
            2),
    ]
    tests2 = [
        ("""(define (f x) (mu () (lambda (y) (+ x y))))
                 (define (g x) (((f (+ x 1))) (+ x 2)))
                 (g 3)""",
            8),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True


@test('18')
def problem_18(grades):
    tests1 = [
         ("(merge < '(1 5 7 9) '(4 8 10))",
          read_line('(1 4 5 7 8 9 10)')),
         ("(merge > '(9 7 5 1) '(10 8 4 3))",
          read_line('(10 9 8 7 5 4 3 1)')),
    ]
    tests2 = [
        ("(merge < '(1 2 3) '(4))",
         read_line('(1 2 3 4)')),
        ("(merge < () '(1 2))",
         read_line('(1 2)')),
    ]
    if check_func(check_scheme, tests1, comp=scheme_equal):
        return True
    if check_func(check_scheme, tests2, comp=scheme_equal):
        return True


problem_19_preamble = """
; True if ss is a list of lists
(define (sol-lol ss)
  (cond ((null? ss) #t)
        ((not (list? ss)) #f)
        ((and (list? (car ss))
              (sol-lol (cdr ss))) #t)
        (else #f)))

; True if ss contains s
(define (sol-contains s ss)
  (and (not (null? ss))
       (or (and (number? s) (= s (car ss)))
           (and (list? s) (sol-contains-all s (car ss)))
           (sol-contains s (cdr ss)))))

; True if ss2 contains all elements of ss1
(define (sol-contains-all ss1 ss2)
  (or (null? ss1)
      (and (sol-contains (car ss1) ss2)
           (sol-contains-all (cdr ss1) ss2))))

; True if ss1 and ss2 are the same list of lists
(define (sol-same-lols ss1 ss2)
  (and (sol-lol ss1)
       (sol-lol ss2)
       (sol-contains-all ss1 ss2)
       (sol-contains-all ss2 ss1)))
"""

@test('19')
def problem_19(grades):
    tests1 = [
        ("""(sol-same-lols (list-partitions 5 2 4)
         '((4 1) (3 2)))""", True),
        ("""(sol-same-lols (list-partitions 7 3 5)
         '((5 1 1) (4 2 1) (3 3 1) (3 2 2) (5 2) (4 3)))""", True),
        ]
    tests2 = [
        ("""(sol-same-lols (list-partitions 7 2 4)
         '((4 3)))""", True),
        ("""(sol-same-lols (list-partitions 7 7 1)
         '((1 1 1 1 1 1 1)))""", True),
    ]
    check = lambda snippet: check_scheme(snippet, problem_19_preamble)
    if check_func(check, tests1, comp=scheme_equal):
        return True
    if check_func(check, tests2, comp=scheme_equal):
        return True

@test('20')
def problem_20(grades):
    tests1 = [
        ('(tree-sums (make-tree 3 nil))',
         read_line('(3)')),
        ('(tree-sums tree)',
         read_line('(20 19 13 16 11)')),
    ]
    tests2 = [
        ("(tree-sums '(9 (4 (3 (8)) (2)) (5) (1 (2 (6)) (5))))",
         read_line('(24 15 14 18 15)')),
        ("(tree-sums '(-3 (-2) (-4)))",
         read_line('(-5 -7)')),
    ]
    if check_func(check_scheme, tests1, comp=scheme_equal):
        return True
    if check_func(check_scheme, tests2, comp=scheme_equal):
        return True

@test('22')
def problem_22(grades):
    scheme.scheme_eval = scheme.scheme_optimized_eval
    tests1 = [
        ("""(define (sum n total)
                   (if (zero? n) total
                     (sum (- n 1) (+ n total))))
                 (sum 1001 0)""",
            501501),
    ]
    tests2 = [
        ("""(define (sum n total)
                   (if (zero? n) total
                     (if #f 42 (sum (- n 1) (+ n total)))))
                 (sum 1001 0)""",
            501501),
    ]
    tests3 = [
        ("""(define (sum n total)
                   (cond ((zero? n) total)
                         ((zero? 0) (sum (- n 1) (+ n total)))
                         (else 42)))
                 (sum 1001 0)""",
            501501),
    ]
    tests4 = [
        ("""(define (sum n total)
                   (if (zero? n) total
                     (add n (+ n total))))
                 (define add (lambda (x+1 y) (sum (- x+1 1) y)))
                 (sum 1001 0)""",
            501501),
    ]
    tests5 = [
        ("""(define (sum n total)
                   (if (zero? n) total
                     (let ((n-1 (- n 1)))
                       (sum n-1 (+ n total)))))
                 (sum 1001 0)""",
            501501),
    ]
    if check_func(scheme_eval, tests1, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests2, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests3, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests4, comp=scheme_equal):
        return True
    if check_func(scheme_eval, tests5, comp=scheme_equal):
        return True

#############
# UTILITIES #
#############

def pairify(lst):
    if not lst:
        return nil
    if type(lst) is not list:
        return lst
    return Pair(pairify(lst[0]), pairify(lst[1:]))

def scheme_equal(x, y):
    """Are Scheme values x and y equal, even if they use different classes?"""
    if hasattr(x, 'first') and hasattr(y, 'first'):
        return scheme_equal(x.first, y.first) and scheme_equal(x.second, y.second)
    if type(x).__name__ == 'nil' and type(y).__name__ == 'nil':
        return True
    elif type(x).__name__ == 'LambdaProcedure' and type(y).__name__ == 'LambdaProcedure':
        if not environments_equal(x.env, y.env):
            return False
        return scheme_equal(x.formals, y.formals) and scheme_equal(x.body, y.body)
    else:
        return x == y

def environments_equal(env1, env2):
    """Are environments env1 and env2 equal, even using different classes?"""
    if type(env1).__name__ != 'Frame' or type(env2).__name__ != 'Frame':
        return False
    if env1.parent is None and env2.parent is None:
        # Assume that all global frames are the same
        return True
    if set(env1.bindings) != set(env2.bindings):
        # The two environments have different bindings
        return False
    for binding, value in env1.bindings.items():
        if not scheme_equal(value, env2.bindings[binding]):
            return False # If the values for the same bindings are different
    return environments_equal(env1.parent, env2.parent)

def catch_syntax_error(fn):
    def caught_syntax(*args):
        try:
            return fn(*args)
        except SyntaxError:
            return 'Error'
    return caught_syntax

def scheme_eval(snippet):
    """Convert snippet into a single expression and scheme_eval it."""
    # TODO: figure out how to do this more cleanly
    buf = scheme.buffer_lines(snippet.split('\n'), show_prompt=True)
    exprs = []
    try:
        while True:
            exprs.append(scheme.scheme_read(buf))
    except EOFError:
        pass
    env = scheme.create_global_frame()
    try:
        for expr in exprs[:-1]:
            scheme.scheme_eval(expr, env)
        return scheme.scheme_eval(exprs[-1], env)
    except scheme.SchemeError as err:
        return 'SchemeError'
    except BaseException as err:
        return type(err).__name__ + ' ' + str(err)

utils = """
(define (square x) (* x x))

(define (abs x)
  (cond ((> x 0) x)
        ((= x 0) 0)
        ((< x 0) (- x))))

(define (len s)
  (if (eq? s '())
    0
    (+ 1 (len (cdr s)))))

(define (equal? x y)
  (cond ((pair? x) (and (pair? y)
                        (equal? (car x) (car y))
                        (equal? (cdr x) (cdr y))))
        ((null? x) (null? y))
        (else (eq? x y))))

(define (map proc items)
  (if (null? items)
      nil
      (cons (proc (car items))
            (map proc (cdr items)))))

(define (filter predicate sequence)
  (cond ((null? sequence) nil)
        ((predicate (car sequence))
         (cons (car sequence)
               (filter predicate (cdr sequence))))
        (else (filter predicate (cdr sequence)))))

(define (accumulate op initial sequence)
  (if (null? sequence)
      initial
      (op (car sequence)
          (accumulate op initial (cdr sequence)))))

(define (combine f)
  (lambda (x y)
    (if (null? x) nil
      (f (list (car x) (car y))
         ((combine f) (cdr x) (cdr y))))))

(define (memq item x)
  (cond ((null? x) false)
        ((eq? item (car x)) x)
        (else (memq item (cdr x)))))

(define compose (lambda (f g) (lambda (x) (f (g x)))))

"""

def make_check_scheme(file='questions.scm'):
    """Check a Scheme question."""
    with open(file, 'r') as f:
        contents = utils + f.read()
    def check_scheme(snippet, preamble=''):
        stuff = contents + preamble + snippet
        return scheme_eval(stuff)
    return check_scheme

check_scheme = make_check_scheme()



##########################
# COMMAND LINE INTERFACE #
##########################

project_info = {
    'name': 'Project 4: Scheme',
    'remote_index': 'http://inst.eecs.berkeley.edu/~cs61a/fa13/proj/scheme/',
    'autograder_files': [
        'scheme_grader.py',
        'scheme_test.py',
        'autograder.py',
    ],
    'version': __version__,
}

@main
def run(*args):
    run_tests(**project_info)