Introduction Even though the syntax of Scheme is simple, it can be very difficult to determine the semantics of an expression. Hacker’s approach: Run it and see what happens. • What if it behaves differently on different machines or with different arguments? • What if you’re in charge of writing the first compiler? CS212 approach: Construct a formal, mathematical model. Overview Develop the model by starting with an extremely simple language and work our way up. – arithmetic – if, booleans – lambda, variables – substitution Goal: be able to construct a formal proof that a given Scheme program evaluates to a specified value. Scheme-0 Syntax: num op ::= + | - | * | / (expressions) e ::= num | op | (e1 … en ) (numbers) (operators) English: Expressions are either numbers, an operator (+, -, *, or /) or a combination which is a sequence of nested expressions surrounded by parentheses. Note: we use blue to denote meta-variables Scheme-0 Values (values) v ::= num | op English: Values are either numbers or an operator. Values are a subset of expressions. Well-formed expressions evaluate to a value. We write e => v when expression e evaluates to value v. Rules for Evaluation There are just two for Scheme-0: One for values, and one for certain kinds of combinations. If no rule applies, then the program is illformed. (DrScheme reports an error.) Evaluation Rule 1: Values v => v English: a value evaluates to itself. Examples: 3 => 3 (by rule 1) + => + (by rule 1) Evaluation Rule 2: Arithmetic To prove (e1 e2 e3) => v show: (a) e1 => op (e1 evaluates to an operator) (b) e2 => v1 (e2 evaluates to a value) (c) e3 => v2 (e3 evaluates to a value) (d) v1 op v2 = v (applying the operator to the values yields the value v.) Evaluation Rule 2 example: (+ 3 4) => 7 (by rule 2) (a) + => + (by rule 1, since + is a value) (b) 3 => 3 (by rule 1, since 3 is a value) (c) 4 => 4 (by rule 1, since 4 is a value) (d) 3+4 = 7 (by math) Scheme-1: op ::= + | - | * | / | < | > | = bool ::= #t | #f e ::= num | op | (e1 … en ) | (if e1 e2 e3) | bool v ::= num | op | bool • We added booleans (as values) and if expressions. • #t is true, #f is false. Eval. Rules 1 and 2 are the same: 1. v => v Examples: #t => #t, < => < 2. (e1 e2 e3) => v if: (a) e1 => op (b) e2 => v1 (c) e3 => v2 (d) v1 op v2 = v Examples: (< 3 4) => #t Evaluation Rule 3a: (if #f) To prove (if e1 e2 e3) => v show: (a) e1 => #f (b) e3 => v Example: (if (< 3 1) -1 0) => 0 (by rule 3a) (a) (< 3 1) => #f (by rule 2) (a) < => < (by rule 1) (b) 3 => 3 (by rule 1) (c) 1 => 1 (by rule 1) (d) 3 < 1 = #f (by math) (b) 0 => 0 (by rule 1) Evaluation Rule 3b (if #t) To prove (if e1 e2 e3) => v show: (a) e1 => v1 and v1 is not #f (b) e2 => v Example: (if (> 3 1) -1 0) => -1 (by rule 3a) (a) (> 3 1) => #t (by rule 2) (a) > => > (by rule 1) (b) 3 => 3 (by rule 1) (c) 1 => 1 (by rule 1) (d) 3 > 1 = #t (by math) (b) -1 => -1 (by rule 1) Notes on If Unlike other combinations, if is lazy. For non-if combinations (rule 2): – evaluate arguments to values eagerly – apply operator to values For if combinations (rule 3): – evaluate first argument (only) to value – if it’s #f, then evaluate third argument (3a) – otherwise, evaluate second argument (3b) Scheme-2 e ::= num | op | (e1 … en ) | (if e1 e2 e3) | bool | fn | x v ::= num | op | bool | fn fn ::= (lambda (x1 … xn) e) • We add lambda expressions (functions) and variables. – We use x to represent an arbitrary variable • Note that functions are values (i.e., lambdas evaluate to themselves.) • Previous rules (1,2,3a,3b) still apply • It’s an error to run into an unbound variable. Eval. Rule 4: (most important) To prove (e1 e2 e3 … en) => v show: (a) e1 => (lambda (x2 x3… xn) e) (b) e2 => v2, e3 => v3, …, en => vn (c) e[v2/x2, v3/x3 ,… , vn/xn] = e’ (i.e., substitute v2,…,vn for x2,…,xn in e) (d) e’ => v • We’ll formally define substitution later. Example: ((lambda (x) (if x 3 (* 4 2))) #f) => 8 (by 4) (a) (lambda (x) (if x 3 (* 4 2))) => (lambda (x) (if x 3 (* 4 2))) (by 1) (b) #f => #f (by 1) (c) (if x 3 (* 4 2))[#f/x] = (if #f 3 (* 4 2)) (by subst.) (d) (if #f 3 (* 4 2)) => 8 (by 3a) (a) #f => #f (by 1) (b) (* 4 2) => 8 (by 2, subgoals obvious) Another Example: (((lambda (x) (lambda (y) y)) 3) 5) => 5 (by rule 4) (a) ((lambda (x) (lambda (y) y)) 3) => (lambda (y) y) (by rule 4 & proof below) (b) 5 => 5 (c) y[5/y] = 5 (by rule 1) (by substitution) (d) 5 => 5 (by rule 1) So now all we have to show is part (a)... Example Continued ((lambda (x) (lambda (y) y)) 3) => (lambda (y) y) (by rule 4) (a) (lambda (x) (lambda (y) y)) => (lambda (x) (lambda (y) y)) (by rule 1) (b) 3 => 3 (by rule 1) (c) (lambda (y) y)[3/x] = (lambda (y) y) (by subst.) (d) (lambda (y) y) => (lambda (y) y) (by rule 1) Hmmmm... Consider changing y to x systematically: Old: ((lambda (x) (lambda (y) y)) 3) New: ((lambda (x) (lambda (x) x)) 3) Body of outer function Following rule 4: (a) (lambda (x) (lambda (x) x)) => (lambda (x) (lambda (x) x)) (b) 3 => 3 (c ) (lambda (x) x)[3/x] = ??? Some Wrong Answers: (lambda (x) x)[3/x] = (lambda (x) 3) (lambda (x) x)[3/x] = (lambda (3) 3) Why are these wrong? – The first x is a binding occurrence (the name of a parameter) – The second x is a free occurrence that refers to a use of the nearest enclosing bound variable. – This is called lexical scope for variables. Formalizing Substitution e ::= num | op | (e1 … en ) | (if e1 e2 e3) | bool | x | (lambda (x1…xn) e) We write e[v1/x1,…,vn/xn] as an abbreviation for performing the substitutions one at a time. So all we really need to define formally is e[v/x]. We do so by cases on e (7 cases): Substitution Rules 1-5 are easy No variable, no substitution: s1. num[v/x] = num ex: 3[#t/y]=3 s2. op[v/x] = op ex: +[#t/y]=+ s3. bool[v/x] = bool ex: #f[#t/x]=#f Usually, just push the substitution in: s4. (e1 … en )[v/x] = (e1 [v/x] … en [v/x]) s5. (if e1 e2 e3)[v/x] = (if e1 [v/x] e2 [v/x] e3 [v/x] ) Examples for rules s4-s5 (+ 3 2)[#t/y] = (by s4) (+[#t/y] 3[#t/y] 2[#t/y]) = (+ 3 2) because +[#t/y] = + 3[#t/y]= 3 2[#t/y]= 2 (by s2) (by s1) (by s1) (if #f 3 2)[#t/y] = (by s5) (if #f[#t/y] 3[#t/y] 2[#t/y]) = (if #f 3 2) Substitution: The Real Action s6. y [v/x] = v if y and x are the same = y otherwise s7. (lambda (x1…xn) e) [v/x] = a. (lambda (x1…xn) e) if x is one of x1…xn . b. (lambda (x1…xn) e[v/x]) if x is not one of x1…xn . Substitution Rule 7 is very important!!! Example For Rule s6: (+ y x)[3/y] = (by s4) (+[3/y] y[3/y] x[3/y]) = (+ 3 x) because +[3/y] = + y[3/y]= 3 x[3/y]= x (by s2) (by s6 -- notice y = y) (by s6 -- notice xy) Example for rule s7: (lambda (x y) (+ z y))[3/z] = (s7b) (lambda (x y) (+ z y)[3/z] ) = (s5) (lambda (x y) (+[3/z] z[3/z] y[3/z] )) = (s2) (lambda (x y) (+ z[3/z] y[3/z] )) = (s6a) (lambda (x y) (+ 3 y[3/z] )) = (s6b) (lambda (x y) (+ 3 y)) = (s2) In the first line, rule s7b applies because the variable we’re replacing (z) does not occur as a parameter to the function. Another Example for rule s7 (lambda (x y) (+ z y))[3/y] = (s7a) (lambda (x y) (+ z y)) Why? Because the variable we’re substituting for (y) is one of the parameters, so we do not push the substitution in to the body of the function. Yet another 7 example (lambda (x y) (+ z y))[3/w] = (s7b) (lambda (x y) (+ z y)[3/w] ) = (s5) (lambda (x y) (+[3/w] z[3/w] y[3/w] )) = (s2) (lambda (x y) (+ z[3/w] y[3/w] )) = (s6b) (lambda (x y) (+ z y[3/w] )) = (s6b) (lambda (x y) (+ z y)) = (s2) This time, the variable we’re replacing (w) is not one of the parameters, but it doesn’t occur in the body of the function so it disappears! Revisiting: (lambda (x) x)[3/x] = (lambda (x) x) (by s7a) Why? The x that we’re substituting 3 for was shadowed by another definition. Most (modern) languages have similar scoping rules -- inner definitions of variables hide outer definitions. Summary Formalized evaluation of Scheme-2 – Gave syntax of expressions • numbers, operators, combinations, booleans, if, and lambda. – Gave syntax-directed rules for evaluating expressions to values. – Substitution comes into play for user-defined functions. – Lexical scope determines rules for when we substitute what. Still need to cover define... Scheme-3 Expressions and values are as before... (programs) (defines) p ::= d1 … dn e d ::= (define x e) The top-level declarations allow us to define global variables (usually functions). But they require a slightly different model... Top-Level Environments: An Environment (Env) is a way to keep track of top-level bindings. It simply maps (some) variables to values. Example: {x:=3, y:=#t} English: if you see x while evaluating, replace it with 3 and if you see y, replace it with #t. Intuition: Suppose our program is: (define x (+ 3 4)) (define inc (lambda (x) (+ x 1))) (inc x) We evaluate as follows: – start with an empty environment Env0 = {} – evaluate (+ 3 4) in Env0, yielding 7 and bind x to 7 resulting in Env1 = {x:=7} – bind inc to the lambda-value resulting in Env2 = {x:=7, inc:=(lambda (x) (+ x 1))} – evaluate (inc x) in Env2 replacing inc with (lambda (x) (+ x 1)) and x with 7 to get 8. Three New Things: 1. Env |- e => v Same as before except if we run into a free variable while evaluating e, we look it up in Env. 2. Env1 |- d => Env2 Evaluating a definition yields a new environment (with that definition) 3. P => v A program yields a value. Intuitively, start with an empty environment, evaluate definitions to get a new environment, and then evaluate the expression of the program. Revisiting Evaluation Rules 1 and 2 1. Env |- v => v (no real change) 2. Env |- ( e1 e2 e3 ) => v if: (a) Env |- e1 => op (b) Env |- e2 => v1 (c) Env |- e3 => v2 (d) Env |- v1 op v2 = v Evaluation Rule 3: 3a. Env |- (if e1 e2 e3) => v if: (a) Env |- e1 => #f (b) Env |- e3 => v 3b. Env |- (if e1 e2 e3) => v if: (a) Env |- e1 => v’ and v’ is not #f (b) Env |- e3 => v Evaluation Rule 4: Env |- (e1 e2 e3 … en) => v if: (a) Env |- e1 => (lambda (x2 x3… xn) e) (b) Env |- e2 => v2 ,…, Env |- en => vn (c) e[v2/x2 ,… , vn/xn] = e’ (d) Env |- e’ => v Eval.Rule 5: (new rule -- variables) Env |- x => v if x is mapped to v by Env. That is, Env has a binding x:=v in it. Eval. Rule 6: (new rule -- define) To prove Env |- (define x e) => Env{x:=v} show Env |- e => v That is, first evaluate e in the current environment Env to yield a value v. Then bind v to the variable x to yield a new environment (for subsequent evaluation.) Eval.Rule 7: (new rule -- program) To prove d1 … dn e => v show: (a) {} |- d1 => Env1 … Envn-1 |- dn => Envn (b) Envn |- e => v That is, start with an empty environment, evaluate the definitions (in order), take the final environment and use it to evaluate e. Putting it all together: Let’s prove that evaluating the program P below yields 6. (define z 1) (define w (* 3 z)) (define f (lambda (n) (if (< n 2) 1 (+ n (f (- n 1)))))) (f w) First Define We start off in an empty environment ({}) and show: {} |- (define z 1)=> {z:= 1} (by rule 6) because {} |- 1 => 1 (by rule 1) Second Define {z:=1} |- (define w (* z 3)) => {z:=1,w:=3} (6) because {z:=1} |- (* z 3) => 3 (2): (a) {z:=1} |- * => * (1) (b) {z:=1} |- z => 1 (5 -- note lookup) (c) {z:=1} |- 3 => 3 (1) (d) 1*3 = 3 (by math) So our 2nd environment is {z:=1,w:=3}. Third Define: This one is easy like the first one, because the expression is already a value (a lambda): {z:=1,w:=3} |- (define f (lambda (n) …)) => Env where Env is {z:=1,w:=3,f:=(lambda (n) …)} by the fact that {z:=1,w:=3} |- (lambda (n) …) => (lambda (n) …) (rule 1). Finally, we must show that Env |- (f w) => 6. Final Expression: Env |- (f w) => 6 (4) (a) Env |- f => (lambda (n) (if (< n 2) 2 (+ n (f(- n 1))))) (5 since f maps to (lambda (n) …) in Env) (b) Env |- w => 3 (5 since w maps to 3 in Env) (c) (if (< n 2) 1 (+ n (f (- n 1))))[3/n] = (if (< 3 2) 1 (+ 3 (f (- 3 1))))(subst) Continuing... (d) Env |(if (< 3 2) 1 (+ 3 (f (- 3 1)))) =>6(3a) (a) Env |- (< 3 2) => #f (2 & obvious) (b) Env |- (+ 3 (f (- 3 1))) => 6 (2) +, 3 obvious, need to show Env |- (f (- 3 1)) => 3 And on... Env |- (f (- 3 1)) => 3 (by 4) (a) Env |- f => (lambda (n) (if …)) (by 5) (b) Env |- (- 3 1) => 2 (by 2 & obvious) (c) (if …)[2/n] = (if (< 2 2) 1 (+ 2 (f (- 2 1)))) (by subst) (d) Env |- (if (< 2 2) 1 (+ 2 (f (- 2 1)))) => 3 (by 3a) And on... (a) Env |- (< 2 2) => #f (2 & obvious) (b) Env |- (+ 2 (f (- 2 1))) => 3 (2) +,2 obvious, need to show Env |- (f (- 2 1)) => 1 (4) (a) Env |- f => (lambda (n) (if …)) (5) (b) Env |- (- 2 1) => 1 (2 & obvious) (c) (if …)[1/n] = (by subst.) (if (< 1 2) 1 (+ 1 (f (- 1 1)))) (d) Env |- (if (< 1 2) 1 …) => 1 (3b) And on... Env |- (if (< 1 2) 1 …) => 1 (3b) (a) Env |- (< 1 2) => #t (2) (b) Env |- 1 => 1 (1) Therefore, adding up the last umpteen slides, the program evaluates to 6. Note: though many steps were skipped, they were obvious. When in doubt, do all of the steps.