Skip to content

feat(formal): discharge optimize_is_permutation — axiom to theorem, in both modules - #206

Merged
hyperpolymath merged 3 commits into
mainfrom
feat/discharge-optimize-permutation
Jul 28, 2026
Merged

feat(formal): discharge optimize_is_permutation — axiom to theorem, in both modules#206
hyperpolymath merged 3 commits into
mainfrom
feat/discharge-optimize-permutation

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

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 Parameters 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 fix(formal): make the local Coq assumptions gate capable of failing #202's fix is intact.
  • reuse lint compliant.

…ules)

#202 landed the same three-line explanatory comment twice in every module —
once above `Set Printing Width 400.` where it belongs, and once orphaned
immediately below it. My fault: I ran the insertion, then deleted only the
`Set Printing Width 400.` line to A/B-test whether the ~78-column wrap was
actually hiding any axiom from the guard, then re-ran the insertion. The sed
that undid the test matched only the directive, not its comment, so the second
pass stacked a fresh copy on top of the leftover.

Comment-only; no directive, no proof and no guard changes. `formal/Justfile`
is untouched.

To be accurate about scope, since the obvious worry is that a text edit near
the imports clipped something: it did not. `Import ListNotations.` and every
`Require Import` are intact in all nine modules — verified before and after.
(A *different* cleanup attempt of mine did briefly eat that import by skipping
a fixed three lines past the marker, which in `Planner.v` overran the comment.
That version was reset and never committed; it is mentioned only so the next
person knows the offset-based approach is the wrong tool here. This commit
removes the second matched block specifically, keyed on occurrence count.)

Verified: real coqc 8.20.1 -> `check-assumptions` exits 0, 9/9 OK; stub coqc
-> exit 127, so the gate from #202 still fails when it should; `Normalizer.out`
still prints at 157 columns, so the wrap fix is unaffected; `reuse lint`
compliant.
…n 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.
@hyperpolymath

Copy link
Copy Markdown
Owner Author

Stacked on #205 (the comment de-duplication) — this branch contains that commit too, so merging #205 first keeps this diff clean. If #205 is closed instead, its one commit rides in here harmlessly.

@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime.
Learn more

Code Review ✅ Approved

Replaces the unprovable optimize_is_permutation axiom with a verified definition in both formal modules by implementing sorting and inductives, successfully discharging the assumption. No issues found.

Auto-approved and auto-merge armed: No blocking issues found.
Please see Auto-approve Docs for details on setting custom approval criteria. — merges when pipeline and required approvals pass.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

⚠️ Gitar auto-approved this PR but could not enable auto-merge: auto-merge is disabled for this repository — enable "Allow auto-merge" in the repository settings.

@gitar-bot gitar-bot Bot added the gitar-approved Added by Gitar label Jul 28, 2026

@gitar-bot gitar-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitar has auto-approved this PR and enabled auto-merge (configure)

@hyperpolymath
hyperpolymath marked this pull request as ready for review July 28, 2026 19:04
@hyperpolymath
hyperpolymath merged commit 2c29af4 into main Jul 28, 2026
9 checks passed
@hyperpolymath
hyperpolymath deleted the feat/discharge-optimize-permutation branch July 28, 2026 19:04
hyperpolymath added a commit that referenced this pull request Jul 28, 2026
#207)

fix(planner): carry projections and early_limit into the physical plan

`PlanNode` has four fields; `PlanStep` had six, and neither
`projections` nor
`early_limit` was among them. The optimizer computed both, then dropped
them at
the logical -> physical boundary, so the executor never saw either
pushdown.

That is worse than merely lossy, and this is the part worth flagging.
`CostModel::estimate` already *discounts* a node carrying an early limit
(`cost.rs:345-350`):

    if let Some(limit) = node.early_limit {
        let limit_factor = (limit as f64 / 1000.0).min(1.0);
        selectivity *= limit_factor;
time_ms *= 0.5 + 0.5 * limit_factor; // At least 50% of base cost
    }

So a plan was priced as cheap as half its base cost *because of* a limit
pushdown that the physical plan had no way to perform. The planner was
rewarding an optimisation it then discarded. Nothing failed loudly —
plans
simply cost less on paper than they cost to run, which is the kind of
defect
that shows up as unexplained latency rather than as an error.

Measured before the change: `early_limit` had exactly one non-test read
in the
entire tree — the cost adjustment above. `projections` had **none**;
nothing
read it after `vcl_bridge.rs` built it.

Both fields are now on `PlanStep` and populated from the node. They
carry
`#[serde(default)]`, so a `PhysicalPlan` serialised before this change
still
deserialises (absent -> `vec![]` / `None`, matching the old behaviour
rather
than inventing a limit).

