Semantics
MotivationMaterial adapted from CSCI 3136 Principles of Programming Languages at Dalhousie University, and from Mark Hills’ CSCI 3675 Programming Languages
Semantic analysis is the third step in compilation, as shown in Figure 1, from Programming Language Pragmatics, by Michael L. Scott.
Figure 1. The syntax analysis converts the parse tree into an abstract syntax tree or other intermediate form.
Syntax and semantics
Syntax
-
Describes form of a valid program
-
Can be described by a context-free grammar
Semantics
-
Describes meaning of a program
-
Cannot be be described by a context-free grammar
Some constraints that may appear syntactic are enforced by semantic analysis:
- E.g., use of identifier only after its declaration
Semantic analysis
-
Enforces semantic rules
-
Builds intermediate representation (e.g., abstract syntax tree)
-
Fills symbol table
-
Passes results to intermediate code generator
Two approaches
-
Interleaved with syntactic analysis
-
As a separate phase
Formal mechanism: Attributes grammars
Enforcing semantic rules
Static semantic rules
-
Enforced by compiler at compile time
-
Example: Do not use undeclared variable
Dynamic semantic rules
-
Compiler generates code for enforcement at run time
-
Examples: division by zero, array index out of bounds
-
Some compilers allow these checks to be disabled
Varieties of semantics
There are many methods of giving semantics to a programming language. Several common methods include:
-
Operational semantics
-
Axiomatic semantics
-
Denotational semantics
Note that:
-
it may be easier to represent certain languages with certain types of semantics
-
the types of semantics are complementary - they are good for different purposes, with no method “the best”
Operational semantics
-
First, start with a definition of our “machine” which we run programs on
-
The machine state, including the program, is often called the configuration
-
Next, describe how to execute programs in a given language by describing how to execute individual statements and parts of statements - rules follow the structure of the program
-
A program’s “meaning” is defined by how it changes the configuration
-
Useful as a basis for implementations
Axiomatic semantics
Axiomatic semantics are also called Floyd-Hoare logic
-
based on logic - first-order predicate calculus
-
semantics represented as a logical system build from axioms and inference rules
-
mainly suited to simple imperative languages
-
used to prove a post-condition from a pre-condition: given something holds in the starting state, we can show something else holds in the end state
Denotational semantics
In denotational semantics, we want to find the meaning, or denotation, of phrases in our language.
-
we construct a function \(\mathcal{M}\) assigning a mathematical meaning to each language construct;
-
these functions are compositional - we can construct the meaning of a language construct by composing the meanings of its components
-
useful for proving properties of programs - used for early type soundness proofs and in theorem provers (plus elsewhere)
Alternative methods
There are many other methods which have been devised to give semantics to languages.
-
“Compiler-based” semantics - the meaning is whatever the compiler says it is
-
Abstract Machines - similar to operational, an abstract machine with instructions, etc is devised and used to give semantics
-
Term Rewriting - semantics are formulated as term rewriting systems, with evaluation given by the rewriting relation
-
Rewriting Logic - an extension of equational logic that provides for concurrency
Transition semantics
Transition semantics is a form of operational semantics.
-
Configurations include the code and machine state: \((C,m)\)
-
Semantics specified as transitions between configurations, altering the machine state
-
Rules of the form: \(\langle C,m \rangle \rightarrow \langle C',m' \rangle\)
-
\(C\), \(C'\) represents the code yet to be executed
-
\(m\), \(m'\) represents the state (store, memory, etc), often a finite map from names to values
-
May not need \(m\) - simple calculator languages with only numbers and operations don’t, for instance
Key point: each transition indicates exactly one step of computation
IMP - a simple imperative language
We can use a simple imperative language, IMP, as a sample language for semantics. This language has the following syntactic categories:
-
numbers N, which are the integers (including negatives)
-
truth values \(\mathbf{T} = \lbrace \mathbf{true},\mathbf{false} \rbrace\)
-
locations Loc
-
arithmetic expressions Aexp
-
boolean expressions Bexp
-
commands Com
For shorthand, \(n,m \in \mathbf{N}; X,Y \in \mathbf{Loc}; a \in \mathbf{Aexp}; b \in \mathbf{Bexp}; c \in \mathbf{Com}\).
IMP syntax
Using a variant of BNF, we can specify the syntax for IMP as: \(\begin{align*} & Aexp & a & ::= & & n\ |\ X\ |\ a_0 + a_1\ |\ a_0 - a_1\ |\ a_0 \times a_1 & \\ & Bexp & b & ::= & & \mathbf{true}\ |\ \mathbf{false}\ |\ a_0 = a_1\ |\ a_0 \leq a_1\ |\ \neg b\ |\ b_0 \wedge b_1\ |\ b_0 \vee b_1 & \\ & Com & c & ::= & & \mathbf{skip}\ |\ X\ :=\ a\ |\ c_0; c_1\ |\ \mathbf{if}\ b\ \mathbf{then}\ c_0\ \mathbf{else}\ c_1\ |\ \\ & & & & & \mathbf{while}\ b\ \mathbf{do}\ c & \\ \end{align*}\)
Important Point: We assume reasonable input programs that parse and we don’t care about precedence, associativity, etc - we assume all that has been figured out for us
IMP configurations
Our configurations will contain two elements:
-
The code (\(a\), \(b\), or \(c\))
-
The state
We can define the set of states \(\Sigma\) as functions \(\sigma: \mathbf{Loc} \rightarrow \mathbf{N}\), or functions from locations to integer values.
Arithmetic rules and simple expressions
Our semantic relation for arithmetic expressions will be of the form:
We assume arithmetic expressions have no side-effects.
- We have a simple axiom for numbers:
- Location lookup is also similar:
Arithmetic expressions: sums

Arithmetic expressions: subtraction

Arithmetic expressions: products

Boolean rules and simple expressions
Our semantic relation for boolean expressions will be of the form:
We assume boolean expressions have no side-effects.
- We have simple axioms for boolean constants, including true:
- and false:
Boolean expressions: equality

The rules for \(\leq\) are similar.
Boolean expressions: and

The rules for \(\vee\) and \(\neg\) are similar.
Commands
While expressions in our language cannot have side effects, commands can. So, here we need to model the changes in state that occur when commands run. Here, our semantic relation will be of the form:
So, when command \(c\) is fully evaluated, potentially altered memory \(\sigma'\) is returned.
Simple commands
The \(\mathbf{skip}\) command does not alter the state:
Assignment - some new notation
The assignment command does alter the state. One way we can view it is we get back a new state function which is the same everywhere except at the location we’ve updated, which now holds the new value. We can define this as:
Assignment
With our new notation, assignment can be shown as follows. Note we reduce the expression we are assigning to \(X\) first, before we do the actual assignment.

Sequencing
We can sequence commands as well in our language. We always complete execution of the first command before starting on the second. This gives us the following semantic rules:

Conditionals
We have a conditional statement in our language. We need to evaluate the guard first before deciding which branch to take. We can represent this as:

Loops
We have one loop, the while command. Here, we need to evaluate the guard - if it is still true, we want to evaluate the body, and we want to then evaluate the loop again. In some sense, this takes us back where we started, but most likely with a different state (if not, we probably won’t terminate). We will expand to a conditional to do this, or else we would “lose” the guard when we evaluate it.
Example
To see how we can “evaluate” something operationally, start with:
In this state, we have at some point assigned the value \(7\) to location \(x\).
Example
Since this is a conditional, we first need to evaluate the guard until we get a value, either \(\mathbf{true}\) or \(\mathbf{false}\);

Example
Now that we just have values in the relational expression, we can use the appropriate rule to determine the truth or falsity of the guard.

Example
Now that we have a truth value for the guard, we can use the appropriate conditional rule to pick the correct statement to continue with.
Example
We can now evaluate the arithmetic expression, since we need to have an integer value before we can do assignment.

Example
Finally, we can use the assignment rule to assign a value to y.
Execution summary
Evaluation in transition semantics
As we saw above, we can view evaluation as a sequence of steps with trees of justifications for each step. This gives us a sequence of evaluation steps:
We can then define \(\rightarrow^*\) as the transitive closure of \(\rightarrow\), essentially giving us a relation that evaluates from \(\langle C_i,m_i \rangle\) to \(m\) (or, from a starting program and state to the final state).
Adding functions and local bindings
We don’t currently have functions or let expressions in our language. How would adding them impact our semantics? We will change arithmetic expressions to just expressions (no need to change anything else defined so far, this just gives it a name more consistent with it’s new use) and add syntax there.
\[\begin{align*} & AExp & a & ::= & & n\ |\ X\ |\ a_0 + a_1\ |\ a_0 - a_1\ |\ a_0 \times a_1\ |\ & \\ & & & & & \mathbf{let}\ X = a_0\ \mathbf{in}\ a_1\ |\ \mathbf{fun}\ X\ \rightarrow\ a\ |\ a_0\ a_1& \\ & Bexp & b & ::= & & \mathbf{true}\ |\ \mathbf{false}\ |\ a_0 = a_1\ |\ a_0 \leq a_1\ |\ \neg b\ |\ b_0 \wedge b_1\ |\ b_0 \vee b_1 & \\ & Com & c & ::= & & \mathbf{skip}\ |\ X\ :=\ a\ |\ c_0; c_1\ |\ \mathbf{if}\ b\ \mathbf{then}\ c_0\ \mathbf{else}\ c_1\ |\ \\ & & & & & \mathbf{while}\ b\ \mathbf{do}\ c & \\ \end{align*}\]Semantics for functions and lets
-
To keep track of bindings, we could use an environment, similar to the type environment we used for types - but in this case a finite map from locations to values
-
This would “violate” what we mean by expression, though - an expression would then have a side effect
-
So, we will use substitution here instead - \([E'/X]E\), meaning to replace all free \(X\) by \(E'\) in \(E\)
We also have one axiom - a function, without application, just evaluates to itself:
Call by value
Using call-by-value, we will evaluate terms before substitution. First, the let expression:
Call by value
Next, function application:
Call by name
Using call-by-name, we will perform substitution first. This lets us get by without additional rules to reduce the arguments first:
\[\langle \mathbf{let}\ X = a_0\ \mathbf{in}\ a_1,\sigma \rangle \rightarrow\langle [a_0/X]a_1, \sigma \rangle\] \[\langle (\mathbf{fun}\ X \rightarrow a_0) a_1, \sigma \rangle \rightarrow\langle [a_1/X]a_0, \sigma \rangle\]Call by value vs. call by name
Question: Is there any difference between the two?
Answer: Maybe, depending on the language…
Church-Rosser
A language (reduction system) that is Church-Rosser is one in which reduction order does not affect the final result (although it can affect convergence): if \(E \rightarrow^{*} E_1\) and \(E \rightarrow^{*} E_2\) then there exists a value \(V\) such that \(E_1 \rightarrow^{*} V\) and \(E_2 \rightarrow^{*} V\). This is also known as confluence or the diamond property.

Are all systems Church-Rosser?
No. Especially considering side-effects, most languages are not Church-Rosser.
-
Anonzo Church and Barkley Rosser proved that the \(\lambda\)-calculus is Church-Rosser in 1936
-
One benefit - can check equality of terms by evaluating them both to canonical forms (if this terminates!)
ICE on transition semantics
Using the semantic rules, show the steps taken to a final value for the following program. If this program does not terminate, write diverges. For this program, the state begins empty (i.e., there are no mappings from names to values).
\[X := 1 + (2 * 3)\]Natural semantics
Natural semantics are similar to transition semantics except
-
transition semantics specifies a relation between individual steps of a computation
-
while natural semantics specifies a relation between a state in the computation and a final result
Rules will be of the form
And it should be the case that
An aside
Both transition semantics and natural semantics are forms of operational semantics. Transition semantics is also known as structural operational semantics, or small-step semantics. Natural semantics is also known as big-step semantics.
Natural semantics of atomic expressions
In both cases we only take one step, so the semantics are roughly the same:
-
Identifiers: \(\langle X,\sigma \rangle \Downarrow \sigma(X)\)
-
Numerals: \(\langle n,\sigma \rangle \Downarrow n\)
-
Booleans:
Arithmetic expressions
Since the rules for addition, subtraction, and multiplication all look the same except for the operator used, we will just let \(\texttt{op} = \lbrace +,-,\times \rbrace\) below:

Relational operations
We again use {\tt op} here to stand for a number of operations which all have similar rules. Let \(\texttt{op} = \lbrace =, \leq \rbrace\) below:

Logical operations: and
Since our and operation “short-circuits”, we need two rules, one that tells us the result when the first operation is false, and one that tells us the result when the first operation is true:

Logical operations: or
Or is similar to and - both “short-circuit”, so we also need two rules here. Note the main difference is that we flip around the value used to decide whether to continue with the second argument:

Logical operations: not
Negation is fairly simple - we can just handle it with two cases, one for evaluating the first argument to true, the other for false:

Commands: skip
Similarly to how expressions are handled, we will evaluate commands completely, yielding the state after the command is executed. Skip is defined similarly to the definition in transition semantics:
Commands: assignment
With assignment, we build in the evaluation of the expression directly into the assignment rule, instead of requiring two steps.

Commands: sequencing
Sequencing again requires we completely evaluate the component commands. Here, we can directly see the result of the first command being “passed on” to the second.

Commands: conditional
We make the choice directly in the rules premises, since we are required to evaluate to the final state.

Commands: while
The rule for the while command is “recursive”, in that it refers to itself. One important point to note is that we can use while directly here, instead of needing to wrap it inside an if like we did with the transition semantics.

Example
To see how we can “evaluate” something with natural semantics, start with:
In this state, we have at some point assigned the value \(7\) to location \(x\).
Example
Example
Example
Example
Example
Example
Example
Example
Example
Example
Example
Example
Commands: let
The rule for let is slightly different than in our transition semantics example - here we allow the let body to be a command, not just an arithmetic expression. This means we now could have side effects in the let body.

Note that, overall, we return \(\sigma"\), not \(\sigma'\). \(\sigma"\) is defined to be identical to \(\sigma'\) everywhere except \(x\) - for \(x\), it is defined as identical to \(\sigma\) if \(x\) is defined in \(\sigma\), else it is undefined. Why? Because we need to propagate all state changes, but also need to enforce scope.
Example

Example

The let construct, with the need to track state, somewhat conflicts with the assignment operation, leading to semantics that are a bit clumsy.
Why have two semantics?
We now have natural and transition semantics for our language. Why have both?
-
Natural semantics has a nice correspondence to a recursive interpreter for our programs
-
Transition semantics corresponds nicely to an imperative interpreter, taking individual steps and modifying the state
-
Natural semantics provides added conciseness…
-
…but cannot model nontermination (and cannot distinguish nontermination from some types of failures)