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
|
||||||
Inference rules are a very general notion. The describe how we can determine (infer) a conclusion
|
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 >}}
|
{{< latex >}}
|
||||||
\frac
|
\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,
|
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.
|
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
|
* 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,
|
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.
|
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:
|
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",
|
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 >}}
|
{{< latex >}}
|
||||||
\frac{c = \text{length}(p)}
|
\frac{c = \text{length}(p)}
|
||||||
{(c, a, v) \Rightarrow_{p} (c, a, v)}
|
{(c, a, v) \Rightarrow_{p} (c, a, v)}
|
||||||
{{< /latex >}}
|
{{< /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
|
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," >}}
|
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.
|
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 `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)
|
the operation of prepending an element (called `h` in the code and \\(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.
|
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\\).
|
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\\).
|
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 >}}
|
{{< 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,
|
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
|
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 -
|
`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\\)
|
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\\).
|
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.
|
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
|
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
|
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
|
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,
|
[`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.
|
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
|
will not make an assumption about how the integers are implemented, and simply
|
||||||
use this generic `t` from now on.
|
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 >}}
|
{{< 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.
|
"failed" and "ok" terminations into Coq data types.
|
||||||
This will make it easier for us to formulate a lemma later on.
|
This will make it easier for us to formulate a lemma later on.
|
||||||
Here are the definitions:
|
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`
|
write that state twice. Thus, both `done`
|
||||||
and `stuck` only take the input `inp`,
|
and `stuck` only take the input `inp`,
|
||||||
and the state, which includes the accumulator
|
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.
|
the program counter at which the program came to an end.
|
||||||
When the program terminates successfully, this program
|
When the program terminates successfully, this program
|
||||||
counter will be equal to the length of the program `n`,
|
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.
|
Notice that we fused two of the premises in the last rule.
|
||||||
Instead of naming the instruction at the current program
|
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
|
`nth inp pc`, which corresponds to \\(p[c]\\) in our
|
||||||
"paper" semantics.
|
"paper" semantics.
|
||||||
|
|
||||||
|
@ -508,14 +543,14 @@ For this, we can use the following two inference rules:
|
||||||
{{< latex >}}
|
{{< latex >}}
|
||||||
\frac
|
\frac
|
||||||
{c : \text{Fin} \; n}
|
{c : \text{Fin} \; n}
|
||||||
{\texttt{acc} \; t \; \text{valid for} \; n, c }
|
{\texttt{add} \; t \; \text{valid for} \; n, c }
|
||||||
\quad
|
\quad
|
||||||
\frac
|
\frac
|
||||||
{c : \text{Fin} \; n \quad o \in \{\texttt{nop}, \texttt{jmp}\} \quad J_v(c, t) = \text{Some} \; c' }
|
{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 }
|
{o \; t \; \text{valid for} \; n, c }
|
||||||
{{< /latex >}}
|
{{< /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
|
at any program counter whose value is less than \\(n\\). This is because running
|
||||||
\\(\\texttt{add}\\) will increment the program counter \\(c\\) by 1,
|
\\(\\texttt{add}\\) will increment the program counter \\(c\\) by 1,
|
||||||
and thus, create a new program counter that's less than \\(n+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 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'\\),
|
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,
|
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
|
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.
|
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:
|
The first proof is rather simple. The claim is:
|
||||||
|
|
||||||
> For our valid program, at any program counter `pc`
|
> 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
|
counter `pc'` and accumulator `acc'` such that the
|
||||||
instruction evaluation relation \\((\rightarrow_i)\\)
|
instruction evaluation relation \\((\rightarrow_i)\\)
|
||||||
connects the two. That is, valid addresses aside,
|
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
|
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`
|
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,
|
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
|
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
|
that this must be the case here! Coq now has adds the following
|
||||||
two lines to our proof state:
|
two lines to our proof state:
|
||||||
|
@ -820,7 +855,7 @@ are fairly trivial:
|
||||||
{{< codelines "Coq" "aoc-2020/day8.v" 237 240 >}}
|
{{< codelines "Coq" "aoc-2020/day8.v" 237 240 >}}
|
||||||
|
|
||||||
We basically connect the dots between the premises (in a form like `done`)
|
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.
|
interesting case is when we can take a step.
|
||||||
|
|
||||||
{{< codelines "Coq" "aoc-2020/day8.v" 241 253 >}}
|
{{< 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.
|
This is precisely what we'd need to solve part 1.
|
||||||
|
|
||||||
But wait, almost? What's missing? We're missing a few implementation details:
|
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
|
* 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
|
* 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.
|
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