Interpreters Lab: Predict, Trace, and Extend Eval/Apply

Practice the core SICP idea of interpreters as a universal machine: parse/represent programs as data, evaluate with environments via the Eval/Apply cycle, and extend a language by adding derived expressions like `unless` (and `let`).

0. Predict: `unless` is Derived from `if` (Exit Ticket)

**Predict what will happen BEFORE running.**

This tiny evaluator supports numbers, `+`, `if`, and a derived form `unless`.

```python
# Expression to evaluate:
expr = ['+', 1, ['unless', False, 10, ['boom']]]
```

1) What value should `eval_expr(expr)` return?
2) Will it raise an error by evaluating `['boom']`?

Write your prediction as a comment, then run to check.

  

1. Trace: Eval/Apply Creates a New Frame for a Call

Trace the **Eval/Apply cycle** for this tiny program (already parsed into ASTs).

Program:
1. `['define', 'square', ['lambda', ['x'], ['*', 'x', 'x']]]`
2. `['square', 5]`

Tasks:
- Predict the final value of evaluating expression 2.
- Then **trace** where the binding `x = 5` lives (which frame?), and why `square` can still find `*`.

Fill in the comments in `run_program()` with your trace before running.

  

2. Extend: Add Derived Expressions `unless` + `let` (SICP Ex 4.6-inspired)

In this evaluator, `unless` and `let` are **not** primitive special forms. Instead, we will implement them as **derived expressions** (syntactic transformations).

Implement `desugar(exp)` so that:

- `['unless', c, a, b]` rewrites to `['if', c, b, a]`
- `['let', [[v1, e1], [v2, e2], ...], body]` rewrites to a combination:
  
  `[[ 'lambda', [v1, v2, ...], body], e1, e2, ...]`

Then the existing `eval_expr` will evaluate the rewritten form using its normal Eval/Apply rules.

After writing `desugar`, predict the value of `prog2` before running.