ensuring correctness

78
λ Fernando Magno Quintão Pereira PROGRAMMING LANGUAGES LABORATORY Universidade Federal de Minas Gerais - Department of Computer Science PROGRAM ANALYSIS AND OPTIMIZATION – DCC888 ENSURING CORRECTNESS The material in these slides has been taken from the paper "A Framework for End-to-End Veri cation and Evaluation of Register Allocators".

Upload: terah

Post on 22-Feb-2016

71 views

Category:

Documents


1 download

DESCRIPTION

Ensuring Correctness. The material in these slides has been taken from the paper "A Framework for End-to-End Verification and Evaluation of Register Allocators". Implementing Correct Compilers. - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: Ensuring Correctness

λFernando Magno Quintão Pereira

PROGRAMMING LANGUAGES LABORATORYUniversidade Federal de Minas Gerais - Department of Computer Science

PROGRAM ANALYSIS AND OPTIMIZATION – DCC888

ENSURING CORRECTNESS

The material in these slides has been taken from the paper "A Framework for End-to-End Verification and Evaluation of Register Allocators".

Page 2: Ensuring Correctness

Implementing Correct Compilers

• A compiler translates a program PH, written in a high-level language, to a program PL, written in a low-level language.

• The compiler is correct if, given a certain PH, it produces a PL such that PH and PL have the same semantics.

• Notice that there may be some PH's that the compiler is not able to translate.– That is ok: the compiler is still correct even if it cannot

translate some programs PH's.

– A compiler that does not translate a single PH is still correct.

– But we want to maximize the amount of PH's that the compiler can translate.

How to ensure that a compiler is correct?

Page 3: Ensuring Correctness

Proving Correctness• We can test the compiler, giving him a number of PH's,

and checking if each PL that it produces is correct.– But to prove correctness, we would have to test every

single possible PH.– Any interesting compiler will handle an infinite number of

PH's; thus, this approach is not really feasible.• But testing is still very useful!– For instance, some folks, from the

University of Utah, have been generating random C programs to find bugs in compilers♤.• They found many bugs in gcc and in

LLVM.

1) Which features should be available to help us to find bugs in compilers?

2) In addition to testing, what are the other ways to ensure the correctness of a compiler?

♤: This tool is called CSmith, and it was described in the paper "Finding and understanding bugs in C compilers", published in PLDI (2011)

Page 4: Ensuring Correctness

Formal Proofs

• There has been a lot of work in proving that compilers are correct mechanically.

• A formal proof must be able to show that each translation that a compiler does preserves semantics.

• There exists some successful stories in this field.

CompCert♤ is a compiler that has been proved correct with the Coq proof assistant♧. CSmith was used on CompCert. It found only one bug in the compiler, whereas it found 400 different bugs in LLVM, for instance. The bug that CSmith found in CompCert was in the front end, which had not been proven to be correct formally.

♧: See "Formal certification of a compiler back-end or: programming a compiler with a proof assistant", POPL (2006)♤: Compcert is publically available at http://compcert.inria.fr/

1) And yet, formal proofs are not used that much in the industry. Why?

2) Are there any other way to gain confidence that a compiler is correct, besides test generators and proof assistants?

Page 5: Ensuring Correctness

Translation Validation

• A third way to ensure correctness of compilers is to do translation validation.

• Translation validation consists in certifying that the output of a compiler is a valid translation of its input.

int fact(int n) { int r = 1; int i = 2; while (i <= n) { r *= i; i++; } return r;}

CompilerInput Output

Translation Validator:(Input ≡ Output)?

Can we use a translation validator to certify that a compiler is correct?

Page 6: Ensuring Correctness

Translation Validation

• A translation validator (henceforth called just a validator) does not prove that a compiler is correct.– It can only certify that the outputs that it sees are correct

translations of the corresponding inputs.

♡: "Formal verification of translation validators: a case study on instruction scheduling optimizations", POPL (2008)

InputOutput

TranslatorCertifiedValidator ✔ Output

Compiler

(≡)?

• But, what if we certify – formally – that the validator is correct?

• We can, then, embed the validator in the compiler, to produce a provably correct translator♡.

Which one do you think is easier: to prove that the translator is correct, or to prove that the validator is correct?

Page 7: Ensuring Correctness

DCC 888

λ Universidade Federal de Minas Gerais – Department of Computer Science – Programming Languages Laboratory

CORRECTNESS OF REGISTER ALLOCATION

Page 8: Ensuring Correctness

Correctness of Register Allocation

• In order to illustrate how a correctness proof works, we will show how we can build a correct register allocator.

• We will proceed in several steps:1. We will define the syntax and operational semantics of a

toy assembly-like programming language.2. We will define register allocation on top of that language.3. We will show that register allocation preserves

semantics.• We will accomplish this last step via a type system.

How can we use types, plus those properties of progress and preservation, to ensure that a given register assignment is correct?

Let's start with our toy language. which features should it have, to allows us to prove properties related to register allocation?

Page 9: Ensuring Correctness

Infinite Register Machine (IRM)

(Programs) P ::= L1I1; … LkIk;

(Code labels) L ::= L1 | L2 | …

(Instr. Sequences) I ::=

- (Jump) | jump L

- (Sequence) | i: I

(Pseudo Regs) p ::= p1 | p2 | …

(Registers) r ::= r1 | … | rk

(Operands) o ::=

- (Constants) | •

- (Pseudos) | p

- (Pre-Colored Regs.) | (r, p)

(Instructions) i ::=

- (Assignment) | p = o

- (Assign Pre-Col.) | (r, p) = o

- (Cond. Jump) | if p jump L

- (Function Call) | (r0, p0) = call (r1, p1) … (rs, ps)

Programs in our infinite register machine can use an unbounded number of variables. We call these variables pseudo-registers, and denote them by the letter p. Some of these pseudo-registers must be assigned to specific registers. We say that they are pre-colored, and denote them by pairs, such as (p, r). We will use r to represent a physical register, such as AH in x86, or R0 in ARM.

Why do we have to care about pre-colored registers in this machine with a limitless surplus of registers?

