---------------------------------------------------------------------------- I. Strachey-style denotational semantics: "what is the _meaning_ of ...?" ---------------------------------------------------------------------------- Q: What is the meaning of a numeral? A: a number Q: What is the meaning of a storage vector? A: a _function_ that takes a var as input and returns a number as output! Q: What is the meaning of an assignment? A: a _function_ that takes a storage vector as input and returns a storage vector as output! Q: How do we implement these meanings? A: Study hardware and compilers.... Example: The meaning of this storage vector: X Y Z ... +-+-+-+ +-+ |0|0|0|...|0| is lam i. 0 +-+-+-+ +-+ Say that we apply Y = 2 to the above vector; what is the _meaning_ of the output ? It is this function: lam j. if (j=Y) then 2 else ((lam i.0) j) = lam j. if (j=Y) then 2 else 0 Of course, we can code j=Y and the if-then-else in lambda-calculus, too! Or, we can do what Strachey did: augment the lambda-calculus with new constants and rewrite rules (delta-rules), like this: (a) Bool-type constants and rules: constants: true, false operators: if rules: (if true E1 E2) => E1 (if false E1 E2) => E2 (b) Int-type constants and rules: constants: 0,1,2,... operators: plus, minus, times, ..., equals rules: (plus 0 0) => 0 (plus 1 0) => 1 ... (minus 0 0) => 0 (minus 1 0) => 1 ... (times 0 0) => 0 (times 1 1) => 1 ... (equals 0 0) => true (equals 1 0) => false (c) Var-type constants and rules: constants: X, Y, Z, A, B, ... operators: = rules: (X = X) => true (X = Y) => false ... (d) Store-type constants and rules: constants: none (use lam-abstractions) operators: lookup, update rules: (lookup i s) => (s i) (update i n s) => lam j. if (j = i) n (lookup j s) ### A quick example: let [[ Y = 2 ]] == lam s. (update Y 2 s) then [[ Y = 2 ]](lam i. 0) => (lam s. update Y 2 s)(lam i. 0) => update Y 2 (lam i. 0) => lam j. if (j = i) 2 (lookup j (lam i. 0)) => lam j. if (j = i) 2 ((lam i. 0) j) => lam j. if (j = i) 2 0 That is, the _meaning_ of Y = 2 applied to the _meaning_ of a store of 0s is a store whose _meaning_ is lam j. if (j = i) 2 0 How do I implement this? Study hardware and compilers.... ---------------------------------------------------------------------------- II. A denotational semantics shows how to define a program's _meaning_ ---------------------------------------------------------------------------- C: Command E: Expression I: Variable N: Numeral C ::= I = E | C1 ; C2 | if E then C1 else C2 | while E do C E ::= N | I | E1 + E2 | E1 - E2 [[ C ]] : Store -> Store [[ I = E ]] = lam s. update I ([[ E ]] s) s [[ C1 ; C2 ]] = lam s. [[ C2 ]] ([[ C1 ]] s) [[ if E then C1 else C2 ]] = lam s. if (equals 0 ([[ E ]] s)) ([[ C2 ]] s) ([[ C1 ]] s) [[ while E do C ]] = lam s. Y (lam w. lam s'. if (equals 0 ([[ E ]] s')) s' (w ([[ C ]] s')) ) where Y = lam f. (f (x x))(f (x x)), as usual [[ E ]] : Store -> Int [[ N ]] = lam s. n that is, the int, n, named by numeral, N [[ I ]] = lam s. lookup I s [[ E1 + E2 ]] = lam s. plus ([[ E1 ]] s) ([[ E2 ]] s) [[ E1 - E2 ]] = lam s. minus ([[ E1 ]] s) ([[ E2 ]] s) How do I implement this? Study hardware and compilers.... ---------------------------------------------------------------------------- III. Example: What is the meaning of X = 1; Z = X * Y when used with the meaning of a zero-ed store ? ---------------------------------------------------------------------------- [[ X = 1; Z = X * Y ]] (lam i. 0) = [[ Z = X * Y ]] ([[ X = 1 ]] (lam i. 0)) = [[ Z = X * Y ]] ([[ X = 1 ]] (lam i. 0)) = [[ Z = X * Y ]] ((lam s. update X ([[ 1 ]] s) s) (lam i. 0)) = [[ Z = X * Y ]] (update X ([[ 1 ]] (lam i. 0)) (lam i. 0)) = [[ Z = X * Y ]] (update X ((lam s. 1) (lam i. 0)) (lam i. 0)) = [[ Z = X * Y ]] (update X 1 (lam i. 0)) = [[ Z = X * Y ]] (lam j. if (j=X) 1 ((lam i. 0) j)) = [[ Z = X * Y ]] (lam j. if (j=X) 1 0) ) = (lam s. update Z ([[ X * Y ]] s) s) (lam j. if (j=X) 1 0) = (lam s. update Z (times ([[ X ]] s) ([[ Y ]] s) s)) (lam j. if (j=X) 1 0) = (lam s. update Z (times ([[ X ]] s) ([[ Y ]] s) s) ) (lam j. if (j=X) 1 0) = (lam s. update Z (times ((lam s. lookup X s) s) ([[ Y ]] s) s) ) (lam j. if (j=X) 1 0) = (lam s. update Z (times (lookup X s) ([[ Y ]] s) s) ) (lam j. if (j=X) 1 0) = (lam s. update Z (times (s X) ([[ Y ]] s) s) ) (lam j. if (j=X) 1 0) = (lam s. update Z (times (s X) (s Y) s) ) (lam j. if (j=X) 1 0) = update Z (times ( (lam j. if (j=X) 1 0) X) ( (lam j. if (j=X) 1 0) Y)) (lam j. if (j=X) 1 0) = update Z (times 1 ( (lam j. if (j=X) 1 0) Y)) (lam j. if (j=X) 1 0) = update Z (times 1 0) (lam j. if (j=X) 1 0) = update Z 0 (lam j. if (j=X) 1 0) = lam j'. if (j'=Z) 0 (((lam j. if (j=X) 1 0) j')) = lam j'. if (j'=Z) 0 (if (j'=X) 1 0) The meaning of the program applied to the meaning of a zero-out store is a (store) function that maps Z to 0, X to 1, and all other vars to 0. ###Exercise: calculate the meaning of this program, without any input: [[ X = 1; Z = X * Y ]] If you proceed correctly, you will reach this meaning: [[ X = 1; Z = X * Y ]] = lam s. (lam s1. update Z (times (lookup X s1) (lookup Y s1) s1) (update X 1 s) This is the _partially evaluated_ denotation of the original program. It is a kind of meta-"compiled code". There is a rich theory of compiling based on dentational semantics.