Finish up draft of Coq post.
This commit is contained in:
parent
c8543961af
commit
dcb1e9a736
|
@ -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.
|
||||
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.
|
||||
|
||||
{{< todo >}}Finish up{{< /todo >}}
|
||||
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…
Reference in New Issue
Block a user