Denotational Semantics

One way to define the meaning of a program is to actually define the meaning of each construct.

Getting Started

We’ll start very simply, with a language that just has integers, addition and subtraction, and print statements. Programs look like this:

print(8 - (3 + 21) + 34);
print(55 - 3 + 2);

The concrete syntax can be given by this grammar:

Program = Stmt+
Stmt    = "print" "(" Exp ")" ";"
Exp     = (Exp ("+" | "-"))? Term
Term    = num | "(" Exp ")"
num     = digit+

When defining a semantics we don’t care about concrete syntax, only abstract syntax. So here is the abstract syntax of this tiny language, which is all we care about moving on:

$\begin{array}{l} n: \mathsf{Nml} \\ e: \mathsf{Exp} = n\;|\;e + e\;|\;e - e \\ s: \mathsf{Stm} = \mathtt{print}\;e \\ p: \mathsf{Pro} = \mathtt{program}\;s^+ \\ \end{array}$

Need Practice?

For the example program in this section, give both the parse tree and the abstract syntax tree.

Semantic Domains

A denotational semantics assigns to each syntactic phrase, its meaning. Sounds simple right? So what’s the meaning? For now, we will take:

Hmmm, we said “domain” and not “set.” We’ll define what domains are later. You can think of them as sets for now. But “sort-of” sets. As a first pass, think of $\mathsf{Num}$ as something like $\mathbb{R}_\bot$, the “lifted” set of real numbers with a special $\bot$ value. (Yes, we’ll define lifting later.)

CLASSWORK
Stop right now and make absolutely sure you know the difference between the syntactic domains (Nml, Exp, Stm, Pro) and the semantic domains (Num, Output).
Domains vs. Sets

While we will clarify the difference later, for now, remember not to use sets like $\mathbb{R}$ and $\mathbb{B}$ directly in denotational semantics. Instead, you will need to use domains like $\mathbb{R_{\bot}}$ and $\mathbb{B_{\bot}}$, which are almost always abbreaviated as $\mathsf{Num}$ and $\mathsf{Bool}$.

When reading a denotational semantics, it is understood that “everything is a domain,” so it is best not to use sets anywhere.

Note that domains are not used in operational semantics.

Semantic Functions

Now let’s write the semantic functions that give meaning to the abstract syntactic constructs:

$\begin{array}{l} \mathscr{E}\,[\![ n ]\!] = n \\ \mathscr{E}\,[\![ e_1 + e_2 ]\!] = \mathscr{E}\,e_1 + \mathscr{E}\,e_2 \\ \mathscr{E}\,[\![ e_1 - e_2 ]\!] = \mathscr{E}\,e_1 - \mathscr{E}\,e_2 \\ \mathscr{S}\,[\![ \mathtt{print}\;e ]\!]\,o = o\!\cdot\!\mathscr{E}\,e \\ \mathscr{P}[\![s_1\!\ldots\!s_n]\!] = \mathscr{S}\,s_n\,( \ldots \mathscr{S}\,s_2\,(\mathscr{S}\,s_1\,()) \ldots ) \end{array}$

Each of the semantic functions map a syntactic construct to a semantic domain:

$\begin{array}{l} \mathscr{E}\,: \mathsf{Exp} \rightarrow \mathsf{Num} \\ \mathscr{S}\,: \mathsf{Stm} \rightarrow \mathsf{Output} \rightarrow \mathsf{Output} \\ \mathscr{P}\,: \mathsf{Pro} \rightarrow \mathsf{Output} \end{array}$
Exercise: Practice with the definitions. Get good at ”reading them.”
Defining Semantic Functions

Note that the following two lines mean the same thing:

$\mathscr{S}\,[\![ \mathtt{print}\;e ]\!]\,o = o\!\cdot\!\mathscr{E}\,e$

$\mathscr{S}\,[\![ \mathtt{print}\;e ]\!] = \lambda o. o\!\cdot\!\mathscr{E}\,e$

Feel free to use whichever style you like.

Denotational Semantics for Astro

Remember Astro?

Here’s the abstract syntax:

