Operational Semantics
Lecture 3
Table of Contents
Review
- Brainstorming a language
- Symbols vs. meaning
- Informal descriptions (C standard)
Questions about the last class?
Quiz
Quiz Discussion
Operational Semantics
Notation
<symbols> => value
- "Symbols" evaluate "value"
- e.g., ASCII evaluates to machine representation or mathmetical object
Number symbols
<2> => 2
- The symbol for two evaluates to the value 2
- Other symbols for two
<two> => 2
<二> => 2
Lots of rules for numbers
<3> => 3
<47> => 47
- How can we define all number symbols?
Meta-symbols
n ::
0 | 1 | 2 | … | 47 | …=- We can use a regular expression for a finite description
- <n> => n
- Symbols for
n
evaluates to the valuen
- Use same meta-symbol for symbol and value for convenience
- Symbols for
- Use n1, n2, etc., when there are multiple numbers involved
Defining operator symbols
<n1 plus n2> => ?
Use mathmetical operations
<n1 plus n2> => n1 + n2
- Can also just use same symbols in our language for convenience
<n1 + n2> => n1 + n2
Proof rules
Proof rule notation
n = n1 + n2 // assuming that n equals n1 plus n2 -------------- // we can conclude that <n1 + n2> => n // the symbols <n1 + n2> evaluate to n
Example
n = 1 + 2 // recall that this is the math operator + ------------ <1 + 2> => n // recall that <1 + 2> are symbols
Axioms
// No assumptions -------- <1> => 1 // Conclusion is defined to be always true -------- <2> => 2 -------- <seven> => 7 -------- <20> => 5 // if you want to make your language confusing
A conclusion without any other assumptions (premises) is defined to be true
For numbers, these are like saying ASCII "2" evaluates to the machine representation for 2
Logical arguments aren't self-justifying, they just mean the argument follows from assumptions. Usually we want at least consistency (no contradictory conclusions).
Permitting contradictions means anything is provable https://math.stackexchange.com/questions/5564/why-an-inconsistent-formal-system-can-prove-everything?noredirect=1&lq=1
i want to prove b pigs can fly
a is true and ¬ a is true
a is true
(b -> a) is true not (a -> not b) is true not (b -> a) is true
Expressions
We can use a grammar to define syntax for expressions
e ::= e + e | e - e | e * e | e / e | (e) | n
Recall that "::=" and pipe "|" are part of the grammar metalanguage
e
is the metasymbol for expressions (nonterminal in grammar terminology)
+ - * / ( )
are all the symbols in the language
Expression example
<(1 + 2) * 7>
=> 21
Proof rules for arithmetic
<e1> => n1 <e2> => n2 n = n1 * n2 ----------------------------------------- <e1 * e2> => n
Assuming that <e1> => n1
, <e2> => n2
, and n = n1 + n2
We can conclude that <e1 * e2>
evaluates to n
Proof rules for arithmetic
<e1> => n1 <e2> => n2 n = n1 * n2 ----------------------------------------- <e1 * e2> => n <e1> => n1 <e2> => n2 n = n1 + n2 ----------------------------------------- <e1 + e2> => n
Similarly for other arithmetic operations
Using proof rules
We'll prove that (1 + 2) * 7 evaluates to 21
<(1 + 2) * 7> => 21
We can work backwards
<(1 + 2) * 7> => 21
Apply the multiplication rule
<1 + 2> => 3 <7> => 7 21 = 3 * 7 ---------------------------------------------------------- <(1 + 2) * 7> => 21
Apply the addition rule to the remaining expression
<1> => 1 <2> => 2 3 = 1 + 2 --------------------------------- <1 + 2> => 3 <7> => 7 21 = 3 * 7 ---------------------------------------------------------- <(1 + 2) * 7> => 21
Think of proof rules as an interpreter
- Evaluator walks syntax tree, returns final value
int evaluate(Node expression)
- Each rule evaluates one language construct
- Proof rules implicily use dynamic dispatch
Think of proof rules as an interpreter
// proof rule for addition <e1> => n1 <e2> => n2 n = n1 + n2 // body of function ----------------------------------------- <e1 + e2> => n // function signature // evaluation function in C int evaluateAdd(e1, e2) { n1 = evaluate(e1); // dynamic dispatch n2 = evaluate(e2); n = n1 + n2; return n; }
Think of the classic infix expression calculator example
- Get syntax tree (or use stack to process infix expressions)
- Evaluate each operation
- Recursively evaluate operands
Variable substitution
Seems kind of tautological
Not too useful when symbols are the same as values
How do we model variables
x = 10 + y print(x * 7)
How do we derive the value of <x * 7>
?
Applying proof rules
??? -------- <x> => ? <7> => 7 n = ? * 7 --------------------------------- <x * 7> => n
Modeling state
- Variables are a mapping
s
from names to valuess' = s.put(name, value)
value = s.get(name)
Adding a symbol creates a fresh mapping (doesn't mutate s
)
s'
is "s prime", common shorthand for making a fresh symbol
lookup returns the value
Tracking state in proof rules
- New notation
s : <1 + 3> => 4
- "under the current context s, <1 + 3> evaluates to the value 4"
s
is sometimes called the "context", the "environment", or a "configuration variable"
Variable expansion rule
n = s.lookup(x) // assuming that n is the value of x in the state --------------- // we can conclude that s : <x> => n // <x> evaluates to the value n
Finishing the example
s : <x> => n1 <7> => 7 --------------------------- s : <x * 7> => n
Finishing the example
n1 = s.lookup(x) // apply variable substitution rule ---------------- s : <x> => n1 <7> => 7 n = n1 * 7 --------------------------- s : <x * 7> => n
Variable assignment
So we have some notation for defining semantics as an evaluator. Who cares?
Can start to notate differences in language design choices.
For example, the behavior of variable assignment.
Example
x = 10 + y print(x * 7)
Assignment rule
s : <x = 10 + y> => ??
What does it mean to evaluate an assignment?
Some choices
- C-like, x = y = z, assignment is an expression that returns a value
- Only updates context (side-effects)
- No updates to context (no side-effects, functional-style let-binding expressions)
One choice: update state for later use
s : <x = 10 + y> => s'
Imperative-style: update memory location. Later expressions can access memory via variable substitution.
When to evaluate the definition?
// evaluate at define-time <e> => n s' = s.put(x, n) ---------------------- s : <x = e> => s'
How should variable assignment work?
A couple of choices:
- evaluate rhs at define time
- evaluate rhs at use time
For instance, make
has both
https://www.gnu.org/software/make/manual/html_node/Flavors.html#Flavors
When to evaluate the definition?
// evaluate at define-time (eager evaluation) <e> => n s' = s.put(x, n) ---------------------- s : <x = e> => s' // store expression for evaluation at use-time (lazy evaluation) s' = s.put(x, e) // store expression itself ---------------------- s : <x = e> => s'
Storing expression requires a different kind of store, one that maps symbols to expressions rather than values.
Ramifications of lazy evaluation
- What should be printed here?
y = 1 x = 10 + y y = 2 print(x * 7)
Do we use the first definition of y? The second?
Side effects complicate lazy evaluation
One solution: no side effects, i.e., no reassignment of variables
Potential personal project: implement alternative variable evaluation semantics on top of our common language
Example of eager evaluation
<e> => n s.put(x, n) ------------------------------------ s : <x = e> => s'
Example of eager evaluation
<10 + y> => ?? s.put(x, ??) ------------------------------------- {y=1} : <x = 10 + y> => {y=1; x=??}
Example of eager evaluation
<10> => 10 <y> => ? --------------------- <10 + y> => ?? s.put(x, ??) ------------------------------------- {y=1} : <x = 10 + y> => {y=1; x=??}
Example of eager evaluation
1 = s.lookup(y) --------------- <10> => 10 <y> => ? --------------------- <10 + y> => ?? s.put(x, ??) ------------------------------------- {y=1} : <x = 10 + y> => {y=1; x=??}
Example of eager evaluation
1 = s.lookup(y) --------------- <10> => 10 <y> => 1 --------------------- <10 + y> => 11 s.put(x, 11) ------------------------------------- {y=1} : <x = 10 + y> => {y=1; x=11}
Additional Resources
Homework
Write a proof that 一加二乘三 evaluates to 7, i.e., <一加二乘三> => 7, using the following semantic rules
-------------- <一> => 1 -------------- <二> => 2 -------------- <三> => 3 <e1> => n1 <e2> => n2 n = n1 + n2 ------------------------------------- <e1 加 e2> => n <e1> => n1 <e2> => n2 n = n1 * n2 ------------------------------------- <e1 乘 e2> => n