Skip to content

Commit 2c29af4

Browse files
feat(formal): discharge optimize_is_permutation — axiom to theorem, in both modules (#206)
feat(formal): discharge optimize_is_permutation — axiom to theorem, in both modules `optimize_is_permutation` was an `Axiom` in `formal/Planner.v` and, separately, a second identical `Axiom` in `formal/PlannerSemantic.v`. Both are now gone. **It was not merely unproven — it was unprovable.** `optimize` was a `Parameter`, so nothing constrained it: `fun _ => []` inhabits the parameter and refutes `forall lp, Permutation lp (optimize lp)`. Assuming the statement was assuming the conclusion, and no amount of proof effort against that signature would have worked. Discharging it *requires* replacing the parameter with a definition, which is what this does: * `modality` becomes an `Inductive` with the six constructors of `crate::Modality`, and `execution_priority` transcribes the Rust match arms (Temporal 10, Vector 20, Document 30, Graph 40, Tensor 50, Semantic 90). * `optimize` becomes `Coq.Sorting.Mergesort`'s `sort` under `node_leb` — execution priority first, cost as tie-breaker, i.e. the key `Optimizer::optimize`'s `sort_by` comparator actually uses. * The permutation property falls out of the standard library's `Permuted_sort`, which is itself closed under the global context. All five call sites were left untouched and still compile: the theorem statement is character-for-character what the axiom asserted. `PlannerSemantic.v` claimed in a comment to be "reusing Planner.v abstractions" while in fact re-declaring its own `modality`, `condition`, `node`, `optimize` and a duplicate axiom. The two files agreed only by coincidence, and the duplicate meant discharging Planner's axiom would have left an identical assumption standing next door. It now genuinely `Require Import Planner`, and `formal/Justfile` gains the `planner-semantic: planner` dependency edge that this makes necessary. **Whitelists tightened — this is the acceptance test, not bookkeeping.** The guards enforce "exactly these assumptions", never "no assumptions", so leaving `optimize_is_permutation` listed would have let it return silently: * Planner: `modality|condition|optimize|optimize_is_permutation` -> `condition|node_cost` * PlannerSemantic: drops `modality`, `optimize`, `optimize_is_permutation` Both `formal/Justfile` and `.github/workflows/coq-build.yml` updated together. Two `Parameter`s remain **by design**, and being parametric in them makes the result stronger, not weaker — it holds for *any* condition representation and *any* cost function: `condition` (the Rust `ConditionKind`'s nine constructors are irrelevant to whether nodes are reordered or lost) and `node_cost` (abstracts `CostModel::estimate`'s `time_ms`). Two honest model-vs-code gaps, both recorded in the module header rather than elided, neither affecting the permutation property: * `node_cost` returns `nat`; the Rust tie-breaker is `f64` compared with `partial_cmp(...).unwrap_or(Equal)`. A NaN would make that comparator non-transitive and violate `sort_by`'s contract. NaN is unreachable today — the only division in `CostModel::estimate` is guarded and feeds `selectivity`, not `time_ms` — but nothing in the Rust types enforces that. * Rust `optimize` is partial (`PlannerError::EmptyPlan`); `sort []` is `[]`. `exec_node_comm` deliberately stays. It is a genuine spec axiom over an uninterpreted `exec_node` and needs per-operator semantics to discharge — not something this change touches, and it should not look like collateral. Verified: * Clean build: `just -f formal/Justfile all` and `check-assumptions` both exit 0, 9/9 OK, with the tightened whitelists. * `optimize_is_permutation` appears zero times in either module's `Print Assumptions` output, and no `Axiom optimize_is_permutation` remains anywhere (was 2). * **Negative control** — reintroducing an axiom a theorem depends on is caught: `ERROR(Planner): unexpected axiom: sneaky_regression`, gate exit 1. The tightened whitelist genuinely fails rather than passing vacuously. * Stub `coqc` still drives the gate to exit 127, so #202's fix is intact. * `reuse lint` compliant.
1 parent 27dfa66 commit 2c29af4

4 files changed

Lines changed: 156 additions & 32 deletions

File tree

.github/workflows/coq-build.yml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,18 +154,26 @@ jobs:
154154
working-directory: formal
155155
run: |
156156
set -euo pipefail
157-
# Whitelist: modality, condition, optimize, optimize_is_permutation
157+
# Whitelist: condition, node_cost.
158+
#
159+
# Tightened 2026-07-28. Was: modality, condition, optimize,
160+
# optimize_is_permutation. `optimize_is_permutation` is now a Theorem
161+
# discharged from Coq.Sorting.Mergesort's Permuted_sort, and both
162+
# `modality` and `optimize` are concrete definitions, so all three
163+
# must NOT reappear here. Re-adding any of them would let the axiom
164+
# come back silently -- this list is the acceptance test for that
165+
# work, not bookkeeping.
158166
UNEXPECTED=$(awk '/^[A-Za-z_][A-Za-z0-9_]* :/ {print $1}' Planner.out \
159167
| sort -u \
160-
| grep -vE '^(modality|condition|optimize|optimize_is_permutation)$' || true)
168+
| grep -vE '^(condition|node_cost)$' || true)
161169
if [ -n "$UNEXPECTED" ]; then
162170
echo "ERROR(Planner): unexpected axiom(s) in Print Assumptions:"
163171
echo "$UNEXPECTED"
164172
echo "---"
165173
cat Planner.out
166174
exit 1
167175
fi
168-
echo "OK(Planner): Q1-lite 3 theorems x 4 axioms whitelisted"
176+
echo "OK(Planner): Q1-lite 3 theorems x 2 parameters whitelisted (0 propositional axioms)"
169177
170178
- name: Verify WAL assumptions whitelist
171179
working-directory: formal
@@ -219,7 +227,7 @@ jobs:
219227
set -euo pipefail
220228
UNEXPECTED=$(awk '/^[A-Za-z_][A-Za-z0-9_]* :/ {print $1}' PlannerSemantic.out \
221229
| sort -u \
222-
| grep -vE '^(modality|condition|octad|exec_node|exec_node_comm|optimize|optimize_is_permutation)$' || true)
230+
| grep -vE '^(condition|node_cost|octad|exec_node|exec_node_comm)$' || true)
223231
if [ -n "$UNEXPECTED" ]; then
224232
echo "ERROR(PlannerSemantic): unexpected axiom(s):"
225233
echo "$UNEXPECTED"