$ \begin{array}{lcl} n: \mathsf{Nml} & & \\ i: \mathsf{Ide} & & \\ e: \mathsf{Exp} & = & n \mid i \mid -e \mid e+e \mid e-e \mid e\,\mathtt{*}\,e \mid e\,/\,e \mid e\,\%\,e \mid e\,\mathtt{**}\,e \mid i\;e^*\\ s: \mathsf{Stm} & = & i = e \mid \mathtt{print}\;e\\ p: \mathsf{Pro} & = & s^+\\ \end{array}$

What do we need for semantics? Well, we have variables, so expressions will have to be evaluated in the context of a memory:

$\mathscr{E}\,: \mathsf{Exp} \rightarrow \mathsf{Mem} \rightarrow \mathsf{Num}$

Statements can affect both the memory (via assignment) and the output (via printing), so a statement transforms a memory and output into a new memory and output:

$\mathscr{S}\,: \mathsf{Stm} \rightarrow (\mathsf{Mem} \times \mathsf{Output}) \rightarrow (\mathsf{Mem} \times \mathsf{Output})$

The meaning of a program is the output it produces:

$\mathscr{P}\,: \mathsf{Pro} \rightarrow \mathsf{Output}$

The output is a sequence of numeric values. A memory stores a value for each identifier, as well as attributes of the identifier such as whether it is writable or not. Values can be numbers, in which case they can be marked read-only or read-write, or functions of $n$ numeric values producing a numeric result:

$\begin{array}{l} \mathsf{Output} =_{\mathit{def}} \mathsf{Num}^* \\ \mathsf{Mem} =_{\mathit{def}} \mathsf{Ide} \rightarrow ((\mathsf{Num} \times \{\mathsf{RO},\mathsf{RW}\}) \;\cup\; ((\mathsf{Num}^* \rightarrow \mathsf{Num}) \times \mathbb{N})) \end{array}$

Remember, we are working with domains, not sets, so all of our domains—Num, Output, etc.—have a special $\bot$ value. The $\bot$ can represent an error in an operation, or some undefined or unknown value, or, in the case of a value looked up from memory, a value that was not found. And all of the operations on domains, such as addition and subtraction on the domain $\mathsf{Num}$, always automatically produce $\bot$ when one of their inputs is $\bot$. This allows a surprisingly concise definition of the semantic functions:

$\mathscr{E}[\![n]\!]\,m = n$ $\mathscr{E}[\![i]\!]\,m = \mathsf{match}\;m(i) = (x,\_)\;\mathsf{in}\;x$ $\mathscr{E} [\![ e_1 \,\mathtt{+}\, e_2 ]\!] \,m = \mathscr{E}e_1 m + \mathscr{E}e_2 m$ $\mathscr{E} [\![ e_1 \,\mathtt{-}\, e_2 ]\!]\,m = \mathscr{E}e_1 m - \mathscr{E}e_2 m$ $\mathscr{E} [\![ e_1 \,\mathtt{*}\, e_2 ]\!]\,m = \mathscr{E}e_1 m \times \mathscr{E}e_2 m$ $\mathscr{E} [\![ e_1 \,\mathtt{/}\, e_2 ]\!]\,m = \mathscr{E}e_1 m \div \mathscr{E}e_2 m$ $\mathscr{E} [\![ e_1 \,\mathtt{\%}\, e_2]\!]\,m = \mathscr{E}e_1 m \;\mathsf{rem}\; \mathscr{E}e_2 m$ $\mathscr{E} [\![ e_1 \,\mathtt{**}\, e_2]\!]\,m = (\mathscr{E}e_1 m)^{\mathscr{E}e_2 m}$ $\mathscr{E}[\![ -e ]\!]\,m = -\mathscr{E}e\,m$ $\mathscr{E} [\![ i\;e_1\!\ldots\!e_n]\!]\,m = \mathsf{match}\;m(i)=(f,n)\;\mathsf{in}\; f(\mathscr{E}e_1 m, \ldots, \mathscr{E}e_n m)$ $\mathscr{S} [\![ i=e ]\!](m,o) = \mathsf{match}\;m(i) = \bot\:|\:(\_,\mathsf{RW}) \;\mathsf{in}\; (m[(\mathscr{E}\,e\,m,\mathsf{RW})\,/\,i], o)$ $\mathscr{S} [\![ \mathtt{print}\;e]\!](m,o) = (m, o\!\cdot\!\mathscr{E}e m)$ $ \begin{array}{l} \mathscr{P}[\![s_1\!\ldots\!s_n]\!] = \\ \;\;\;\;\mathsf{let}\; (m,o) = \mathscr{S}s_n( \ldots \mathscr{S}s_1(m_0,\;()) \ldots ) \;\mathsf{in}\; o \\ \;\;\;\;\;\;\;\; \mathsf{where}\;m_0 = \lambda\,i. \bot \:[\\ \;\;\;\;\;\;\;\;\;\;\;\; (\pi, \mathsf{RO})\,/\,\mathtt{π}] \:[\\ \;\;\;\;\;\;\;\;\;\;\;\; (\lambda x.\sqrt{x}, 1)\,/\,\mathtt{sqrt}] \:[\\ \;\;\;\;\;\;\;\;\;\;\;\; (\lambda x.\sin{x}, 1)\,/\,\mathtt{sin}] \:[\\ \;\;\;\;\;\;\;\;\;\;\;\; (\lambda x.\cos{x}, 1)\,/\,\mathtt{cos}] \:[\\ \;\;\;\;\;\;\;\;\;\;\;\; (\lambda (x,y).\sqrt{x^2+y^2}, 2)\,/\,\mathtt{hypot}] \end{array} $

It is not as bad as it looks, but it definitely is concise.

We’ve introduce a new mathematical operator, $\mathsf{match}$ which should be familiar to Python, Swift, Rust, or OCaml programmers. The mathematical expression $\mathsf{match}\;e_1=e_2\;\mathsf{in}\;e_3$ evaluates to $e_3$ after binding variables in $e_1$ and $e_2$ with each other, or to $\bot$ if nothing matches. As in Python and other programming languages, $e_2$ can have multiple alternatives.

CLASSWORK
We will go over each rule in class.

Loops

Let’s add a while statement to Astro and try to give its semantics. The naive approach fails, though:

$\mathscr{S} [\![ \mathtt{while}\;e\;s ]\!](m,o) = \mathsf{if}\;\mathscr{E}\;e\;m = 0\;\mathsf{then}\;(m,o)\;\mathsf{else}\; \mathscr{S} [\![ \mathtt{while}\;e\;s ]\!](\mathscr{S}\;s\;(m,o))$

This definition is non-compositional. Not a good look. We can get rid of the self-reference like this:

$\begin{array}{l} \mathscr{S} [\![ \mathtt{while}\;e\;s ]\!] = \\ \;\;\;\; \textrm{the function}\;w\;\textrm{such that} \\ \;\;\;\;\;\;\;\; w(m,o) = \mathsf{if}\;\mathscr{E}\;e\;m = 0\;\mathsf{then}\;(m,o)\;\mathsf{else}\; w\;(\mathscr{S}\;s\;(m,o)) \end{array}$

or a little more formally:

$\begin{array}{l} \mathscr{S} [\![ \mathtt{while}\;e\;s ]\!] = w\;\mathsf{where} \\ \;\;\;\;w(m,o) = \mathsf{if}\;\mathscr{E}\;e\;m = 0\;\mathsf{then}\;(m,o)\;\mathsf{else}\; w\;(\mathscr{S}\;s\;(m,o)) \end{array}$
Exercise: Make sure you understand what this is saying: the semantics of a while statement is the function $w$ that transforms the memory and output as follows: If $e$ is falsy, the memory and output stay the same; otherwise the body is executed and then $w$ is executed.

But the problem is that there may be many such functions $w$ that satisfy the desired condition. But when we give a semantics, we want to be able to say there is one, and exactly one, such function. Let’s assume the existence of a function we will call $\mathit{fix}$ that will give us not just a fixed point of a function, but exactly the fixed point we want. If this function could actually be shown to exist, we could write:

$\mathscr{S} [\![ \mathtt{while}\;e\;s ]\!] = \mathit{fix}\;\lambda w. \lambda (m,o). \mathsf{if}\;\mathscr{E}\;e\;m = 0\;\mathsf{then}\;(m,o)\;\mathsf{else}\; w\;(\mathscr{S}\;s\;(m,o))$

While it may look odd at first, but once you get used to it, it’s not so bad.

