Browse Source

Finish up draft of Coq post.

donations
Danila Fedorin 11 months ago
parent
commit
dcb1e9a736
  1. 116
      content/blog/01_aoc_coq.md

116
content/blog/01_aoc_coq.md

@ -39,7 +39,7 @@ we go about using _inference rules_. Let's talk about those next.
#### Inference Rules
Inference rules are a very general notion. The describe how we can determine (infer) a conclusion
from a set of assumption. It helps to look at an example. Here's a silly little inference rule:
from a set of assumptions. It helps to look at an example. Here's a silly little inference rule:
{{< latex >}}
\frac
@ -181,7 +181,7 @@ it will add 0 to the accumulator (keeping it the same),
do nothing, and finally jump back to the beginning. At this point, it will try to run the addition instruction again,
which is not allowed; thus, the program will terminate.
Did you catch that? The semantics of this language will require more information than just our program itself (which we'll denote \\(p\\)).
Did you catch that? The semantics of this language will require more information than just our program itself (which we'll denote by \\(p\\)).
* First, to evaluate the program we will need a program counter, \\(\\textit{c}\\). This program counter
will tell us the position of the instruction to be executed next. It can also point past the last instruction,
which means our program terminated successfully.
@ -242,16 +242,30 @@ is done evaluating, and is in a "failed" state.
We use \\(\\text{length}(p)\\) to represent the number of instructions in \\(p\\). Note the second premise:
even if our program counter \\(c\\) is not included in the valid set, if it's "past the end of the program",
the program terminates in an "ok" state. Here's a rule for terminating in the "ok" state:
the program terminates in an "ok" state.
{{< sidenote "left" "avoid-c-note" "Here's a rule for terminating in the \"ok\" state:" >}}
In the presented rule, we don't use the variable <code>c</code> all that much, and we know its concrete
value (from the equality premise). We could thus avoid introducing the name \(c\) by
replacing it with said known value:
{{< latex >}}
\frac{}
{(\text{length}(p), a, v) \Rightarrow_{p} (\text{length}(p), a, v)}
{{< /latex >}}
This introduces some duplication, but that is really because all "base case" evaluation rules
start and stop in the same state. To work around this, we could define a separate proposition
to mean "program \(p\) is done in state \(s\)", then \(s\) will really only need to occur once,
and so will \(\text{length}(p)\). This is, in fact, what we will do later on,
since being able to talk abut "programs being done" will help us with
components of our proof.
{{< /sidenote >}}
{{< latex >}}
\frac{c = \text{length}(p)}
{(c, a, v) \Rightarrow_{p} (c, a, v)}
{{< /latex >}}
{{< todo >}}
We can make this closer to the Coq version.
{{< /todo >}}
When our program counter reaches the end of the program, we are also done evaluating it. Even though
both rules {{< sidenote "right" "redundant-note" "lead to the same conclusion," >}}
In fact, if the end of the program is never included in the valid set, the second rule is completely redundant.
@ -350,8 +364,8 @@ Inductive t A : nat -> Type :=
```
The `nil` constructor represents the empty list \\([]\\), and `cons` represents
the operation of prepending an element (called `h` in the code in \\(x\\) in our inference rules)
to another vector of length \\(n\\), which is remains unnamed in the code but is called \\(\\textit{xs}\\) in our rules.
the operation of prepending an element (called `h` in the code and \\(x\\) in our inference rules)
to another vector of length \\(n\\), which remains unnamed in the code but is called \\(\\textit{xs}\\) in our rules.
These two definitions work together quite well. For instance, suppose we have a vector of length \\(n\\).
If we were to access its elements by indices starting at 0, we'd be allowed to access indices 0 through \\(n-1\\).
@ -369,8 +383,6 @@ and convert that index into a \\(\\text{Fin} \\; n\\). We formalize it in a lemm
{{< codelines "Coq" "aoc-2020/day8.v" 80 82 >}}
{{< todo >}}Prove this (at least informally) {{< /todo >}}
There's a little bit of a gotcha here. Instead of translating our above statement literally,
and returning a value that's the result of "tightening" our input `f`, we return a value
`f'` that can be "weakened" to `f`. This is because "tightening" is not a total function -
@ -378,6 +390,29 @@ it's not always possible to convert a \\(\\text{Fin} \\; (n+1)\\) into a \\(\\te
However, "weakening" \\(\\text{Fin} \\; n\\) _is_ a total function, since a number less than \\(n\\)
is, by the transitive property of a total order, also less than \\(n+1\\).
The Coq proof for this claim is as follows:
{{< codelines "Coq" "aoc-2020/day8.v" 88 97 >}}
The `Fin.rectS` function is a convenient way to perform inductive proofs over
our finite natural numbers. Informally, our proof proceeds as follows:
* If the current finite natural number is zero, take a look at the "bound" (which
we assume is nonzero, since there isn't a natural number less than zero).
* If this "bounding number" is one, our `f` can't be tightened any further,
since doing so would create a number less than zero. Fortunately, in this case,
`n` must be `0`, so `f` is the finite representation of `n`.
* Otherwise, `f` is most definitely a weakened version of another `f'`,
since the tightest possible type for zero has a "bounding number" of one, and
our "bounding number" is greater than that. We return a tighter version of our finite zero.
* If our number is a successor of another finite number, we check if that other number
can itself be tightened.
* If it can't be tightened, then our smaller number is a finite representation of
`n-1`. This, in turn, means that adding one to it will be the finite representation
of `n` (if \\(x\\) is equal to \\(n-1\\), then \\(x+1\\) is equal to \\(n\\)).
* If it _can_ be tightened, then so can the successor (if \\(x\\) is less
than \\(n-1\\), then \\(x+1\\) is less than \\(n\\)).
Next, let's talk about addition, specifically the kind of addition done by the \\(\\texttt{jmp}\\) instruction.
We can always add an integer to a natural number, but we can at best guarantee that the result
will be an integer. For instance, we can add `-1000` to `1`, and get `-999`, which is _not_ a natural
@ -393,7 +428,7 @@ that Coq provides facilities for working with arbitrary implementations of integ
without relying on how they are implemented under the hood. This can be seen in its
[`Coq.ZArith.Int`](https://coq.inria.fr/library/Coq.ZArith.Int.html) module,
which describes what functions and types an implementation of integers should provide.
Among those is `t`, the type an integer in such an arbitrary implementation. We too
Among those is `t`, the type of an integer in such an arbitrary implementation. We too
will not make an assumption about how the integers are implemented, and simply
use this generic `t` from now on.
@ -453,7 +488,7 @@ providing a proof that `valid_jump_t pc t = Some pc'`.
{{< codelines "Coq" "aoc-2020/day8.v" 103 110 >}}
Next, it will help us to combine the premises for a
Next, it will help us to combine the premises for
"failed" and "ok" terminations into Coq data types.
This will make it easier for us to formulate a lemma later on.
Here are the definitions:
@ -465,7 +500,7 @@ end in the same state, there's no reason to
write that state twice. Thus, both `done`
and `stuck` only take the input `inp`,
and the state, which includes the accumulator
`acc`, set of allowed program counters `v`, and
`acc`, the set of allowed program counters `v`, and
the program counter at which the program came to an end.
When the program terminates successfully, this program
counter will be equal to the length of the program `n`,
@ -483,7 +518,7 @@ Finally, we encode the three inference rules we came up with:
Notice that we fused two of the premises in the last rule.
Instead of naming the instruction at the current program
counter and using it in another premise, we simply use
counter (by writing \\(p[c] = i\\)) and using it in another premise, we simply use
`nth inp pc`, which corresponds to \\(p[c]\\) in our
"paper" semantics.
@ -508,14 +543,14 @@ For this, we can use the following two inference rules:
{{< latex >}}
\frac
{c : \text{Fin} \; n}
{\texttt{acc} \; t \; \text{valid for} \; n, c }
{\texttt{add} \; t \; \text{valid for} \; n, c }
\quad
\frac
{c : \text{Fin} \; n \quad o \in \{\texttt{nop}, \texttt{jmp}\} \quad J_v(c, t) = \text{Some} \; c' }
{o \; t \; \text{valid for} \; n, c }
{{< /latex >}}
The first rule states that if a program has length \\(n\\), then it's valid
The first rule states that if a program has length \\(n\\), then \\(\\texttt{add}\\) is valid
at any program counter whose value is less than \\(n\\). This is because running
\\(\\texttt{add}\\) will increment the program counter \\(c\\) by 1,
and thus, create a new program counter that's less than \\(n+1\\),
@ -524,7 +559,7 @@ which, as we discussed above, is perfectly valid.
The second rule works for the other two instructions. It has an extra premise:
the result of `jump_valid_t` (written as \\(J_v\\)) has to be \\(\\text{Some} \\; c'\\),
that is, `jump_valid_t` must succeed. Note that we require this even for no-ops,
since it later turns out of the them may be a jump after all.
since it later turns out that one of the them may be a jump after all.
We now have our validity rules. If an instruction satisfies them for a given program
and at a given program counter, evaluating it will always result in a program counter that has a proper value.
@ -576,7 +611,7 @@ available to all of the proofs we write in this section.
The first proof is rather simple. The claim is:
> For our valid program, at any program counter `pc`
and accumulator `acc`, there must exists another program
and accumulator `acc`, there must exist another program
counter `pc'` and accumulator `acc'` such that the
instruction evaluation relation \\((\rightarrow_i)\\)
connects the two. That is, valid addresses aside,
@ -676,7 +711,7 @@ That is, `(jmp, t0)` is a valid instruction at `pc`. Then, using
Coq's `inversion` tactic, we ask: how is this possible? There is
only one inference rule that gives us such a conclusion, and it is named `valid_inst_jmp`
in our Coq code. Since we have a proof that our `jmp` is valid,
it must mean that this rule was used. Furthermore, sicne this
it must mean that this rule was used. Furthermore, since this
rule requires that `valid_jump_t` evaluates to `Some f'`, we know
that this must be the case here! Coq now has adds the following
two lines to our proof state:
@ -820,7 +855,7 @@ are fairly trivial:
{{< codelines "Coq" "aoc-2020/day8.v" 237 240 >}}
We basically connect the dots between the premises (in a form like `done`)
and the corresponding inference rule (`run_noswap_done`). The more
and the corresponding inference rule (`run_noswap_ok`). The more
interesting case is when we can take a step.
{{< codelines "Coq" "aoc-2020/day8.v" 241 253 >}}
@ -864,10 +899,43 @@ this proof will __return to us the final program counter and accumulator!__
This is precisely what we'd need to solve part 1.
But wait, almost? What's missing? We're missing a few implementation details:
* We've not provided a concrete impelmentation of integers.
* We've not provided a concrete impelmentation of integers. The simplest
thing to do here would be to use [`Coq.ZArith.BinInt`](https://coq.inria.fr/library/Coq.ZArith.BinInt.html),
for which there is a module [`Z_as_Int`](https://coq.inria.fr/library/Coq.ZArith.Int.html#Z_as_Int)
that provides `t` and friends.
* We assumed (reasonably, I would say) that it's possible to convert a natural
number to an integer.
number to an integer. If we're using the aforementioned `BinInt` module,
we can use [`Z.of_nat`](https://coq.inria.fr/library/Coq.ZArith.BinIntDef.html#Z.of_nat).
* We also assumed (still reasonably) that we can try convert an integer
back to a finite natural number, failing if it's too small or too large.
{{< todo >}}Finish up{{< /todo >}}
There's no built-in function for this, but `Z`, for one, distinguishes
between the "positive", "zero", and "negative" cases, and we have
`Pos.to_nat` for the positive case.
Well, I seem to have covered all the implementation details. Why not just
go ahead and solve the problem? I tried, and ran into two issues:
* Although this is "given", we assumed that our input program will be
valid. For us to use the result of our Coq proof, we need to provide it
a constructive proof that our program is valid. Creating this proof is tedious
in theory, and quite difficult in practice: I've run into a
strange issue trying to pattern match on finite naturals.
* Even supposing we _do_ have a proof of validity, I'm not certain
if it's possible to actually extract an answer from it. It seems
that Coq distinguishes between proofs (things of type `Prop`) and
values (things of type `Set`). things of types `Prop` are supposed
to be _erased_. This means that when you convert Coq code,
to, say, Haskell, you will see no trace of any `Prop`s in that generated
code. Unfortunately, this also means we
[can't use our proofs to construct values](https://stackoverflow.com/questions/27322979/why-coq-doesnt-allow-inversion-destruct-etc-when-the-goal-is-a-type),
even though our proof objects do indeed contain them.
So, we "theoretically" have a solution to part 1, down to the algorithm
used to compute it and a proof that our algorithm works. In "reality", though, we
can't actually use this solution to procure an answer. Like we did with day 1, we'll have
to settle for only a proof.
Let's wrap up for this post. It would be more interesting to devise and
formally verify an algorithm for part 2, but this post has already gotten
quite long and contains a lot of information. Perhaps I will revisit this
at a later time. Thanks for reading!

Loading…
Cancel
Save