SEN2001 · Week 3 · Describing Syntax and Semantics II

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.

static semanticsattribute grammarsynthesized attributeinherited attributeintrinsic attributepredicateoperational semanticsdenotational semanticsaxiomatic semanticspreconditionpostconditionweakest preconditionassignment axiomloop invariant

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:

  1. Synthesized flows up, inherited flows down.
  2. First look-up, then expected_type, then actual_type, and finally the predicate.
  3. 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.
  4. 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 end at 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:

KindDirection of flowExample
SynthesizedFrom children to parent, up<expr>.actual_type is computed from the operands
InheritedFrom parent/sibling to child, down or sideways<expr>.expected_type, comes from its sibling <var>
IntrinsicAt the leaves, from outsidelook-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 anbncna^n b^n c^n 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

MethodHow meaning is definedWho uses it
OperationalThe state changes the program makes while running on a (virtual) machineLanguage manuals, teaching
DenotationalMapping every construct to a mathematical object/functionTheoretical work, proofs, compiler generation
AxiomaticLogical assertions that are true before and after statementsProgram 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}  S  {Q}\{P\}\; S \;\{Q\}

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 x=Ex = E and postcondition Q:

wp(x=E, Q)=Qx→Ewp(x = E,\ Q) = Q_{x \to E}

Write E in place of every x in Q, and simplify. Example: x = 2 * y - 3 {x > 7}:

2y−3>7  ⇒  2y>10  ⇒  y>52y - 3 > 7 \;\Rightarrow\; 2y > 10 \;\Rightarrow\; y > 5

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}: −2b+4<0⇒−2b<−4⇒b>2-2b + 4 < 0 \Rightarrow -2b < -4 \Rightarrow b > 2. 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 S1; S2S_1;\ S_2 first find the weakest precondition of S2S_2; it becomes the postcondition of S1S_1:

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:

wp=(B∧wp(S1,Q))∨(¬B∧wp(S2,Q))wp = (B \wedge wp(S_1, Q)) \vee (\neg B \wedge wp(S_2, Q))

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 (I∧¬BI \wedge \neg B) 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

WrongCorrect
Computing wp from beginning to endGo backward from the postcondition at the end
Keeping the direction when dividing by -2When dividing by a negative, < ↔ >
Synthesized = flows downSynthesized up, inherited down
expected_type is synthesizedIt is inherited because it comes from a sibling
“A stronger precondition is invalid”Valid, but not the weakest
Static semantics at run timeChecked at compile time
Attribute grammar = CFGCFG + attributes + functions + predicates
Denotational = running on a machineThat is operational; denotational is a mathematical function

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1medium

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
No, the predicate fails

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.

Question 2medium

Same grammar: does the predicate hold for A = B + C when A is real, B is int and C is int?

Show answer
No

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.)

Question 3easy

In the grammar above, classify actual_type, expected_type, and the type that look-up returns for a <var> leaf.

Show answer
actual_type: synthesized; expected_type: inherited; look-up result: intrinsic

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.

Question 4medium

An attribute grammar for anbncna^n b^n c^n 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
aabbcc and abc

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.

Question 5easy

Compute the weakest precondition: x = 2 * y - 3 {x > 7}

Show answer
{y > 5}

Assignment axiom: write 2y - 3 in place of x in the postcondition. 2y−3>7⇒2y>10⇒y>52y - 3 > 7 \Rightarrow 2y > 10 \Rightarrow y > 5. Check: y = 6 → x = 9 > 7 ✔; y = 5 → x = 7, 7 > 7 is false ✔ (the boundary is excluded).

Question 6medium

Compute the weakest precondition of the sequence: y = x + 4; x = y * 2; {x < 20}

Show answer
{x < 6}

Work from the end to the beginning. Last assignment: 2y<20⇒y<102y < 20 \Rightarrow y < 10. This becomes the postcondition of the first assignment. First assignment: x+4<10⇒x<6x + 4 < 10 \Rightarrow x < 6. Check: x = 5 → y = 9 → x = 18 < 20 ✔; x = 6 → y = 10 → x = 20 ✘.

Question 7hard

Compute the weakest precondition: a = -2 * b + 4 {a < 0}

Show answer
{b > 2}

−2b+4<0⇒−2b<−4-2b + 4 < 0 \Rightarrow -2b < -4. When you divide by a negative number the inequality changes direction: b>2b > 2. Check: b = 3 → a = -2 < 0 ✔; b = 2 → a = 0 ✘. The most common mistake is forgetting to flip the sign and writing b < 2.

Question 8hard

For integer x, compute the weakest precondition: if (x > 0) y = x - 1; else y = x + 1; {y > 0}

Show answer
{x > 1 or x = 0}

Selection rule: (x>0∧wp1)∨(x≤0∧wp2)(x > 0 \wedge wp_1) \vee (x \le 0 \wedge wp_2). Then branch: x−1>0⇒x>1x - 1 > 0 \Rightarrow x > 1; together with x > 0, x > 1. Else branch: x+1>0⇒x>−1x + 1 > 0 \Rightarrow x > -1; together with x ≤ 0, only x = 0 for integers. Result: x > 1 or x = 0. (For real numbers the else part would be −1<x≤0-1 < x \le 0.)

Question 9medium

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
(b) and (c)

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.

Question 10medium

Using the denotational rules below, compute MbinM_{bin} 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>) + 1
Show answer
22

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).

Question 11easy

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
(a) operational (b) denotational (c) axiomatic

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.

Question 12medium

Compute the weakest precondition: b = a + 1; a = 3 * b; {a > 12}

Show answer
{a > 3}

Last assignment: 3b>12⇒b>43b > 12 \Rightarrow b > 4. First assignment: a+1>4⇒a>3a + 1 > 4 \Rightarrow a > 3. 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).

Review card

static semantics: compile-time rules a CFG cannot write (type compatibility, declare before use) attribute grammar = CFG + attributes + semantic functions + predicates synthesized: from below UP (actual_type) inherited: from above DOWN/sideways (expected_type) intrinsic: at a leaf, from the symbol table (look-up) order: look-up → expected down → actual up → predicate operational = state change on a virtual machine (manual) denotational = mathematical function (most rigorous) axiomatic = {P} S {Q}, verification wp(x = E, Q) = write E in place of x in Q, simplify sequence: FROM THE END TO THE START if: (B and wp(then)) or (not B and wp(else)) dividing by a negative flips the inequality a stronger precondition is also valid; weakest = least restrictive