Skip to content

Commit

Permalink
Tweak writing a little & add headings for fabs post
Browse files Browse the repository at this point in the history
  • Loading branch information
dbp committed Apr 18, 2018
1 parent 65b871d commit 3abd9e7
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 2 deletions.
8 changes: 7 additions & 1 deletion _site/drafts/how-to-prove-a-compiler-fully-abstract.html
Expand Up @@ -35,6 +35,7 @@ <h2>How to prove a compiler fully abstract</h2>
<p>Both <strong>equivalence preservation</strong> and <strong>equivalence reflection</strong> (what make a compiler fully abstract) relate to how the compiler treats program equivalences, which in this case I’m considering observational equivalence. Two programs <code>p1</code> and <code>p2</code> are <strong>observationally equivalent</strong> if you cannot make any external observations that can distinguish them.</p>
<p>For example, if the only externally observable behavior about programs in your language that you can make is see what output they print, this means that the two programs print the same output, even if they are implemented in completely different ways. Observational equivalence is extremely useful, especially for compilers, which when optimizing may change how a particular program is implemented but should not change the observable behavior. But it is also useful for programmers, who commonly refactor code, which means they change how the code is implemented (to make it easier to maintain, or extend, or better support some future addition), without changing any functionality. <em>Refactoring is an equivalence-preserving transformation.</em> We write observational equivalence on programs formally as:</p>
<pre><code>p1 ≈ p1</code></pre>
<h3 id="contextual-equivalence">Contextual equivalence</h3>
<p>But we often also want to compile not just whole programs, but particular modules, or expressions, or in the general sense, <strong>components</strong>, and in that case, we want an analogous notion of equivalence. Two components are <strong>contextually equivalent</strong> if in all program contexts they produce the same observable behavior. In other words, if you have two modules, but any way you combine those modules with the rest of a program (so the rest is the same, but the modules differ), the result is the same, then those two modules are contextually equivalent. We will write this, overloading the <code></code> for both observational and contextual equivalence, as:</p>
<pre><code>e1 ≈ e1</code></pre>
<p>As an example, if we consider a simple functional language and consider our components to be individual expressions, it should be clear that these two expressions are contextually equivalent:</p>
Expand All @@ -44,18 +45,21 @@ <h2>How to prove a compiler fully abstract</h2>
<p>Would not be contextually equivalent, because there exists a program context that can distinguish them. What is that context? Well, if we imagine plugging in the functions above into the “hole” written as <code>[·]</code> below, the result will be different for the two functions! This is because the <code>toString()</code> method on functions in Javascript returns the source code of the function.</p>
<div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript">([·]).<span class="at">toString</span>()</code></pre></div>
<p>From the perspective of optimizations, this is troublesome, as you can’t be sure that a transformation between the above programs was safe (assuming one was much faster than the other), as there could be code that relied upon the particular way that the source code had been written. There are more complicated things you can do (like optimizing speculatively and falling back to unoptimized versions when reflection was needed). In general though, languages with that kind of reflection are both harder to write fast compilers for and harder to write secure compilers for, and while it’s not the topic of this post, it’s always important to know what you mean by contextual equivalence, which usually means: <em>what can program contexts determine about components</em>.</p>
<h3 id="part-1.-equivalence-reflection">Part 1. Equivalence reflection</h3>
<p>With that in mind, what does <strong>equivalence reflection</strong> and <strong>equivalence preservation</strong> for a compiler mean? Let’s start with <strong>equivalence reflection</strong>, as that’s the property that all your correct compilers already have. Equivalence reflection means that if two components, when compiled, are equivalent, then the source components must have been equivalent. We can write this more formally as (where we write <code>s ↠ t</code> to mean a component <code>s</code> is compiled to <code>t</code>):</p>
<pre><code>∀ s1 s2 t1 t2. s1 ↠ t1 ∧ t2 ↠ s2 ∧ t1 ≈ t2 ⇒ s1 ≈ s2</code></pre>
<p>What are the consequences of this definition? And why do correct compilers have this property? Well, the contrapositive is actually easier to understand: it says that if the source components weren’t equivalent then the target components would have to be different, or more formally:</p>
<pre><code>∀ s1 s2 t1 t2. s1 ↠ t1 ∧ t2 ↠ s2 ∧ s1 ≉ s2 ⇒ t1 ≉ t2</code></pre>
<p>If this didn’t hold, then the compiler could take different source components and compile them to the same target component! Which means you could have different source programs you wrote, which have observationally different behavior, and your compiler would produce the same target program! Any correct compiler has to preserve observational behavior, and it couldn’t do that in this case, as the target program only has one behavior, so it can’t have both the behavior of <code>s1</code> and <code>s2</code> (for pedants, not considering non-deterministic targets).</p>
<p>So equivalence reflection should be thought of as related to compiler correctness. Note, however, that equivalence reflection is <em>not</em> the same as compiler correctness: as long as your compiler produced different target programs for different source programs, all would be fine – your compiler could hash the source program and produce target programs that just printed the hash to the screen, and it would be equivalence reflecting, since it would produce different target programs not only for source programs that were observationally different, but even syntactically different! That would be a pretty bad compiler, and certainly not correct, but it would be equivalence reflecting.</p>
<h3 id="part-2.-equivalence-preservation">Part 2. Equivalence preservation</h3>
<p>Equivalence preservation, on the other hand, is the hallmark of fully abstract compilers, and it is a property that even most correct compilers do not have, though it would certainly be great if they did. It says that if two source components are equivalent, then the compiled versions must still be equivalent. Or, more formally:</p>
<pre><code>∀ s1 s2 t1 t2. s1 ↠ t1 ∧ t2 ↠ s2 ∧ s1 ≈ s2 ⇒ t1 ≈ t2</code></pre>
<p>(See, I just reversed the implication. Need trick! But now it means something totally different). One place where this has been studied extensively is by security researchers, because what it tells you is that observers in the target can’t make observations that aren’t possible to distinguish in the source. Let’s make that a lot more concrete, where we will also see why it’s not frequently true, even of proven correct compilers.</p>
<p>Say your language has some information hiding feature, like a private field, and you have two source components that are identical except they have different values stored in the private field. If the compiler does not preserve the fact that it is private (because, for example, it translates the higher level object structure into a C struct or just a pile of memory accessed by assembly), then other target code could read the private values, and these two components will no longer be equivalent.</p>
<p>This also has implications for programmer refactoring and compiler optimizations: I (or my compiler) might think that it is safe to replace one version of the program with another, because I know that in my language these are equivalent, but what I don’t know is that the compiler reveals distinguishing characteristics, and perhaps some target-level library that I’m linking with relies upon how the old code worked. If that’s the case, I can have a working program, and make a change that does not change the meaning of the component <em>in my language</em>, but the whole program can no longer work.</p>
<p>Proving a compiler fully abstract, therefore, is all about proving equivalence preservation. So how do we do it?</p>
<h3 id="how-to-prove-equivalence-preservation">How to prove equivalence preservation</h3>
<p>Looking at what we have to prove, we see that given contextually equivalent source components <code>s1</code> and <code>s2</code>, we need to show that <code>t1</code> and <code>t2</code> are contextually equivalent. We can expand this to explicitly quantify over the contexts that combine with the components to make whole programs:</p>
<pre><code>∀ s1 s2 t1 t2. s1 ↠ t1 ∧ t2 ↠ s2 ∧ (∀Cs. Cs[s1] ≈ Cs[s2]) ⇒ (∀Ct. Ct[t1] ≈ Ct[t2])</code></pre>
<p>Noting that as mentioned above, I am overloading <code></code> to now mean whole-program observational equivalence (so, running the program produces the same observations).</p>
Expand All @@ -65,6 +69,7 @@ <h2>How to prove a compiler fully abstract</h2>
<pre><code>Cs'[s1] ≈ Cs'[s2]
≈ ≈
Ct[t1] ? Ct[t2]</code></pre>
<h3 id="concrete-example-of-languages-compiler-proof-of-full-abstraction">Concrete example of languages, compiler, &amp; proof of full abstraction</h3>
<p>Let’s make this concrete with an example. This will be presented somewhat in english and some in Coq.</p>
<p>Our source language is arithmetic expressions over integers with addition and subtraction:</p>
<pre><code>e ::= n
Expand All @@ -75,7 +80,7 @@ <h2>How to prove a compiler fully abstract</h2>
| Num : Z -&gt; Expr
| Plus : Expr -&gt; Expr -&gt; Expr
| Minus : Expr -&gt; Expr -&gt; Expr.</code></pre>
<p>Evaluation is standard (if you wanted to parse this, you would need to deal with left/right associativity, and probably add parenthesis to disambiguate, but we consider the point where you already have a tree structure, so it is unambiguous). We can write this evaluation relation as:</p>
<p>Evaluation is standard (if you wanted to parse this, you would need to deal with left/right associativity, and probably add parenthesis to disambiguate, but we consider the point where you already have a tree structure, so it is unambiguous). We can write the evaluation function as:</p>
<pre class="coq"><code>Fixpoint eval_Expr (e : Expr) : Z :=
match e with
| Num n =&gt; n
Expand Down Expand Up @@ -268,6 +273,7 @@ <h2>How to prove a compiler fully abstract</h2>
specialize (eqsource c'); simpl in *; congruence.
Qed.</code></pre>
<p>This was obviously a very tiny language and a very restrictive linker that only allowed very restrictive contexts, which was done primarily to make the proofs very short, but the general shape of the proof is the same as that used in more realistic languages published in research conferences today!</p>
<p>So next time you see a result about a correct (or even hoped to be correct), ask if it is fully abstract! And if it’s not, are the violations of equivalences something that could be exploited? Or something that would invalidate optimizations?</p>
<blockquote>
<p>As stated at the top of the post, all the code in this post is available at <a href="https://github.com/dbp/howtoprovefullabstraction" class="uri">https://github.com/dbp/howtoprovefullabstraction</a>. If you have any trouble getting it going, open an issue on that repository.</p>
</blockquote>
Expand Down
17 changes: 16 additions & 1 deletion drafts/how-to-prove-a-compiler-fully-abstract.md
Expand Up @@ -37,6 +37,8 @@ programs formally as:
p1 ≈ p1
```

### Contextual equivalence

But we often also want to compile not just whole programs, but particular
modules, or expressions, or in the general sense, **components**, and in that case, we
want an analogous notion of equivalence. Two components are **contextually equivalent** if in all program
Expand Down Expand Up @@ -90,6 +92,8 @@ secure compilers for, and while it's not the topic of this post, it's always
important to know what you mean by contextual equivalence, which usually means:
_what can program contexts determine about components_.

### Part 1. Equivalence reflection

With that in mind, what does **equivalence reflection** and **equivalence
preservation** for a compiler mean? Let's start with **equivalence reflection**,
as that's the property that all your correct compilers already have. Equivalence
Expand Down Expand Up @@ -131,6 +135,8 @@ different target programs not only for source programs that were observationally
different, but even syntactically different! That would be a pretty bad
compiler, and certainly not correct, but it would be equivalence reflecting.

### Part 2. Equivalence preservation

Equivalence preservation, on the other hand, is the hallmark of fully abstract
compilers, and it is a property that even most correct compilers do not have,
though it would certainly be great if they did. It says that if two source
Expand Down Expand Up @@ -169,6 +175,8 @@ component _in my language_, but the whole program can no longer work.
Proving a compiler fully abstract, therefore, is all about proving equivalence
preservation. So how do we do it?

### How to prove equivalence preservation

Looking at what we have to prove, we see that given contextually equivalent
source components `s1` and `s2`, we need to show that `t1` and `t2` are
contextually equivalent. We can expand this to explicitly quantify over the
Expand Down Expand Up @@ -207,6 +215,8 @@ Cs'[s1] ≈ Cs'[s2]
Ct[t1] ? Ct[t2]
```

### Concrete example of languages, compiler, & proof of full abstraction

Let's make this concrete with an example. This will be presented somewhat in english and some in Coq.

Our source language is arithmetic expressions over integers with addition and
Expand All @@ -230,7 +240,7 @@ Inductive Expr : Set :=
Evaluation is standard (if you wanted to parse this, you would need to deal with
left/right associativity, and probably add parenthesis to disambiguate, but we
consider the point where you already have a tree structure, so it is
unambiguous). We can write this evaluation relation as:
unambiguous). We can write the evaluation function as:

```coq
Fixpoint eval_Expr (e : Expr) : Z :=
Expand Down Expand Up @@ -591,6 +601,11 @@ allowed very restrictive contexts, which was done primarily to make the proofs
very short, but the general shape of the proof is the same as that used in more
realistic languages published in research conferences today!

So next time you see a result about a correct (or even hoped to be correct), ask
if it is fully abstract! And if it's not, are the violations of equivalences
something that could be exploited? Or something that would invalidate
optimizations?


> As stated at the top of the post, all the code in this post is available at
> [https://github.com/dbp/howtoprovefullabstraction](https://github.com/dbp/howtoprovefullabstraction).
Expand Down

0 comments on commit 3abd9e7

Please sign in to comment.