formal/Justfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ normalizer:
3737
vcl:
3838
{{COQC}} -q VCL.v > VCL.out
3939

40-
planner-semantic:
40+
planner-semantic: planner
4141
{{COQC}} -q PlannerSemantic.v > PlannerSemantic.out
4242

4343
octad:
@@ -81,11 +81,11 @@ check-transaction: transaction
8181
check-planner: planner
8282
@awk '/^[A-Za-z_][A-Za-z0-9_]* :/ {print $1}' Planner.out \
8383
| sort -u \
84-
| grep -vE '^(modality|condition|optimize|optimize_is_permutation)$' \
84+
| grep -vE '^(condition|node_cost)$' \
8585
| { if read -r line; then \
8686
echo "ERROR(Planner): unexpected axiom: $line"; cat; exit 1; \
8787
else \
88-
echo "OK(Planner): Q1-lite 3 theorems x 4 axioms whitelisted"; \
88+
echo "OK(Planner): Q1-lite 3 theorems x 2 parameters whitelisted (0 propositional axioms)"; \
8989
fi; }
9090

9191
check-wal: wal
@@ -120,7 +120,7 @@ check-vcl: vcl
120120
check-planner-semantic: planner-semantic
121121
@awk '/^[A-Za-z_][A-Za-z0-9_]* :/ {print $1}' PlannerSemantic.out \
122122
| sort -u \
123-
| grep -vE '^(modality|condition|octad|exec_node|exec_node_comm|optimize|optimize_is_permutation)$' \
123+
| grep -vE '^(condition|node_cost|octad|exec_node|exec_node_comm)$' \
124124
| { if read -r line; then \
125125
echo "ERROR(PlannerSemantic): unexpected axiom: $line"; cat; exit 1; \
126126
else \

formal/Planner.v

Lines changed: 122 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,58 @@
2020
direction: optimize is a permutation. Semantic equivalence then
2121
follows once per-operator semantics are formalised.
2222
23-
Declared axiom: [optimize_is_permutation]. This is a structural
24-
contract on the Rust optimizer enforced by inspection of
25-
[Planner::optimize] (no node insertion, no node deletion, only
26-
reordering via [Vec::sort_by]). A property test added alongside
27-
Provenance.v's chain-link integrity check would exercise this in
28-
Rust.
23+
[optimize_is_permutation] was an [Axiom] until 2026-07-28. It is now a
24+
[Theorem]. The change matters because, as an axiom over a [Parameter]
25+
[optimize], it was not merely unproven — it was *unprovable*: nothing
26+
constrained [optimize], so [fun _ => []] inhabited the parameter and
27+
refuted the statement. Assuming it was therefore assuming the conclusion.
28+
29+
The discharge replaces the parameter with a definition, which is what
30+
makes the statement provable at all:
31+
32+
- [modality] becomes an [Inductive] mirroring [crate::Modality]'s six
33+
constructors, with [execution_priority] transcribing the Rust match
34+
arms exactly (verisim-planner/src/lib.rs).
35+
- [optimize] becomes [Mergesort]'s [sort] over a decidable total order
36+
on nodes: execution priority first, cost as tie-breaker — the same
37+
key [Optimizer::optimize]'s [sort_by] comparator uses.
38+
- The permutation property is then discharged by the standard library's
39+
[Permuted_sort], which is itself closed under the global context.
40+
41+
Two [Parameter]s remain by design, and being parametric in them makes the
42+
result stronger rather than weaker — the theorem holds for *any* condition
43+
representation and *any* cost function:
44+
45+
- [condition] : the Rust [ConditionKind] has nine constructors carrying
46+
[String]/[usize] payloads; none of that is relevant to a permutation.
47+
- [node_cost] : abstracts [CostModel::estimate]'s [time_ms]. Modelling
48+
the real cost model would constrain the theorem to one cost function
49+
for no gain.
50+
51+
Two honest gaps between this model and the Rust, neither affecting the
52+
permutation property, both worth stating rather than eliding:
53+
54+
- [node_cost] returns [nat]; the Rust tie-breaker is [f64] compared with
55+
[partial_cmp(...).unwrap_or(Equal)]. A NaN would make that comparator
56+
non-transitive and break [sort_by]'s contract. NaN is unreachable today
57+
(the only division in [CostModel::estimate] is guarded and feeds
58+
[selectivity], not [time_ms]) but nothing in the Rust types enforces it.
59+
- The Rust [optimize] is partial — it returns [PlannerError::EmptyPlan]
60+
for an empty plan — whereas [sort []] is [[]]. Total here, partial there.
61+
62+
Full Q1 (semantic equivalence) remains MODERATE and open; [exec_node_comm]
63+
in PlannerSemantic.v is a genuine spec axiom needing per-operator semantics,
64+
not something this change touches.
2965
3066
Tracking issue: hyperpolymath/verisimdb#77.
3167
*)
3268