Page 10: Ensuring Correctness

Abstractions

(Programs) P ::= L1I1; … LkIk;

(Code labels) L ::= L1 | L2 | …

(Instr. Sequences) I ::=

- (Jump) | jump L

- (Sequence) | i: I

(Pseudo Regs) p ::= p1 | p2 | …

(Registers) r ::= r1 | … | rk

(Operands) o ::=

- (Constants) | •

- (Pseudos) | p

- (Pre-Colored Regs.) | (r, p)

(Instructions) i ::=

- (Assignment) | p = o

- (Assign Pre-Col.) | (r, p) = o

- (Cond. Jump) | if p jump L

- (Function Call) | (r0, p0) = call (r1, p1) … (rs, ps)

If we want to prove properties about programs, we must try to abstract as much details away as possible. For instance, we are interested only in the relations between variables, i.e., which variables are used and defined by each instruction. Therefore, we can abstract away the semantics of some individual instructions. For instance, we can represent p1 = p2 + p3 as a sequence of three instructions:

What is this symbol (•) good for?

• = p2

• = p3

p1 = •

Page 11: Ensuring Correctness

Example of an IRM Program

1) What is the convention that this hypothetical architecture uses to represent function calls?

2) Can you figure out how many registers we would need to compile this program?

3) Do we have variables used without being defined?

4) And do we have variables defined, but not used in any instruction?

5) Why can't we just remove them?

Page 12: Ensuring Correctness

Defining Register Allocation

• A register allocation is a map (Pseudo × Point) → Register.• An IRM program, after register allocation, contains only

pre-colored pairs (p, r).

What would be a valid register assignment to our example program?

Page 13: Ensuring Correctness

Register Mapping

2) In order to represent register assignments, we need a bit more of syntax, than our current definition of IRM provides us. Which syntax am I talking about?

1) Why is the program larger, after allocation?

Page 14: Ensuring Correctness

Dealing with Memory

(Pseudo Registers) p ::= p1 | p2 | …

(Memory Locations) l ::= l1 | l2 | …

(Registers) r ::= r1 | … | rk

(Operands) o ::=

- (Register Bindings) | (r, p)

- (Memory Bindings) | (l, p)

(Instructions) i ::=

- (Load) (r, p) = (l, p)

- (Store) (l, p) = (r, p)

If the register pressure is too high, and variables must be spilled, we need a syntax to map them to memory. We describe memory locations with the new names li, and we now use them to describe loads to and stores from memory.

Page 15: Ensuring Correctness

• We have now a slightly different language, which has physical locations:– Finite number of registers– Infinite number of memory cells.

What is an invalid register mapping? In other words, what is an invalid FiRM program?

Finite Register Machine (FiRM)

• This language has new instructions:– Loads– Stores

• And now every pseudo is bound to a register or memory location.

(Pseudo Registers) p ::= p1 | p2 | …

(Memory Locations) l ::= l1 | l2 | …

(Registers) r ::= r1 | … | rk

(Operands) o ::=

- (Register Bindings) | (r, p)

- (Memory Bindings) | (l, p)

(Instructions) i ::=

- (Load) (r, p) = (l, p)

- (Store) (l, p) = (r, p)

Page 16: Ensuring Correctness

Errors in Register Allocation

(r0, p0) = •(r2, p2) = (r1, p0)

(r0, p0) = •(r0, p1) = •(r1, p2) = (r0, p0)

(l0, p0) = (r0, p0)(l0, p1) = (r1, p1)(r1, p2) = (l0, p0)

(r1, p1) = •(r0, p0) = call(r2, p2) = (r1, p1)

What is the problem of each one of these programs?

Page 17: Ensuring Correctness

Errors in Register Allocation

(r0, p0) = •(r2, p2) = (r1, p0)

(r0, p0) = •(r0, p1) = •(r1, p2) = (r0, p0)

(l0, p0) = (r0, p0)(l0, p1) = (r1, p1)(r1, p2) = (l0, p0)

(r1, p1) = •(r0, p0) = call(r2, p2) = (r1, p1)

Variable defined in a register, and expected in another.

Register is overwritten while its value is still alive.

Memory is overwritten while its value is still alive.

Caller save register r1 is overwritten by function call.

Page 18: Ensuring Correctness

Errors in Register Allocation

And what is the problem of this program?

Page 19: Ensuring Correctness

Errors in Register Allocation

In this program we have the possibility of p0 to reach a use point, in block L3, in a register different than the expected. This assignment should place p0 into r0, if we have hopes to read p0 in this register here.

If we want to be able to test if a given register assignment is correct, we must be prepared to take the program's control flow into consideration.

Page 20: Ensuring Correctness

The Operational Semantics of FiRM Programs

• In order to show that a register assignment is correct, we must show that it preserves semantics.

• But we still do not have any semantics to preserve.

• Let's define this semantics now.– For simplicity, we shall remove

function calls from the rest of our exposition.

P ::= L1I1; … LkIk;

L ::= L1 | L2 | …

I ::=

| jump L

| i: I

p ::= p1 | p2 | …

r ::= r1 | … | rk

l ::= l1 | l2 | …

o ::=

| •

| (r, p)

| (l, p)

i ::=

| (r, p) = o

| (l, p) = o

| if (r, p) jump L

What is the semantics of a FiRM program?

Page 21: Ensuring Correctness

Abstract Machine

• FiRM programs change the state of an abstract machine, which we describe as a tuple (C, D, R, I):– [C] is the code heap, a map of labels to sequences of

instructions, e.g., {L1 = I1, …, Lk = Ik}.– [D] is the data heap, a map of memory locations to pseudo

variables, e.g., {l1 = p1, …, lm = pm}.– [R] is the bank of registers, a map of registers to pseudo

variables, e.g., {r1 = p1, …, rn = pn}.– [I] is the sequence of instructions that we have to evaluate

to finish the program.• If M is a state, and there exists another state M', such

that M → M', then we say that M takes a step to M'.• A program state M is stuck if M cannot take a step.

Page 22: Ensuring Correctness

Heap and Registers• The data heap and the bank of registers are the locations

that we are allowed to use in our FiRM programs.• We have a different state at each program point, during the

execution of the program.

Page 23: Ensuring Correctness

Code Heap

• The code heap is a map of labels to basic blocks.• That is how we will model the semantics of jumps… wait

and see!

L1 → (r1, p0) = •; (l0, p0) = (r1, p0); if (r1, p0); if (r1, p0) jump L3; jump L2

L2 → (r1, p1) = •; (l1, p1) = (r1, p1) ; (r0, p0) = (l0, p0); if (r0, p0) jump L2; jump L3

L3 → (r1, p0) = (l0, p0); jump exit

Given that an abstract state is (C, D, R, I), what do you think will be the semantics of a jump?

Page 24: Ensuring Correctness

Bindings

• FiRM programs bind pseudos to registers, as the execution of instructions progresses.

• Thus, the semantics of each instruction is parameterized by an instance of D, and an instance of R:

D, R : •

D, R : (r, p), if R(r) = p p ≠ ∧ ⊥

D, R : (l, p), if D(l) = p p ≠ ∧ ⊥

If we write D, R : o, then we are saying that o is well-defined under the bindings D and R. In other words, the symbol • is always well-defined. On the other hand, a tuple like (r, p) is only well-defined if R(r) = p. Because we may have registers that are not bound to any pseudo, we use the symbol to denote their image. ⊥

Page 25: Ensuring Correctness

Simple Assignments

D, R : o(C, D, R, (r, p) = o; I) → (C, D, R[r p], I)➝

We define the semantics of simple assignments according to the inference rule below:

1) What is this body "(r, p) = o; I"?

2) What is the meaning of this syntax: R[r p]?➝

3) What would be the semantics of a load, e.g., (l, p) = o?

[ASSIGN]

Page 26: Ensuring Correctness

Assignments to Memory

D, R : o(C, D, R, (l, p) = o; I) → (C, D[l p], R, I)➝

We define the semantics of stores according to the inference rule below:

1) What is the semantics of jump instructions such as "jump L"?

2) Before you answer, think: do we care about the actual target of the jump in this little formalism of ours?

[STORE]

Page 27: Ensuring Correctness

Jumps

D, R : (r, p) L Dom(C) C(L) = I'∈(C, D, R, if (r, p) jump L; I) → (C, D, R, I')

D, R : (r, p) (C, D, R, if (r, p) jump L; I) → (C, D, R, I)

L Dom(C) C(L) = I∈(C, D, R, jump L) → (C, D, R, I)

[JUMPTOTARGET]

[FALLTHROUGH]

[UNCONDJUMP]

The Rules [JumpToTarget] and [FallThrough] give two different semantics to the same instruction. We can afford being non-deterministic, given that we are not really interested in the values that the program computes, but only in the mappings of pseudos to physical locations.

Page 28: Ensuring Correctness

Operational Semantics

D, R : o(C, D, R, (r, p) = o; I) → (C, D, R[r p], I)➝

D, R : o(C, D, R, (l, p) = o; I) → (C, D[l p], R, I)➝

D, R : (r, p) L Dom(C) C(L) = I'∈(C, D, R, if (r, p) jump L; I) → (C, D, R, I')

D, R : (r, p) (C, D, R, if (r, p) jump L; I) → (C, D, R, I)

L Dom(C) C(L) = I∈(C, D, R, jump L) → (C, D, R, I)

[ASSIGN]

[STORE]

[JUMPTOTARGET]

[FALLTHROUGH]

[UNCONDJUMP]

D, R : •D, R : (r, p), if R(r) = p p ≠ ∧ ⊥D, R : (l, p), if D(l) = p p ≠ ∧ ⊥

Remember:

1) When does a program become stuck?

2) When does a program terminate?

Page 29: Ensuring Correctness

Abstracting Useless Details Away

• We have not defined termination in our semantics. In other words, a program either runs forever, or jumps into a label that is not defined in the data heap. In the latter case, it is stuck.

• When we try to prove properties about programs, it is a good advice to try to remove as much details from the semantics of the underlying programming language as possible.– These details may not be important to the proof, and they

may complicate it considerably.

(r0, p0) = •(r0, p1) = •(r1, p2) = (r0, p0)

By the way, does the program on the left become stuck?

Page 30: Ensuring Correctness

Stuck Program ≡ Invalid Register Allocation

D, R : o(C, D, R, (r, p) = o; I) → (C, D, R[r p], I)➝

D, R : •

D, R : (r, p), if R(r) = p p ≠ ∧ ⊥

D, R : (l, p), if D(l) = p p ≠ ∧ ⊥

This program is stuck, because in the last instruction it is not the case that D, R : (r0, p0). At that program point we have that R(r0) = p1, which is not the expected value. Therefore, the premise of Rule [ASSIGN] is not true, and we are stuck.

Is it possible to determine if a program can be stuck before running it?

Page 31: Ensuring Correctness

DCC 888

λ Universidade Federal de Minas Gerais – Department of Computer Science – Programming Languages Laboratory

TYPES FOR REGISTER ALLOCATION

Page 32: Ensuring Correctness

Types to the Rescue

• Let's define the type of a register, or memory location, as the pseudo that is stored there.

• We will define a series of typing rules, in such a way that, if a program type-checks, then we know that it cannot be stuck during its execution.

Syntactically, the type of a value is either p or the special type Const.

The type of the register bank is a mapping Γ = {r1 : p1, …, rm : pm}, and the type of the data heap is a mapping Δ = {l1 : p1, …, ln : pn}. We have also the type of the code heap, which is another mapping Ψ = {L1 : (Γ1, Δ1), …, Lk : (Γk, Δk)}

Alert: the type of the code heap is usually when understanding drops to 0%. But be patient, and we will talk more about these weird types.

Page 33: Ensuring Correctness

Types of Operands

Γ(r) = p p ≠ ⊥Γ › (r, p) : p [TPREG]

[TPMEM]

› • : Const

Δ(l) = p p ≠ ⊥Δ › (l, p) : p

[TPCON]

1) How do I read the symbol "›"?

2) Can you infer the meaning of each of these three rules?

3) What is the type of each operand used in the program below?

Page 34: Ensuring Correctness

The Idea of Type Checking

• We are trying to "Type-Check" an assembly program, to ensure that it is correct.

• If we declare a variable as an integer, we expect that this variable will be used as an integer.

• If the compiler finds that it is used in some other way, an error is reported.

1) What do you think: does this program on the right compile☂?

2) Which kind of analysis does gcc use to type check this program?

int* foo() { int* r0 = NULL; if (r0) { return r0; } else { float r1 = r0; return r1; }}

☂: Well, given how lenient the C compilers are, we could expect anything…

Page 35: Ensuring Correctness

The Idea of Type Checking

int* foo() { int* r0 = NULL; if (r0) { return r0; } else { float r1 = r0; return r1; }}

1) Do you see any similarities between the C program and our FiRM example?

2) Do you have now an intuition on how we will use types to verify if a register allocation is correct?

t.c: In function ‘foo’:9: error: incompatible types in initialization10: error: incompatible types in return

Page 36: Ensuring Correctness

Types of Assignments

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝ [TPASG]

Δ, Γ › o : t p ≠ ⊥Ψ › (l, p) = o : (Γ × Δ) (Γ × Δ[l : p])➝ [TPSTR]

Assignments modify the typing environments of the register bank and the data heap. Neither simple assignments nor stores use the types of their operands to build up the type of the location that they define. Nevertheless, the

2) What is the meaning of the type of an instruction? This type looks like the type of a function, e.g., (Γ × Δ) (Γ' × Δ')➝

instruction type checks only if its operand does.

1) What is the Ψ on the right of the type sign › good for?

Page 37: Ensuring Correctness

Typing Environments

When we write Ψ › (r, p) = o : (Γ × Δ), or Δ, Γ › o : t, we are specifying typing environments. A sentence like T › e : t says that the expression e has type t on the environment T. A typing environment is like a table that associates typing information with the free variables in e, in such a way to allows us to reconstruct the type t.

For instance, we can only type check an expression like x + y + 1 in SML if we know that variables x and y have the int type. But, if we look at only this expression's syntax, we have no clues about the type of x and y. We need a typing environment to conclude the verification. In this case, the environment is a table that associates

let val x = 1; val y = 2in x + y + 1end

type information with names of free variables. In this example, we would have: {x : int, y : int} › x + y + 1 : int. The variables x and y are free in the expression x+y+1 because these variables have not being declared in that expression.

Page 38: Ensuring Correctness

The Type of Instructions

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝ [TPASG]

Δ, Γ › o : t p ≠ ⊥Ψ › (l, p) = o : (Γ × Δ) (Γ × Δ[l : p])➝ [TPSTR]

An instruction modifies the binding environments of the machine. We have two bindings, the environment that describes the register bank, Γ, and the environment that describes the memory, Δ. So, an instruction may modify any of these environments, and its type is, hence, (Γ × Δ) (Γ' × Δ')➝

In this program on the right, what is Γ after the last instruction?

Page 39: Ensuring Correctness

The Type of Instructions

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝ [TPASG]

Very tricky: how to type check jumps and sequences of instructions?

Page 40: Ensuring Correctness

Type-Checking Conditional Jumps

Γ › (r, p) : p Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › if (r, p) jump L : (Γ × Δ) (Γ × Δ)➝ [TPIFJ]

1) What is this first premise ensuring?

2) Do you remember what is the typing environment Ψ?

3) And what is the meaning of this inequality?

Remember: each instruction – but unconditional jumps – map a typing environment such as (Γ × Δ) into another typing environment such as (Γ' × Δ'). The typing rule for a jump does not create new bindings in the typing environment. But we must still ensure that we are jumping to a program point whose typing environment is valid to us.

Page 41: Ensuring Correctness

(Γ × Δ) ≤ (Γ' × Δ')

• We are talking about a polymorphic type system.• An entity can have multiple types, and for each one of

them, the program still type checks.

Γ ≤ Γ' r, if r : p Γ' then r : p Γ∀ ∈ ∈

Δ ≤ Δ' l, if l : p Δ' then l : p Δ∀ ∈ ∈

(Γ × Δ) ≤ (Γ' × Δ') if Γ ≤ Γ' and Δ ≤ Δ'

For instance: {r0: p0, r1: p1} ≤ {r0: p0} ≤ {}

This is subtyping polymorphism. If Γ ≤ Γ', then we say that Γ is a subtype of Γ'

Page 42: Ensuring Correctness

Subtyping Polymorphism

Γ › (r, p) : p Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › if (r, p) jump L : (Γ × Δ) (Γ × Δ)➝ [TPIFJ]

Page 43: Ensuring Correctness

Subtype to ForgetSubtyping polymorphism is a way to "forget" information. This idea was introduced in a famous paper about typed assembly languages♡.

At this point we have a register environment with many registers defined: Γ1 = {r1 : p0, r2 : p1}. However, at this point here we only need {r1 : p0}. Without subtyping, we would not be able to type check this program, even though it works fine. So, subtyping polymorphism let's us "forget" some information.

♡: From System F to typed assembly language, POPL (1998)

Page 44: Ensuring Correctness

Jumps and Sequences

Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › jump L : (Γ × Δ) [TPJMP]

Ψ › i : (Γ × Δ) → (Γ' × Δ') Ψ › I : (Γ' × Δ')Ψ › i; I : (Γ × Δ) [TPSEQ]

1) Can you explain each of these two rules?

2) Do you understand the difference between type checking and type inference?

3) What are we doing: type checking or inference?

The type of a sequence of instructions is the typing relations that must be obeyed so that the sequence can execute. In other words, we are telling the typing system what is the minimum type table that must be true so that the sequence executes correctly.

Page 45: Ensuring Correctness

Typing Sequences of Instructions

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝

Page 46: Ensuring Correctness

Typing Sequences of Instructions

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝

Page 47: Ensuring Correctness

Typing Sequences of Instructions

Γ › (r, p) : p Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › if (r, p) jump L : (Γ × Δ) (Γ × Δ)➝

Of course: for this rule to work here, what we must know about L3?

Page 48: Ensuring Correctness

Typing Sequences of Instructions

Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › jump L : (Γ × Δ)

Let's assume that Ψ › L2 : ({} × {})

Why do we need some assumption like this one?

Page 49: Ensuring Correctness

Typing Sequences of Instructions

Ψ › i : (Γ × Δ) → (Γ' × Δ') Ψ › I : (Γ' × Δ')Ψ › i; I : (Γ × Δ) [TPSEQ]

How can we apply the Rule [TPSEQ] to find a type for this entire code sequence? Let's assume that Ψ › L2 : ({} × {}), and that Ψ › L3 : ({} × {})

Page 50: Ensuring Correctness

Typing Sequences of Instructions

Ψ › i : (Γ × Δ) → (Γ' × Δ') Ψ › I : (Γ' × Δ')Ψ › i; I : (Γ × Δ) [TPSEQ]

The sequence formed by only "jump L2", by Rule [TPSEQ], has type ((Γ[r1: p0])[r2: p1] × Δ[]). Thus, by rule [TPSEQ], the sequence "if (r1, p0) jump L3; jump L2" has type ((Γ[r1: p0])[r2: p1] × Δ[])

Page 51: Ensuring Correctness

Typing Sequences of Instructions

Ψ › i : (Γ × Δ) → (Γ' × Δ') Ψ › I : (Γ' × Δ')Ψ › i; I : (Γ × Δ) [TPSEQ]

By Rule [TPSEQ], the sequence"(r1, p0) = •; if (r1, p0) jump L3; jump L2" has type (Γ[r1: p0] × Δ[])

So, what is the type of the entire sequence?

Page 52: Ensuring Correctness

Typing Sequences of Instructions

Ψ › i : (Γ × Δ) → (Γ' × Δ') Ψ › I : (Γ' × Δ')Ψ › i; I : (Γ × Δ) [TPSEQ]

The entire sequence, by Rule [TPSEQ], has type (Γ×Δ), where Γ and Δ are the environments that existed before the sequence. This type indicates that the running program does not require any assignment of registers to types to work correctly, i.e., it does not use pre-defined variables.

Page 53: Ensuring Correctness

Type Checking a Jump

1) What must be the type Γ after the second instruction?

2) What is the type expected at L2?

3) What are the possible types that we can infer for "jump L2"?

4) Does the sequence L1; L2 type check?

Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › jump L : (Γ × Δ)

Page 54: Ensuring Correctness

Type Checking a Jump

The sequence L1;L2 is fine, because by Rule [TPJMP], we expect {r1: p0} at L2, and by two applications of Rule [TPSEQ], we know that we have {r1:p0, r2:p1} at the end of the second assignment. And from our definition of polymorphism, we have that {r1:p0, r2:p1} < {r1:p0}

Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › jump L : (Γ × Δ) [TPJMP]

Ψ › i : (Γ × Δ) → (Γ' × Δ') Ψ › I : (Γ' × Δ')Ψ › i; I : (Γ × Δ) [TPSEQ]

Does the sequence L1;L3 type check?

Page 55: Ensuring Correctness

Typing the Entire Program

∀r domain(Γ), R(r) : Γ(r)∈› R : Γ [TPBNK]

∀l domain(Δ), D(l) : Δ(l)∈› D : Δ [TPMEM]

∀L domain(Ψ), Ψ › C(L) : Ψ(L)∈› C : Ψ [TPMEM]

› C : Ψ › D : Δ › R : Γ Ψ › I : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')› (C, D, R, I)

1) Can you explain each one of these rules?

2) How many rules, in total, have we defined till this point?

[TPPRG]

Page 56: Ensuring Correctness

The Entire Type System

∀r domain(Γ), R(r) : Γ(r)∈Ψ › R : Γ

∀l domain(Δ), D(l) : Δ(l)∈Ψ › D : Δ

∀L domain(Ψ), Ψ › C(L) : Ψ(L)∈› C : Ψ

› C : Ψ › D : Δ › R : Γ Ψ › I : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')› (C, D, R, I)

Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › jump L : (Γ × Δ)

Ψ › i : (Γ × Δ) → (Γ' × Δ') Ψ › I : (Γ' × Δ')Ψ › i; I : (Γ × Δ)

Γ › (r, p) : p Ψ › L : (Γ' × Δ') (Γ × Δ) ≤ (Γ' × Δ')Ψ › if (r, p) jump L : (Γ × Δ) (Γ × Δ)➝

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝

Δ, Γ › o : t p ≠ ⊥Ψ › (l, p) = o : (Γ × Δ) (Γ × Δ[l : p])➝

Γ(r) = p p ≠ ⊥Γ › (r, p) : p

› • : Const

Δ(l) = p p ≠ ⊥Δ › (l, p) : p

This looksscary!!!

Page 57: Ensuring Correctness

Soundness

• Preservation: if › M, and M → M', then › M'

• Progress: if › M, then there exists M' such that M → M'

• Soundness: if › M, then M cannot go wrong. In other words, there is no execution of M such that M is stuck.

Soundness gives us a way to define, formally, the meaning of a correct register assignment. Any valid register allocation preserves semantics. In other words, the program, after register allocation, has the semantics that we would expect it to have.

Page 58: Ensuring Correctness

Preservation: if › M, and M → M', then › M'

• For each rule used to show › M, we must see how M → M', and for each possible way to step, we must show › M'

Example: let's assume that we took a step by Rule [ASSIGN]. Thus, we know that the instruction is a copy, e.g., (r, p) = o. We also know that we have used [TPASG] to type check this rule. Below we have an enumeration of the facts that we know:

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝

[TPASG]

› C : Ψ › D : Δ' › R : Γ' (Γ' × Δ') ≤ (Γ × Δ)› (C, D, R, (r, p) = o; I)

Ψ › I : (Γ[r : p] × Δ)Ψ › (r, p) = o; I : (Γ × Δ)

[TPSEQ]

D, R : o(C, D, R, (r, p) = o; I) → (C, D, R[r p], I)➝ [ASSIGN]

[TPPRG]We must show that (C, D, R[r p], I) type checks.➝

Page 59: Ensuring Correctness

› C : Ψ › D : Δ' › R[r p] : Γ'[r : p] Ψ › I : (Γ[r : p] × Δ) (Γ'[r : p] × Δ') ≤ (Γ[r : p] × Δ)➝› (C, D, R[r p], I)➝

Preservation: if › M, and M → M', then › M'

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝

› C : Ψ › D : Δ' › R : Γ' (Γ' × Δ') ≤ (Γ × Δ)› (C, D, R, (r, p) = o; I)

Ψ › I : (Γ[r : p] × Δ)Ψ › (r, p) = o; I : (Γ × Δ)

Proof:1. We know that R : Γ'

1. If R : Γ', then we know that R[r p] : Γ'[r : p].➝2. We know that (Γ' × Δ') ≤ (Γ × Δ)

1. If (Γ' × Δ') ≤ (Γ × Δ), then we know that (Γ'[r : p] × Δ') ≤ (Γ[r : p] × Δ)3. We know that › C : Ψ4. We know that › D : Δ'5. We know that Ψ › I : (Γ[r : p] × Δ)By combining (1), (2), (3), (4) and (5), we have that:

Why do we know (1-5)? Where these facts come from?

Page 60: Ensuring Correctness

Progress: if › M, then M' such that M → M'∃• For each rule used to show › M, we must determine a M', and find a

rule that allows M to evolve into M'

Example: let's assume that we type check M by Rule [TPASG]. Like in the case for preservation, we know that the instruction is a copy, e.g., (r, p) = o. Below we have an enumeration of the facts that we know:

Δ, Γ › o : t p ≠ ⊥Ψ › (r, p) = o : (Γ × Δ) (Γ[r : p] × Δ)➝

[TPASG]

› C : Ψ › D : Δ' › R : Γ' (Γ' × Δ') ≤ (Γ × Δ)› (C, D, R, (r, p) = o; I)

Ψ › I : (Γ[r : p] × Δ)Ψ › (r, p) = o; I : (Γ × Δ)

[TPSEQ]

[TPPRG]

D, R : o(C, D, R, (r, p) = o; I) → (C, D, R[r p], I)➝ [ASSIGN]

What we want to show: by a quick inspection, the only rule that evaluates (r, p) = o is [Assign]. We need to show that the premise of this rule, e.g., D, R : o is valid. Hence, for eachpossible o, we need to show that D, R : o

Page 61: Ensuring Correctness

Progress: if › M, then M' such that M → M'∃

D, R : •

D, R : (r, p), if R(r) = p p ≠ ∧ ⊥D, R : (l, p), if D(l) = p p ≠ ∧ ⊥

There exist 3 different patterns of operands that match "o" in D, R : o. These patterns, plus the conditions that are expected upon them, are shown below:

If we assume that o = •, then we are done, because there is no precondition on D, R: •, and (r, p) = • can always take a step.

If we assume that o is (r0, p0), then we must ensure that R(r0) = p0, before we execute the assignment, and that p0 ≠ . This is the only precondition that ⊥the assignment Rule [ASSIGN] enforces. From the hypothesis of the theorem, the following facts are true:

Δ, Γ › (r0, p0) : p0 p0 ≠ ⊥Ψ › (r, p) = (r0, p0) : (Γ × Δ) (Γ[r : p] × Δ)➝

› C : Ψ › D : Δ' › R : Γ' (Γ' × Δ') ≤ (Γ × Δ)› (C, D, R, (r, p) = (r0, p0); I)

Ψ › I : (Γ[r : p] × Δ)Ψ › (r, p) = (r0, p0); I : (Γ × Δ)

Can you help me finishing the proof when o is (r0, p0)?

Γ(r0) = p0 p0 ≠ ⊥

Page 62: Ensuring Correctness

Progress: if › M, then M' such that M → M'∃

Concluding the proof: we obtain p0 ≠ for free as one of the premises of ⊥Rule [TPASG]. To obtain R(r0) = p0, we recur to a Lemma that we have not shown here: "the inversion of the typing relation", applied on Rule [TPREG]. If Δ, Γ › (r0, p0) : p0, then we have that Γ(r0) = p0. Inverting the Rule [TPBNK], we know that if Γ(r0) = p0, then R(r0) = p0.

p0 ≠ ⊥Ψ › (r, p) = (r0, p0) : (Γ × Δ) (Γ[r : p] × Δ)➝

› C : Ψ › D : Δ' › R : Γ' (Γ' × Δ') ≤ (Γ × Δ)› (C, D, R, (r, p) = (r0, p0); I)

Ψ › I : (Γ[r : p] × Δ)Ψ › (r, p) = (r0, p0); I : (Γ × Δ)

Γ(r0) = p0 p0 ≠ ⊥Δ, Γ › (r0, p0) : p0

∀r domain(Γ), R(r) : Γ(r)∈› R : Γ [TPBNK]

[TPREG]

[TPASG]

[TPSEQ]

D, R : •

D, R : (r, p), if R(r) = p p ≠ ∧ ⊥D, R : (l, p), if D(l) = p p ≠ ∧ ⊥

Attention: we must also prove progress when o is (l, p), but this case is similar to the case when o is (r, p). Hence, we will – diabolically – leave it to the interested reader

Page 63: Ensuring Correctness

DCC 888

λ Universidade Federal de Minas Gerais – Department of Computer Science – Programming Languages Laboratory

WRITING A BIT OF THIS IN TWELF

Page 64: Ensuring Correctness

Formalizing Correctness in Twelf

• We can formalize everything that we have discussed in Twelf.– This is a bit more complicated than what we have been doing so

far with Twelf, because now we must deal with associations between variables and values. In this case, we have to deal with bindings between registers and pseudos.

• There exists a complete formalization, available at http://compilers.cs.ucla.edu/ralf/twelf/

• In the rest of this class we will focus only on the operational semantics of FiRM.

Page 65: Ensuring Correctness

Writing the Operational Semantics in Twelf

• To make things easier, let's just forget the memory, i.e., the data heap. Thus, instead of representing a machine as (C, D, R, I), we will have that a machine is just (C, R, I)

• Our first challenge is how to implement data structures in Twelf.

2) A program, i.e., the code heap, is a map between labels and sequences of instructions. How do we represent this in Twelf?

3) The register bank is a map between registers and pseudos. How to represent this in Twelf?

1) We must interpret sequences of instructions. How can we represent these sequences in Twelf?

• We will write the operational semantics of FiRM in Twelf, and will show how to evaluate a few terms.

Page 66: Ensuring Correctness

Lists to Represent Everything

• We can represent all these mappings as lists:

exp : type.exps : type.

nil_exp : exps.cons_exp : exp -> exps -> exps.

proj_exp : exps -> nat -> exp -> type.%mode proj_exp +EL +N -E.proj_exp_z : proj_exp (cons_exp E EL) z E.proj_exp_s : proj_exp (cons_exp E EL) (s N) E' <- proj_exp EL N E'.

update_exp : exps -> exp -> nat -> exps -> exp -> type.%mode update_exp +EL +E +N -EL' -E.update_exp_z : update_exp (cons_exp E' EL) E z (cons_exp E EL) E'.update_exp_s : update_exp (cons_exp E' EL) E (s N) (cons_exp E' EL') E'' <- update_exp EL E N EL' E''.

nat : type.z : nat.s : nat -> nat.

nt : nat -> type.nt_z : nt z.nt_s : nt (s X) <- nt X.

1) Which basic operations do we have in this data type?

2) What is an empty list?

3) And what is a list that has at least one element?

4) What is the role of the mode declarations in this type description?

5) What does proj_exp do?

6) What does update_exp do?

Page 67: Ensuring Correctness

Using the Lists

exp : type.exps : type.

nil_exp : exps.cons_exp : exp -> exps -> exps.

proj_exp : exps -> nat -> exp -> type.%mode proj_exp +EL +N -E.proj_exp_z : proj_exp (cons_exp E EL) z E.proj_exp_s : proj_exp (cons_exp E EL) (s N) E' <- proj_exp EL N E'.

update_exp : exps -> exp -> nat -> exps -> exp -> type.%mode update_exp +EL +E +N -EL' -E.update_exp_z : update_exp (cons_exp E' EL) E z (cons_exp E EL) E'.update_exp_s : update_exp (cons_exp E' EL) E (s N) (cons_exp E' EL') E'' <- update_exp EL E N EL' E''.

psd : nat -> exp.top?- proj_exp (cons_exp (psd (s z)) (cons_exp (psd (s (s z))) nil_exp)) (s z) E.Solving...E = psd (s (s z)).More? yNo more solutions

We can only insert instances of the exp type in our lists. In this example we have defined a new type psd, that converts a natural into an element of type "exp". We can insert these elements in our list.

We need to represent the syntax of FiRM programs. Do you remember this syntax?

Page 68: Ensuring Correctness

Basic Values

psd : nat -> exp.

blt : exp.

reg : nat -> nat -> exp.

lbl : nat -> exp.

The term psd describes the pseudo variables. We represent each pseudo as a natural number. Because we will have to store them in lists, psd converts naturals to exps.

The term blt represents the symbol • . This symbol is a surrogate for everything that is not a register in our programs, like constants, for instance.

The term reg represents a physical register. A register is always a pair, where the first element denotes a position in the register bank, and the second denotes the pseudo that is stored in that position. For instance, in our convention, reg 4 2 represents the physical register R4 holding the value of pseudo P2.

Finally, the term lbl denotes the labels of basic blocks. A program is a list of basic blocks, each of them addressed by a single label.

Page 69: Ensuring Correctness

Evaluating Operands

D, R : •

D, R : (r, p), if R(r) = p p ≠ ∧ ⊥

D, R : (l, p), if D(l) = p p ≠ ∧ ⊥

eval: exps -> exp -> exp -> type.%mode eval +R +Reg -V.

eval_blt : eval R blt blt.

eval_reg: eval R (reg Nr Np) (psd Np) <- proj_exp R Nr (psd Np) <- not_zero Np.

Remember: we will pretend that the memory is not necessary. This is just to keep our presentation within acceptable time constraints. Thus, we will not talk about memory addresses l, and we will forget all about the data heap D.

not_zero : nat -> type.%mode not_zero +N.

not_zero_ : not_zero (s N).

We will let zero denote a register that has not been assigned any value. Every register is empty at the beginning of the execution of the program. Thus, we assume that these registers all hold zero.

Page 70: Ensuring Correctness

Evaluating Operands - Examplestop?- eval _ blt V.Solving...V = blt;More? yNo more solutions

?- eval R (reg (s (s (s z))) (s z)) V.Solving...V = psd (s z);R = cons_exp X1 (cons_exp X2 (cons_exp X3 (cons_exp (psd (s z)) X4))).More? yNo more solutions

?- eval (cons_exp (psd (s z)) (cons_exp (psd (s z)) (cons_exp (psd (s z)) (cons_exp (psd (s z)) nil_exp)))) (reg (s (s (s z))) (s z)) V.Solving...V = psd (s z).More? yNo more solutions

not_zero : nat -> type.%mode not_zero +N.

not_zero_ : not_zero (s N).

eval: exps -> exp -> exp -> type.%mode eval +R +Reg -V.

eval_blt : eval R blt blt.

eval_reg: eval R (reg Nr Np) (psd Np) <- proj_exp R Nr (psd Np) <- not_zero Np.

What are these Xi's that we see in the definition of R?

Page 71: Ensuring Correctness

The Syntax of Instructions

i_seq : exps -> exp.

i_mov : exp -> exp -> exp.

i_cnd : exp -> exp -> exp.

i_jmp : exp -> exp.

I ::= | jump L | i: Ii ::= | (r, p) = o | (l, p) = o | if (r, p) jump L

The Original Syntax of Instructions in FiRM

The Twelf Syntax of Instructions in FiRM

We have a Twelf term to describe each instruction in our toy language. We are skipping the stores, because, again, we are not dealing with memory.

1) How do we evaluate these instructions?

2) What would be the syntax of a term to evaluate instructions?

3) Do you remember how we described the evaluation of instructions in FiRM?

Page 72: Ensuring Correctness

The Evaluation of Instructions

step: exps -> exps -> exps -> exps -> exps -> exps -> type.%mode step +C +R +I -C' -R' -I'.

D, R : o(C, D, R, (r, p) = o; I) → (C, D, R[r p], I)➝ [ASSIGN]✗ ✗✗

step_mov : step C R (cons_exp (i_mov (reg Nr Np) O) I) C R' I <- eval R O V <- update_exp R (psd Np) Nr R' P.

1) What does the update_exp relation do?

2) Can you remember which other instructions we have in FiRM? Stores are out.

A FiRM abstract machine is a tuple (C, D, R, I). We are not considering D's in our Twelf exposition, so let's assume the machine is (C, R, I). An instruction takes an abstract machine in, and produces another abstract machine, which results from updating R, and consuming the first instruction of I.

Page 73: Ensuring Correctness

Evaluating The Copy Instruction?- step C (cons_exp (psd (s z)) (cons_exp (psd (s z)) (cons_exp (psd (s z)) nil_exp))) (cons_exp (i_mov R (reg (s z) (s z))) nil_exp) C (cons_exp (psd (s z)) (cons_exp (psd (s (s (s z)))) (cons_exp (psd (s z)) nil_exp))) nil_exp.Solving...R = ???C = C.More? yNo more solutions

1) Easy: what is the value of the code heap that allows Twelf to reconstruct this term?

2) And what is the value of R that we must have?

Page 74: Ensuring Correctness

Evaluating Copiestop?- step _ (cons_exp (psd (s z)) (cons_exp (psd (s z)) (cons_exp (psd (s z)) nil_exp))) (cons_exp (i_mov R (reg (s z) (s z))) nil_exp) _ NewRegBank nil_exp.Solving...NewRegBank = cons_exp (psd X1) (cons_exp (psd (s z)) (cons_exp (psd (s z)) nil_exp));R = reg z X1.

More? yNewRegBank = cons_exp (psd (s z)) (cons_exp (psd X1) (cons_exp (psd (s z)) nil_exp));R = reg (s z) X1.

More? yNewRegBank = cons_exp (psd (s z)) (cons_exp (psd (s z)) (cons_exp (psd X1) nil_exp));R = reg (s (s z)) X1.

More? yNo more solutions

Quick question: what is this X1 in the reconstruction of reg?

Page 75: Ensuring Correctness

Evaluating Instructions

step_jmp : step C R (cons_exp (i_jmp (lbl N)) nil_exp) C R I' <- proj_exp C N (i_seq I').

D, R : (r, p) (C, D, R, if (r, p) jump L; I) → (C, D, R, I)

L Dom(C) C(L) = I∈(C, D, R, jump L) → (C, D, R, I)

[FALLTHROUGH]

[UNCONDJUMP]

step_jeq : step C R (cons_exp (i_cnd (reg Nr Np) (lbl N)) I) C R I <- eval R (reg Nr Np) V.

step_jne : step C R (cons_exp (i_cnd (reg Nr Np) (lbl N)) I) C R I' <- eval R (reg Nr Np) V <- proj_exp C N (i_seq I').

D, R : (r, p) L Dom(C) C(L) = I'∈(C, D, R, if (r, p) jump L; I) → (C, D, R, I') [JUMPTOTARGET]

From the rule step_jne, can you see how we are encoding the heap?

Why do we have two premises in rule [UNCONDJUMP], and just one in step_jmp?

Page 76: Ensuring Correctness

Understanding Jumps

step_jne : step C R (cons_exp (i_cnd (reg Nr Np) (lbl N)) I) C R I' <- eval R (reg Nr Np) V <- proj_exp C N (i_seq I').

top?- step (cons_exp (i_seq (cons_exp (i_mov (reg z (s z)) blt) nil_exp)) nil_exp) (cons_exp (psd (s z)) nil_exp) (cons_exp (i_cnd (reg z (s z)) (lbl z)) nil_exp) (cons_exp (i_seq (cons_exp (i_mov (reg z (s z)) blt) nil_exp)) nil_exp) (cons_exp (psd (s z)) nil_exp) (cons_exp (i_mov (reg z (s z)) blt) nil_exp).

D, R : (r, p) L Dom(C) C(L) = I'∈(C, D, R, if (r, p) jump L; I) → (C, D, R, I') [JUMPTOTARGET]

1) What is the original code heap? What about the final code heap?

2) What is the original register bank? What about the final bank?

3) What is I'? In other words, what will be the next instruction to run?

Page 77: Ensuring Correctness

What about the Rest of it?

• We can formalize the entire type system, and prove its soundness in Twelf.

• That, of course, would take quite a lot of time.• But if you are interested, you can fish up the

formalization in the course webpage.

You will find proofs of preservation and progress. All these proofs use many different lemmas. Combining them is, already, a good exercise in Twelf. There are a few other interesting features in these proofs, like how to define the polymorphism that our type system requires, for instance.

Page 78: Ensuring Correctness

A Bit of History

• The type system described in this presentation was introduced by Nandivada et al. in 2007.

• This type system is strongly based on the Typed Assembly Language (TAL) proposed by Morrisett et al.

• One of the first formal proofs that a compiler is correct is due to Necula and Lee.

• Xavier Leroy's group has a number of papers about proving mechanically the correctness of compilers.

• Nandivada, V., Pereira, F and Palsberg, J. "A Framework for End-to-End Verification and Evaluation of Register Allocators", SAS, pp 153-169 (2007)

• Morrisett, G., Walker, D., Crary, K., and Glew, N. "From System F to Typed Assembly Language", POPL, pp 85-97 (1998)

• Necula, G. and Lee, P. "The Design and Implementation of a Certifying Compiler", PLDI, pp 333-444 (1998)

• Rideau, S. and Leroy, X. "Validating Register Allocation and Spilling", Compiler Construction, pp 224-243 (2010)