Exercise: Why was this technique not needed in Operational Semantics?

But what the heck? Why in the world would we ever assume such a function $\mathit{fix}$ even exists, let alone how would we ever go about finding it? That’s a deep, but extremely important question. Short answer is “because we are using domains and not not sets” but this supposes we’re all comfortable with what domains are, and that will wait until we cover domain theory below. We are not ready though. Let’s go back into our comfort zone and look at a more complex language.

Denotational Semantics for Bella

Remember Bella? We saw the official definition of the language earlier with its formal syntax and formal operational semantics.

It’s time for a denotational semantics. We’ll repeat the abstract syntax here for reference:

$ \begin{array}{l} n\!: \mathsf{Numeral} \\ i\!: \mathsf{Identifier} \\ e\!: \mathsf{Expression} = n \;|\; i \;|\; \mathtt{true} \;|\; \mathtt{false} \;|\; \mathit{unaryop} \; e \;|\; e_1 \; \mathit{binop} \; e_2 \;|\; i \; e^* \;|\; e \; \mathtt{?} \; e_1 \; \mathtt{:} \; e_2 \\ s\!: \mathsf{Statement} = \mathtt{let}\;i = e \;|\; \mathtt{func}\;i\;i^*=e \;|\; i = e \;|\; \mathtt{print}\;e \;|\; \mathtt{while}\;e\;b \\ b\!: \mathsf{Block} = \mathtt{block}\; s^* \\ p\!: \mathsf{Program} = \mathtt{program}\; b \end{array} $

Let’s see what the semantic functions should look like:

$\begin{array}{l} \mathscr{E}: \mathsf{Expression} \rightarrow \mathsf{Mem} \rightarrow \mathsf{Num} \\ \mathscr{S}: \mathsf{Statement} \rightarrow \mathsf{State} \rightarrow \mathsf{State} \\ \mathscr{B}: \mathsf{Block} \rightarrow \mathsf{State} \rightarrow \mathsf{State} \\ \mathscr{P}: \mathsf{Program} \rightarrow \mathsf{Output} \end{array}$

Here we rolled up everything manipulated by a statement (a memory and an output) into a new domain called $\mathsf{State}$ (Turing would be proud: programs are like state machines, right?). We also need a new kind of value for Bella: the user-defined function. Such functions are a list of parameters and an expression for the body. So we define:

$\begin{array}{l} \mathsf{Mem}_{\mathit{def}}\;=\;\mathsf{Identifier} \rightarrow \\ \;\;\;\;\;\;\;\; (\mathsf{Num} \times \{\mathsf{RO},\mathsf{RW}\}) \; \cup \\ \;\;\;\;\;\;\;\; (\mathsf{Identifier}^* \times \mathsf{Expression}) \; \cup \\ \;\;\;\;\;\;\;\; ((\mathsf{Num}^* \rightarrow \mathsf{Num}) \times \mathbb{N}) \\ \mathsf{Output}_{\mathit{def}}\;=\;\mathsf{Num}^* \\ \mathsf{State}_{\mathit{def}}\;=\;\mathsf{Mem \times Output} \\ \end{array}$

Here is a fragment of the definitions of the semantic functions:

TODO

CLASSWORK
Finish the semantic function definitions for Bella.

Continuations

TODO

Interesting Language Features

If Statements

TODO

Swap

TODO

For Loops

TODO

Nondeterminism

TODO

Types

Many real programming languages have sophisticated type systems. How do we define what they mean?

Strong Typing

Suppose we wanted to define Bella so that expressions would produce not only numbers, but numbers or booleans. Suppose we also added lists to the language. As in Python. Dynamic types, and all that. And strong typing, too: so type mismatches cause errors rather than type conversions. We begin with a slight change to the signature for the semantic function for expressions:

TODO

And then we enhance the semantic domain of values:

TODO

Static Typing

TODO

Domain Theory

We made a lot of assumptions in the semantic definitions above, among them:

If we’re going to be able to use the mechanisms of denotational semantics, all this stuff has to be grounded in rigorous mathematical foundations. Fortunately, such foundations do exist. First, let’s motivate them.

Errors

TODO

Domains and Partial Orders

TODO

Fixpoints

TODO

Summary

We’ve covered:

  • ...
  • ...