3369
Require Import Coq.Lists.List.
3470
Require Import Coq.Sorting.Permutation.
71+
Require Import Coq.Sorting.Mergesort.
72+
Require Import Coq.Structures.Orders.
73+
Require Import Coq.Arith.PeanoNat.
74+
Require Import Lia.
3575

3676
(* Keep Print Assumptions output on one line per axiom: the CI and Justfile
3777
guards match /^name :/ with awk, and Coq's default ~78-column wrap would
@@ -42,9 +82,38 @@ Import ListNotations.
4282

4383
(** ** Domain *)
4484

45-
(** Opaque modality and condition types. Mirrors [crate::Modality]
46-
and [crate::plan::Condition] in the Rust source. *)
47-
Parameter modality : Type.
85+
(** [modality] mirrors [crate::Modality] (verisim-planner/src/lib.rs).
86+
87+
Six constructors, not the octad's eight: the planner declares its own
88+
canonical enum and does not cover Provenance or Spatial, even though both
89+
are full crates. That gap is real and tracked separately; this module
90+
mirrors the Rust as it is rather than as it should be, because a model
91+
that covered eight would no longer describe the code it validates. *)
92+
Inductive modality : Type :=
93+
| Graph
94+
| Vector
95+
| Tensor
96+
| Semantic
97+
| Document
98+
| Temporal.
99+
100+
(** Transcribes [Modality::execution_priority] arm for arm. Lower runs
101+
earlier: Temporal first (often cached), Vector/Document next (selective
102+
indexes), Graph middle, Tensor moderate, Semantic last (ZKP expensive). *)
103+
Definition execution_priority (m : modality) : nat :=
104+
match m with
105+
| Temporal => 10
106+
| Vector => 20
107+
| Document => 30
108+
| Graph => 40
109+
| Tensor => 50
110+
| Semantic => 90
111+
end.
112+
113+
(** Conditions stay opaque. [crate::plan::ConditionKind] has nine constructors
114+
(Equality, Range, Fulltext, Similarity, Traversal, AtTime,
115+
ProofVerification, TensorOp, Predicate), none of whose structure bears on
116+
whether the optimizer reorders or loses nodes. *)
48117
Parameter condition : Type.
49118

50119
(** A plan node is a (modality, condition list) pair. *)
@@ -60,14 +129,53 @@ Definition logical_plan : Type := list node.
60129
structural-preservation property below. *)
61130
Definition physical_plan : Type := list node.
62131

63-
(** ** Optimizer contract
132+
(** ** Optimizer
64133
65-
The optimizer is a permutation: it reorders the input nodes but
66-
neither adds nor drops any. *)
67-
Parameter optimize : logical_plan -> physical_plan.
134+
[CostModel::estimate]'s [time_ms], abstracted. See the header for why this
135+
stays a [Parameter] and what the [nat]-vs-[f64] gap is. *)
136+
Parameter node_cost : node -> nat.
68137

