Goals of the course:
* Learn how to construct models/specifications of systems
* "system" == language
* could be something like Java or ML or the JVML
* could be the x86
* could be a protocol like TCP
* the goal is to have precise, useful specifications that are amenable
to formal manipulation.
* What do we do with thse models?
* understand linguistic features
* e.g., scope/binding/naming
* e.g., control: exceptions, continuations, prompts, threads, ...
* e.g., types, modules, objects, classes, ...
* prove I/O equivalence (or refinement) of two programs
* e.g., a specification vs. an implementation
* prove correctness of an analysis, implementation or transformation
* e.g., correctness of a compiler transformation/optimization
* prove properties of a language
* e.g., Java is type-safe
* e.g., program equivalence is decidable
We'll look at three basic kinds of models:
* operational: think interpreters
* specify a space of abstract machine states
* specify the initial state(s) that a program gets loaded into
* specify a transition relation from one state to another
* Pros: very easy, scales to full language
* Cons: difficult to prove equivalence, not very extensible
* e.g., difficult to prove fact_iter == fact_recur
* denotational: think compile to math (or some easier to reason about target)
* emphasis on a compositional translation E[e]
* often interpret expressions or commands as mathematical functions
* Pros: easier to reason about I/O equivalence, "open" in a certain sense,
particularly good for deriving program analyses.
* Cons: gets complicated as we deal with "real" language features like
objects or threads.
* axiomatic: think compile to logic
* again, compositional translation
* relational instead of functional -- more algebraic flavor
* Pros: good for specification vs. implementation, more powerful sorts of
analyses.
* Cons: gets complicated as we deal with even moderatly simple features
like higher-order functions.
We'll study these 3 kinds of models for a very simple, imperative
programming language called IMP, which is really just an idealized
FORTRAN, and compare/contrast the approaches.
Our next goal is to work from the inside out to models of more
realistic linguistic features. We'll start with the simply-typed
lambda calculus and study it very carefully.
* TLC used as a meta-language -- due to purity
* TLC forms core of *every* language (scope, binding, procedures/functions)
* TLC a reasonable target for many language features
* TLC used within advanced type systems, specification languages, logics,etc.
We'll then start to scale TLC up to cover most of the features that
show up in languages such as ML, Java, and Haskell such as:
* evaluation and normalization strategies (e.g., CBV, CBN, ...)
* various kinds of data structures (e.g., records, datatypes, ...)
* various control constructs (exceptions, continuations, ...)
* various xformations (e.g., cps, closure, object conversion)
* state/references
* polymorphism (e.g., subtyping, parametric, bounded-parametric, h.o.,...)
* classes, modules, and mixins
* threads, channels, etc.
We'll also look at a few alternative foundational calculi (e.g., pi-calculus,
object calculi) but for the most part, stick with lambda as the core.
Logistics:
Books: none but Winskell and Pierce are good references if you're
uncomfortable with that.
Grades: homeworks and class participation, including scribing of
lecture notes. The homeworks will be done in either SML, O'Caml,
Haskell, Scheme, or Java.
-----------------------------------------------------------------------
IMP
x in Id
i in Z
bop in Binop ::= + | * | -
e in Exp ::= x | i | e1 bop e2
c in Com ::= x := e | skip | c1;c2 | if e then c1 else c2 | while e do c
Note that we're using abstract syntax here, not concrete syntax.
Formally, a command, boolean expression, or expression is a (finite)
tree, but for convenience sake, I'm writing these as strings.
We'll formalize these BNF definitions a bit later.
Identifiers in IMP are globally scoped, and range over integers (bignums).
Intuitively, a program is a command which we run in the context of some
store (or memory) to produce a new store. We model stores as functions
from identifiers to integers:
s in Store = Id -> Z
To make the meaning of commands (and expressions and boolean expressions)
precise, we'll construct a small-step, structured operational semantics.
What we're essentially going to do is formally describe an abstract machine
that takes a configuration and steps to a new configuration, representing
one "instruction" being executed in the command.
Our configurations will be a pair of a command and a store. Our
step relation will take one configuration to another: -> .
A terminal configuration will be of the form representing
a program that has completed execution. We'll write ->* s
if there exists a sequence of zero or more steps that produce a
configuraiton .
We'll define the step relation using inference rules and some auxiliary
relations for evaluating expressions and boolean expressions.
(C1) -> i]>
-exp->
(C2) ----------------------------
->
(C3) ->
->
(C4) ---------------------------
->
(C5) -> (i != 0)
(C6) ->
-exp->
(C7) --------------------------------------------------------
->
(C8) ->
(E0) -exp-> ~~
(E1) -exp-> ~~* (i == i1 bop i2)
-exp->
(E2) ---------------------------------------
-exp->
-exp->
(E3) -------------------------------------
** -exp-> **
Notes: I claim (and we'll later prove) that for any configuration
there is at most one rule whose conclusion the configuration
matches. That is, at most one rule can fire for a given configuration.
For example, consider:
Rule C1 doesn't apply because 3+4 is not an integer -- it's a
compound expression. So only rule C2 can possibly apply. It
only applies if we can prove <3+4,s> -> ** for some i.
That follows from rule E1. But does E4 also apply? No,
because that would require us to find a rule that matches
<3,s> -exp-> ???, but no such rule exists.
Formally, when we're proving that a configuration steps, we
need to produce a derivation. In the case of the example above,
the derivation would look like this:
(E1) ---------------------
<3+4,s> -exp-> <7,s>
(C2) --------------------------------
->
And note that:
(C1) ---------------------------------
-> 7]>
Great, we've built a little interpreter for a tiny language. Let's
play around with some variations.
* What if we wanted to change the evaluation order of expressions
to be right-to-left instead of left-to-right?
* Suppose we changed E3 to look like this:
-exp->
(E3') --------------------------------------
-exp->
How would this effect evaluation of <(3+4)+(2+1),s>?
* Suppose we wanted to add a new command c1 || c2 with the meaning
that we should run the two commands in parallel. What rules should we add?
Now let's go back and prove some properties about the language.
Let us define:
c1 <= c2 to mean for all stores s, if ->* s' then ->* s'
and also define:
c1 == c2 to mean c1 <= c2 and c2 <= c1.
This expresses an input/output equivalence (known as Kleene equivalence)
for programs: if the two programs map the same input to the same output,
then we consider them equal. Note that the definition equates all
commands that do not terminate. For instance, while true do skip ==
{x := 1 ; while x do y := y + 1 }
*