---
# System prepended metadata

title: Code test

---

# Code test

Given the following tree definition (in Python 3+)

```python
class Expr:
    pass

@dataclass
class BinOp(Expr):
    l  : Expr
    op : str
    r  : Expr

@dataclass
class Literal(Expr):
    lit : int

@dataclass
class IfExpr(Expr):
    cond      : Expr
    then_expr : Expr
    else_expr : Expr
```

Implement the evaluation function with the following prototype:

```python
def eval(expr: Expr) -> int:
    pass
```

For if-expression conditions, you can assume that booleans are encoded as in C: 0 is `false` and anything else is `true`.

Here is the complete code template with tests

```python
from dataclasses import dataclass

class Expr:
    pass

@dataclass
class BinOp(Expr):
    l  : Expr
    op : str
    r  : Expr

@dataclass
class Literal(Expr):
    lit : int

@dataclass
class IfExpr(Expr):
    cond      : Expr
    then_expr : Expr
    else_expr : Expr

def eval(expr: Expr) -> int:
    ...


for expr, expected_res in [
    (BinOp(Literal(1), "+", Literal(2)), 3),
    (BinOp(Literal(3), "-", Literal(1)), 2),
    (IfExpr(BinOp(Literal(1), "+", Literal(2)), Literal(1), Literal(2)), 1),
    (IfExpr(Literal(0), Literal(1), Literal(2)), 2),
]:
    res = eval(expr)
    if res != expected_res:
        print(f"Unexpected result from evaluating {expr}: {res}")
    else:
        print(f"Eval {expr} = {res}")
```

## Advanced question 1: type_check function

Add boolean literals, and the `and` and `or` short circuit operators. Add a type_check function to check the correctness node types.

One example of an error that the type_check function should be able to catch, is that operands of an operator should be of the same type.

**The type_check function is expected to compute the type of an expression. In case of error (type checking failure), it must print the error and raise an exception.**

```python
from dataclasses import dataclass
from typing import Union

class Expr:
    pass

@dataclass
class BinOp(Expr):
    l  : Expr
    op : str
    r  : Expr

@dataclass
class Literal(Expr):
    lit : Union[int, bool]

@dataclass
class IfExpr(Expr):
    cond      : Expr
    then_expr : Expr
    else_expr : Expr

def eval(expr: Expr) -> Union[int, bool]:
    ...

def type_check(expr: Expr) -> type:
    ...


for expr, expected_res in [
    (IfExpr(Literal(False), Literal(1), Literal(2)), 2),
    (IfExpr(Literal(True), Literal(1), Literal(2)), 1),
    (IfExpr(Literal(1), Literal(2), Literal(True)), None),
    (IfExpr(Literal(True), Literal(False), Literal(False)), False),
    (IfExpr(Literal(True), Literal(True), Literal(2)), None),
    (BinOp(Literal(True), "+", Literal(False)), None),
    (BinOp(Literal(True), "or", Literal(False)), True),
    (BinOp(Literal(1), "or", Literal(2)), None),
    (BinOp(BinOp(Literal(1), "+", Literal(3)), "/", IfExpr(Literal(True), Literal(1), Literal(2))), 4)
]:
    try:
        type_check(expr)
        res = eval(expr)
        if res != expected_res:
            print(f"Unexpected result from evaluating {expr}: {res}")
        else:
            print(f"Eval {expr} = {res}")
    except Exception as exc:
        print(
            f"{'Expected'if expected_res is None else 'Unexpected'} "
            f"type checking failure for {expr}:\n  {exc}"
        )
```

## Advanced question 2: Let expressions

Add an expression to the evaluator: this expression is called a `Let` expression, and allows the user to introduce a temporary name for an intermediate expression.

```python
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, Optional, Union

class Expr:
    pass

@dataclass
class BinOp(Expr):
    l  : Expr
    op : str
    r  : Expr

@dataclass
class Literal(Expr):
    lit : Union[int, bool]

@dataclass
class IfExpr(Expr):
    cond      : Expr
    then_expr : Expr
    else_expr : Expr

@dataclass
class Let(Expr):
    name : str
    val  : Expr
    into : Expr

@dataclass
class Ref(Expr):
    name: str

class Scope:
    def __init__(self, parent: Optional[Scope]):
        self.data: Dict[str, Any] = {}
        self.parent = parent

    def get(self, key: str) -> Any:
        if key in self.data:
            return self.data[key]
        elif self.parent is None:
            raise KeyError
        else:
            return self.parent.get(key)

    def set(self, key: str, val: Any) -> None:
        self.data[key] = val

    def new_scope(self) -> Scope:
        return Scope(self)


def eval(expr: Expr, scope: Optional[Scope] = None) -> Union[int, bool]:
    ...

def type_check(expr: Expr, scope: Optional[Scope] = None) -> type:
    ...

for expr in [
    Let("a", Literal(1), BinOp(Ref("a"), "+", Literal(2))),
    Let("a", Literal(True), BinOp(Ref("a"), "+", Literal(2)))
]:
    try:
        type_check(expr)
        print(f"Eval {expr} = {eval(expr)}")
    except Exception as exc:
        print(f"Type checking {expr} failed:\n  {exc}")

```

# General questions

* What is a hash table?
    * How is it implemented?
    * What is the complexity of basic operations?

* What is the asymptotic complexity of fib? Modify it to make it linear any way you want.

```
  function fib(n) 
       if n <= 1 return n
       return fib(n − 1) + fib(n − 2)
```

* What is dynamic programming? What is memoization?