69-
Axiom optimize_is_permutation :
138+
(** The comparator from [Optimizer::optimize]'s [sort_by]: execution priority
139+
first, total cost as tie-breaker. *)
140+
Definition node_leb (a b : node) : bool :=
141+
if Nat.ltb (execution_priority (fst a)) (execution_priority (fst b)) then true
142+
else if Nat.ltb (execution_priority (fst b)) (execution_priority (fst a)) then false
143+
else Nat.leb (node_cost a) (node_cost b).
144+
145+
(** Totality — the property [f64] with [unwrap_or(Equal)] cannot guarantee, and
146+
the reason the cost carrier is [nat] here. *)
147+
Lemma node_leb_total : forall a b, node_leb a b = true \/ node_leb b a = true.
148+
Proof.
149+
intros a b. unfold node_leb.
150+
destruct (Nat.ltb (execution_priority (fst a)) (execution_priority (fst b))) eqn:Hab.
151+
- left; reflexivity.
152+
- destruct (Nat.ltb (execution_priority (fst b)) (execution_priority (fst a))) eqn:Hba.
153+
+ right; reflexivity.
154+
+ apply Nat.ltb_ge in Hab. apply Nat.ltb_ge in Hba.
155+
destruct (Nat.leb_spec (node_cost a) (node_cost b)) as [H|H].
156+
* left; reflexivity.
157+
* right. apply Nat.leb_le. lia.
158+
Qed.
159+
160+
Module PlanOrder <: TotalLeBool.
161+
Definition t := node.
162+
Definition leb := node_leb.
163+
Infix "<=?" := leb (at level 70, no associativity).
164+
Theorem leb_total : forall a1 a2, leb a1 a2 = true \/ leb a2 a1 = true.
165+
Proof. exact node_leb_total. Qed.
166+
End PlanOrder.
167+
168+
Module PlanSort := Sort PlanOrder.
169+
170+
(** The optimizer: a stable mergesort under [node_leb]. *)
171+
Definition optimize (lp : logical_plan) : physical_plan := PlanSort.sort lp.
172+
173+
(** The former axiom, now discharged from [Permuted_sort]. *)
174+
Theorem optimize_is_permutation :
70175
forall lp, Permutation lp (optimize lp).
176+
Proof.
177+
intro lp. apply PlanSort.Permuted_sort.
178+
Qed.
71179

72180
(** ** Q1-lite corollaries *)
73181

formal/PlannerSemantic.v

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
Require Import Coq.Lists.List.
5252
Require Import Coq.Sorting.Permutation.
53+
Require Import Planner.
5354

5455
(* Keep Print Assumptions output on one line per axiom: the CI and Justfile
5556
guards match /^name :/ with awk, and Coq's default ~78-column wrap would
@@ -58,14 +59,21 @@ Set Printing Width 400.
5859

5960
Import ListNotations.
6061

61-
(** ** Domain (reusing Planner.v abstractions) *)
62+
(** ** Domain
63+
64+
This module said "reusing Planner.v abstractions" while in fact
65+
re-declaring its own [modality], [condition], [node], [optimize] and a
66+
second copy of [optimize_is_permutation]. The two files agreed only by
67+
coincidence, and the duplicated axiom meant discharging it in Planner.v
68+
would have left an identical assumption standing here.
69+
70+
It now genuinely imports Planner.v, so [modality] (an [Inductive]),
71+
[node], [optimize] (a [Definition]) and the discharged
72+
[optimize_is_permutation] [Theorem] all come from one place. *)
6273

63-
Parameter modality : Type.
64-
Parameter condition : Type.
6574
Parameter octad : Type.
6675

67-
Definition node : Type := (modality * list condition)%type.
68-
Definition plan : Type := list node.
76+
Definition plan : Type := logical_plan.
6977
Definition state : Type := list octad.
7078

7179
(** ** Per-node execution semantics
@@ -93,12 +101,12 @@ Fixpoint exec_plan (p : plan) (s : state) : state :=
93101
| n :: rest => exec_plan rest (exec_node n s)
94102
end.
95103

96-
(** ** Optimizer (axiomatised from Planner.v) *)
97-
98-
Parameter optimize : plan -> plan.
104+
(** ** Optimizer
99105
100-
Axiom optimize_is_permutation :
101-
forall p, Permutation p (optimize p).
106+
Imported from Planner.v, where [optimize] is [Mergesort]'s [sort] under a
107+
decidable total order and [optimize_is_permutation] is a [Theorem]
108+
discharged from [Permuted_sort]. Previously both were re-declared here as
109+
a [Parameter] and an [Axiom]. *)
102110

103111
(** ** Helper: exec_plan is invariant under permutation of nodes *)
104112
Theorem exec_plan_perm_invariant :

0 commit comments

Comments
 (0)