This surfaced while discharging `optimize_is_permutation` (#206). The
Coq model
abstracts a node as `(modality, list condition)` — precisely the
projection that
omits the two fields being lost. The abstraction was not wrong, but it
was drawn
around the bug, which is why proving the permutation property could not
have
caught it.

Test: `test_pushdown_survives_optimization` asserts both fields survive,
and
matches steps by modality rather than index — asserting on `steps[0]`
would test
the sort order, not the pushdown. Verified as a real guard, not a
vacuous one:
with the population reverted it fails with `projection pushdown was
dropped at
the physical boundary`; with the fix it passes.

Verified: `cargo test -p verisim-planner` 118 passed / 0 failed; `cargo
clippy
-p verisim-planner --all-targets` clean; `cargo check -p verisim-api
--all-targets` clean (it maps `PlanStep` into its own GraphQL type);
`reuse
lint` compliant.

Not addressed here, and deliberately so: `pushed_predicates` remains
`Vec<String>` built by `format!("{:?}", c)`, so conditions still cross
the
boundary as Rust `Debug` output rather than as `ConditionKind`. Retyping
it is a
larger change with its own serialisation surface, and it is not needed
to stop
the cost model lying.
hyperpolymath added a commit that referenced this pull request Jul 29, 2026
#209)

test(planner): property-test the Rust image of optimize_is_permutation

`formal/Planner.v` proves `optimize_is_permutation` about a *Coq*
function.
That says nothing about this crate until something checks the Rust
against the
same statement, and `Planner.v:23-28` has promised exactly that since
the module
was written:

> A property test added alongside Provenance.v's chain-link integrity
check
  > would exercise this in Rust.

It was never added. This is that test, and it closes the refinement link
#113
owes — the half that #206 could not supply, because discharging an axiom
in Coq
tells you nothing about the Rust it models.

**The gap it closes.** `optimizer.rs` already has eight unit tests, but
they
assert *ordering* (`test_vector_before_graph`,
`test_semantic_always_last`) and
*counts*. None asserts the multiset — which is the actual content of the
permutation property, and the only thing that catches a node being
silently
dropped, duplicated, or swapped for another.

Four properties, stated over the same `(modality, list condition)`
projection
the Coq model uses so the two are comparable:

* `prop_optimize_is_permutation` — multiset preserved.
* `prop_optimize_total_cost_invariant` — permuting the input must not
change the
estimate, since that is what makes reordering a legitimate optimisation.
  Compared with a relative tolerance rather than `==`: f64 `+`/`*` are
commutative but **not associative**, so `combine` is
permutation-invariant
  only up to rounding. Same caveat recorded in `Planner.v`'s header.
* `prop_optimize_never_panics` — guards the comparator. The tie-break is
`partial_cmp(...).unwrap_or(Equal)` on f64, which a NaN makes
non-transitive,
violating `sort_by`'s contract and potentially panicking on Rust >=
1.81. NaN
looks unreachable today (the only division in `CostModel::estimate` is
guarded
and feeds `selectivity`, not `time_ms`) but nothing in the types
enforces it,
  so the belief is tested rather than asserted.
* `prop_pushdown_fields_survive` — generalises #207's single hand-built
case
  across the generated space.

Plus `empty_plan_is_rejected`, stated once because the Coq `optimize` is
total
while the Rust one is partial (`PlannerError::EmptyPlan`). Recording
that
difference beats letting the generators quietly avoid it.

**Verified as real guards, not vacuous ones.** Two negative controls:

1. Dropping a node -> 2 properties fail with `node count changed`.
2. A **count-preserving** corruption (duplicating node 0 over the last
node) —
invisible to any length assertion — is caught by the multiset with the
exact
diagnostic `("Graph", "Equality { field: \"a\", ... }"): 2`. That is the
specific case the existing unit tests cannot see, demonstrated rather
than
   claimed.

Both restored to green afterwards.

Note on the committed `.proptest-regressions`: the repo already tracks
these
(see `verisim-document`), and proptest recommends checking them in. But
be clear
about provenance — both seeds come from the *injected* failures above,
not from
any real historical bug. They shrink to a trivial one-node Graph plan
and pass
now; they are cheap to replay and should not be read as evidence of a
past
defect.

Verified: `cargo test -p verisim-planner` 118 + 5 + 4 + 1 passing, 0
failed;
clippy clean on `--all-targets`; `cargo doc --no-deps` clean (it is a
required
check); `reuse lint` compliant.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gitar-approved Added by Gitar

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant