Semantics: attribute grammar and weakest precondition
Attribute grammars for static semantics (synthesized, inherited, intrinsic), the three methods of dynamic semantics, and computing weakest preconditions with axiomatic semantics.
Syntax defines the form of a program; but rules like “a variable must be declared before it is used” or “a real cannot be assigned to an int” cannot be written in BNF. This page covers two things: the attribute grammar for writing these rules and three methods for defining the meaning (semantics) of a program. The two question types that come up most on exams: type checking with an attribute grammar and computing a weakest precondition.
Golden rules:
- Synthesized flows up, inherited flows down.
- First look-up, then expected_type, then actual_type, and finally the predicate.
- The weakest precondition is computed from the end to the beginning. In place of the variable in the postcondition, write the right-hand side of the assignment.
- When you divide by a negative number the inequality changes direction.
Static semantics
Some rules are checked at compile time but cannot be expressed with a context-free grammar, or expressing them is very laborious:
- In Java, a float value cannot be assigned to an int variable.
- All variables must be declared before use.
- In Ada, the name after
endat the end of a subprogram must be the same as the name at its start.
These are called static semantics: rules related to meaning but that can be checked at compile time, without running. Although it is called “semantics”, it is actually close to syntax.
Attribute grammar
An attribute grammar adds three things to a CFG:
- Attributes: values attached to each symbol (
actual_type,expected_type). - Semantic functions (attribute computation functions): rules attached to each rule, saying how the attributes are computed.
- Predicates: conditions that must be true for the rule to be valid. If one is false there is a static semantics error.
There are three kinds of attributes:
| Kind | Direction of flow | Example |
|---|---|---|
| Synthesized | From children to parent, up | <expr>.actual_type is computed from the operands |
| Inherited | From parent/sibling to child, down or sideways | <expr>.expected_type, comes from its sibling <var> |
| Intrinsic | At the leaves, from outside | look-up(<var>.string) from the symbol table |
A type checking example
1. <assign> -> <var> = <expr>
<expr>.expected_type <- <var>.actual_type
2. <expr> -> <var>[2] + <var>[3]
<expr>.actual_type <- if (<var>[2].actual_type = int) and
(<var>[3].actual_type = int) then int else real
predicate: <expr>.actual_type == <expr>.expected_type
3. <expr> -> <var>
<expr>.actual_type <- <var>.actual_type
predicate: <expr>.actual_type == <expr>.expected_type
4. <var> -> A | B | C
<var>.actual_type <- look-up(<var>.string)
The bracketed indices (<var>[2]) distinguish different instances of the same nonterminal. For A = B + C, let A be real, B real, C int:
step value
1. <var>.actual_type <- look-up(A) real (intrinsic)
2. <expr>.expected_type <- real real (inherited)
3. <var>[2] = look-up(B), <var>[3] real, int (intrinsic)
4. <expr>.actual_type real (synthesized: both are not int)
5. predicate real == real TRUE
If A were int, B int, C real: expected int, actual real, predicate false. This grammar does no automatic conversion (coercion); even A real, B int, C int gives an error because actual is int and expected is real.
A language that is not context-free
The language cannot be written with a CFG. With an attribute grammar it is easy:
<S> -> <A> <B> <C> predicate: <A>.size == <B>.size == <C>.size
<A> -> a | <A>[2] a <A>.size <- 1 | <A>.size <- <A>[2].size + 1
(the same for B and C)
The CFG part accepts the form a+b+c+. aabbcc passes; aabbc is syntactically correct but the predicate is false.
Dynamic semantics: three methods
| Method | How meaning is defined | Who uses it |
|---|---|---|
| Operational | The state changes the program makes while running on a (virtual) machine | Language manuals, teaching |
| Denotational | Mapping every construct to a mathematical object/function | Theoretical work, proofs, compiler generation |
| Axiomatic | Logical assertions that are true before and after statements | Program verification |
Operational semantics
It gives the meaning of a high-level construct by translating it into a simpler and well-known language. C’s for loop:
for (expr1; expr2; expr3) { ... }
expr1;
loop: if expr2 == 0 goto out
...
expr3;
goto loop
out: ...
Even if the reader doesn’t know for, if they know if and goto they can work out the meaning.
Denotational semantics
A mathematical function is defined for each syntactic construct. The binary number example:
M_bin('0') = 0
M_bin('1') = 1
M_bin(<bin_num> '0') = 2 * M_bin(<bin_num>)
M_bin(<bin_num> '1') = 2 * M_bin(<bin_num>) + 1
For 101: ‘1’ → 1, ‘10’ → 2, ‘101’ → 5. For 10110 it is 22. In programs the state consists of the values of the variables; each statement is a function from one state to another.
Axiomatic semantics
For a statement S, the following triple is called a Hoare triple:
P is the precondition, Q is the postcondition. Meaning: if P is true before S runs, then Q is true when S finishes. Given Q, the least restrictive precondition that guarantees Q is called the weakest precondition.
The assignment axiom
For the assignment and postcondition Q:
Write E in place of every x in Q, and simplify. Example: x = 2 * y - 3 {x > 7}:
Make checking a habit: y = 6 → x = 9 > 7 true; y = 5 → x = 7, 7 > 7 false. The boundary is correctly excluded.
PITFALL · a negative coefficient.
a = -2 * b + 4 {a < 0}: . When you divide both sides of the inequality by -2 the direction flips. Whoever writes b < 2 is toast. Check by trying a value: b = 3 → a = -2 < 0, correct.
Sequences of statements
For first find the weakest precondition of ; it becomes the postcondition of :
y = x + 4;
x = y * 2;
{x < 20}
step 1 (last assignment): 2y < 20 → y < 10
step 2 (first assignment): x + 4 < 10 → x < 6
result: {x < 6}
Selection
For if B then S1 else S2:
if (x > 0) y = x - 1; else y = x + 1; {y > 0}: the then branch gives x > 1 (combined with x > 0 it is still x > 1). The else branch gives x > -1; combined with x ≤ 0, only x = 0 for integers. Answer: x > 1 or x = 0.
A stronger precondition is also valid
{x > 5} x = x - 3 {x > 0} is a valid triple, but it is not the weakest. The weakest is x > 3. Rule of consequence: if P’ ⇒ P and {P} S {Q} is valid, then {P’} S {Q} is also valid. So any precondition stronger than the weakest precondition works; a weaker one does not (for x ≥ 3, x = 3 → 0 > 0 is false).
Loops
For while B do S the weakest precondition cannot be computed directly; you need to find a loop invariant I. I must be true before the loop, must stay true after each iteration, and when the loop ends () it must imply Q. Finding an invariant is like induction and is usually asked superficially on the midterm: “what is an invariant, which three conditions must it satisfy?”
Common mistakes
| Wrong | Correct |
|---|---|
| Computing wp from beginning to end | Go backward from the postcondition at the end |
| Keeping the direction when dividing by -2 | When dividing by a negative, < ↔ > |
| Synthesized = flows down | Synthesized up, inherited down |
| expected_type is synthesized | It is inherited because it comes from a sibling |
| “A stronger precondition is invalid” | Valid, but not the weakest |
| Static semantics at run time | Checked at compile time |
| Attribute grammar = CFG | CFG + attributes + functions + predicates |
| Denotational = running on a machine | That is operational; denotational is a mathematical function |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
Using the type-checking attribute grammar below, does the predicate hold for A = B + C when A is int, B is int and C is real?
1. <assign> -> <var> = <expr>
<expr>.expected_type <- <var>.actual_type
2. <expr> -> <var>[2] + <var>[3]
<expr>.actual_type <- if (<var>[2].actual_type = int) and
(<var>[3].actual_type = int) then int else real
predicate: <expr>.actual_type == <expr>.expected_type
3. <expr> -> <var>
<expr>.actual_type <- <var>.actual_type
predicate: <expr>.actual_type == <expr>.expected_type
4. <var> -> A | B | C
<var>.actual_type <- look-up(<var>.string)Show answer
Order: first the leaves get their types with look-up (A int, B int, C real). Rule 1: expected_type ← the type of A = int. Rule 2: both operands are not int → actual_type = real. Predicate: real == int → false. The compiler reports this as a type error at compile time.
Same grammar: does the predicate hold for A = B + C when A is real, B is int and C is int?
Show answer
actual_type: both operands are int → int. expected_type: the type of A is real. int == real is false → the predicate is false. This grammar does no coercion; the types must be exactly the same. (In the case A real, B real, C int, however, actual = real, expected = real → true.)
In the grammar above, classify actual_type, expected_type, and the type that look-up returns for a <var> leaf.
Show answer
Synthesized: information flows from the children up to the parent (<expr>.actual_type is computed from the operands). Inherited: information flows down or sideways from the parent or a sibling (<expr>.expected_type comes from its sibling <var>). Intrinsic: the value at a leaf comes from outside, from the symbol table.
An attribute grammar for uses <S> -> <A> <B> <C>, where each of <A>, <B>, <C> has a synthesized attribute size and the predicate is A.size = B.size = C.size. Which strings pass both syntax and the predicate? aabbcc, aabbc, abc, aabcc, abca
Show answer
The CFG part accepts the form a+ b+ c+, and the predicate checks the counts. aabbcc: 2,2,2 ✔. abc: 1,1,1 ✔. aabbc: 2,2,1 ✘ (predicate false). aabcc: 2,1,2 ✘. abca: wrong form, syntax error. This language is not context-free; a CFG alone cannot generate it, and this is where the power of the attribute grammar lies.
Compute the weakest precondition: x = 2 * y - 3 {x > 7}
Show answer
Assignment axiom: write 2y - 3 in place of x in the postcondition. . Check: y = 6 → x = 9 > 7 ✔; y = 5 → x = 7, 7 > 7 is false ✔ (the boundary is excluded).
Compute the weakest precondition of the sequence: y = x + 4; x = y * 2; {x < 20}
Show answer
Work from the end to the beginning. Last assignment: . This becomes the postcondition of the first assignment. First assignment: . Check: x = 5 → y = 9 → x = 18 < 20 ✔; x = 6 → y = 10 → x = 20 ✘.
Compute the weakest precondition: a = -2 * b + 4 {a < 0}
Show answer
. When you divide by a negative number the inequality changes direction: . Check: b = 3 → a = -2 < 0 ✔; b = 2 → a = 0 ✘. The most common mistake is forgetting to flip the sign and writing b < 2.
For integer x, compute the weakest precondition: if (x > 0) y = x - 1; else y = x + 1; {y > 0}
Show answer
Selection rule: . Then branch: ; together with x > 0, x > 1. Else branch: ; together with x ≤ 0, only x = 0 for integers. Result: x > 1 or x = 0. (For real numbers the else part would be .)
Which preconditions make the triple {P} x = x - 3 {x > 0} valid? (a) x > 0 (b) x > 3 (c) x > 5 (d) x >= 3
Show answer
The weakest precondition is x > 3. Every valid P must imply it (P ⇒ x > 3). x > 5 is stronger but valid. x > 0 and x ≥ 3 are invalid: for x = 3 the result is 0, and 0 > 0 is false. Weakest = the least restrictive of the valid ones.
Using the denotational rules below, compute of 10110.
M_bin('0') = 0
M_bin('1') = 1
M_bin(<bin_num> '0') = 2 * M_bin(<bin_num>)
M_bin(<bin_num> '1') = 2 * M_bin(<bin_num>) + 1Show answer
It accumulates from left to right: '1' → 1; '10' → 2·1 = 2; '101' → 2·2 + 1 = 5; '1011' → 2·5 + 1 = 11; '10110' → 2·11 = 22. Denotational semantics maps every syntactic construct to a mathematical object (here an integer).
Name the dynamic semantics method described: (a) meaning is the sequence of state changes when the program runs on a virtual machine; (b) meaning is a mathematical function from states to states; (c) meaning is given by logical assertions before and after statements.
Show answer
Operational: think of it as translating the program into a simpler machine language and running it; used in language manuals and teaching. Denotational: the most rigorous method, mathematical functions. Axiomatic: precondition/postcondition assertions, the goal is program verification.
Compute the weakest precondition: b = a + 1; a = 3 * b; {a > 12}
Show answer
Last assignment: . First assignment: . Note: the a at the end and the a at the start are the same variable but with values at different moments; going from the end to the beginning, each assignment changes its own variable.
Verification: The attribute grammar type checks, the a^n b^n c^n predicate and the denotational binary number values were computed with node; every weakest precondition answer was compared by brute force, running the program itself on all integers in the range -50..50 (verify/SEN2001/03a.mjs).