From 58f7fa55cd181b8fee21277e2ab5a9fcc282d918 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 14 Jun 2026 08:57:50 +0200 Subject: [PATCH] =?UTF-8?q?v1.9.0=20(FEAT-016=20slice-3,=20AC-011):=20octa?= =?UTF-8?q?gon=20Min=C3=A9=20strong=20closure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the referenced-paper precision item AC-011 — the final refinement of the octagon arc. Pure algebra; NO analyzer output change (FEAT-016's AC was already met in v1.8.0). scry-octagon: - strong_close: Floyd-Warshall close + the Miné tightening m[i][j] := min(m[i][j], floor((m[i][î] + m[ĵ][j]) / 2)) + re-close. Derives a difference bound between two variables from their UNARY bounds (x≤10 ∧ y≥0 ⟹ x-y≤10) that plain Floyd-Warshall cannot. Sound for integers via the floor. - bound_of now projects through strong_close. - 3 γ-sweep tests: precision win (derives x_0-x_1≤10 where close gives INF), concretization-preservation over a grid, bottom-preservation. Proof (proofs/rocq/OctagonStrongClose.v, strong_close_step_sound): from -2·vi≤a and 2·vj≤b the tightened bound floor((a+b)/2) over-approximates vj-vi — the step drops no concrete integer point. Admit-free; verified locally with Coq 9.0.1 and wired into the rocq-proofs CI job. HONEST SCOPE: the analyzer output is byte-identical to v1.8.0. Strong closure tightens difference/sum bounds, never a variable's own unary bound, and the analyzer projects octagon→per-variable intervals (unary), so on the current difference+unary corpus strong vs plain closure is invisible at the output (fixtures 08-11 unchanged). The precision becomes observable when a consumer reads relational bounds or the analyzer generates sum constraints (future work). SCRY_VERSION → 1.9.0 is a release stamp. FEAT-016 stays implemented; the octagon arc (slices 1/2a/2b-i/2b-ii/3) is now complete. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 50 +++++++++++++ artifacts/roadmap-2.0.yaml | 18 +++++ crates/scry-analyze-core/src/lib.rs | 2 +- crates/scry-octagon/src/lib.rs | 110 +++++++++++++++++++++++++++- proofs/rocq/BUILD.bazel | 16 ++++ proofs/rocq/OctagonStrongClose.v | 62 ++++++++++++++++ 6 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 proofs/rocq/OctagonStrongClose.v diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be6bc3..ac0af6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,56 @@ Versioning: [SemVer 2.0](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [1.9.0] — 2026-06-14 + +Headline: **the octagon gains Miné strong (tight) closure — pure algebra, +no analyzer output change.** This closes the referenced-paper precision item +AC-011 (FEAT-016 slice-3). Strong closure derives a ±difference bound between +two variables from their unary bounds (`x ≤ 10 ∧ y ≥ 0 ⟹ x − y ≤ 10`) that +plain Floyd–Warshall cannot. FEAT-016's acceptance criterion was already met in +v1.8.0; this is the final precision refinement of the octagon arc. + +### Added + +- **`scry_octagon::strong_close`** — Floyd–Warshall closure followed by the + octagon tightening `m[i][j] := min(m[i][j], ⌊(m[i][ī] + m[j̄][j])/2⌋)` and a + re-close. Sound for integers via the floor (`div_euclid`), matching + `bound_of`'s projection rounding. `bound_of` now projects through + `strong_close`. +- **Mechanized soundness** (`proofs/rocq/OctagonStrongClose.v`, + `strong_close_step_sound`): from `−2·vi ≤ a` and `2·vj ≤ b`, the tightened + bound `⌊(a+b)/2⌋` over-approximates `vj − vi` — the step drops no concrete + integer point. No admits/axioms; verified by the `rocq-proofs` CI job (and + locally with Coq 9.0.1). +- **3 new γ-sweep tests** in scry-octagon: the precision win + (`strong_close_derives_difference_from_unary_bounds`: derives `x_0 − x_1 ≤ 10` + from unary where `close` gives INF), concretization-preservation over a grid, + and bottom-preservation. + +### Not changed (honest scope) + +- **The analyzer output is identical to v1.8.0.** Strong closure tightens + difference/sum bounds, never a variable's own unary bound, and the analyzer + projects the octagon back to **per-variable intervals** (which read unary + bounds) — so on the current corpus, where the analyzer generates only + difference + unary constraints, strong vs. plain closure is invisible at the + output. All fixture invariants (08–11) are byte-identical. `SCRY_VERSION` → + 1.9.0 is a release stamp. The precision becomes observable only when a + consumer reads the relational (off-diagonal) bounds or the analyzer generates + sum constraints — future work, not claimed here. + +### Falsification statement + +What v1.9 claims, made falsifiable: **strong closure is sound (drops no +concrete point) and strictly more precise than Floyd–Warshall on a constraint +system where a difference is implied only by unary bounds.** Falsifier: the +γ-sweep test asserts `strong_close` and the base system admit exactly the same +concrete points (soundness), and `strong_close_derives_difference_from_unary_bounds` +asserts it derives `x_0 − x_1 ≤ 10` where `close` leaves INF (precision); the +Rocq `strong_close_step_sound` independently proves the tightening step. What +v1.9 does **not** claim: any change to the analyzer's interval output (see +above), nor the full integer tight closure's extra unary even-ification. + ## [1.8.0] — 2026-06-13 Headline: **the octagon relational product is wired into the analyzer — a loop diff --git a/artifacts/roadmap-2.0.yaml b/artifacts/roadmap-2.0.yaml index a1c26a8..79b4f1a 100644 --- a/artifacts/roadmap-2.0.yaml +++ b/artifacts/roadmap-2.0.yaml @@ -751,6 +751,24 @@ artifacts: post-fixpoint (instantiated at the octagon transfer) + host oracle over all fixtures. REMAINING (precision only, NOT the AC): slice-3 Miné strong/tight closure (referenced-paper AC-011). + + EVIDENCE (v1.9.0 — slice-3 per DD-015, AC-011): Miné STRONG closure landed + in scry-octagon (strong_close: Floyd-Warshall + the m[i][j] := + min(m[i][j], floor((m[i][ī]+m[j̄][j])/2)) tightening + re-close), deriving + a difference bound from two unary bounds (x≤10 ∧ y≥0 ⟹ x-y≤10) that plain + close cannot. Sound (γ-sweep concretization-preservation) + strictly more + precise (γ-sweep precision test) + mechanized: proofs/rocq/ + OctagonStrongClose.v strong_close_step_sound (the tightening drops no + concrete integer point, admit-free). bound_of projects through + strong_close. HONEST SCOPE: NO analyzer output change — strong closure + tightens difference/sum bounds, never a variable's own unary bound, and + the analyzer projects octagon→per-variable intervals (unary), so on the + current difference+unary corpus strong vs plain closure is invisible at + the output (fixtures 08-11 byte-identical). The precision becomes + observable only when a consumer reads relational bounds or the analyzer + generates sum constraints (future work). With slice-3 landed, the octagon + arc (slices 1/2a/2b-i/2b-ii/3) is complete; FEAT-016 implemented (could go + verified on an assessor/qualification pass). tags: [capability, relational, octagon, precision, v1.4] fields: phase: phase-2 diff --git a/crates/scry-analyze-core/src/lib.rs b/crates/scry-analyze-core/src/lib.rs index 5faf3f9..7e9fb60 100644 --- a/crates/scry-analyze-core/src/lib.rs +++ b/crates/scry-analyze-core/src/lib.rs @@ -300,7 +300,7 @@ mod domain { scry_taint::join(a, b) } } -const SCRY_VERSION: &str = "1.8.0"; +const SCRY_VERSION: &str = "1.9.0"; const INVARIANT_SCHEMA_URL: &str = "https://pulseengine.eu/scry-invariants/v1"; /// Default Wasm linear-memory page size (64 KiB). diff --git a/crates/scry-octagon/src/lib.rs b/crates/scry-octagon/src/lib.rs index cd5f98d..2d887a4 100644 --- a/crates/scry-octagon/src/lib.rs +++ b/crates/scry-octagon/src/lib.rs @@ -171,6 +171,51 @@ pub fn close(o: &Octagon) -> Octagon { r } +/// Miné **strong** closure (FEAT-016 slice-3, AC-011). After the standard +/// Floyd–Warshall [`close`], apply the octagon tightening +/// +/// ```text +/// m[i][j] := min( m[i][j], ⌊ (m[i][ī] + m[j̄][j]) / 2 ⌋ ) ī = i^1 +/// ``` +/// +/// then re-close to propagate the new bounds. This derives a ±difference/sum +/// bound between two variables from their UNARY bounds — e.g. from `x ≤ 10` +/// and `y ≥ 0` it derives `x − y ≤ 10`, which plain Floyd–Warshall (no edge +/// between `x` and `y`) cannot. Sound for integers: `v(ī) − v(i) = −2·v(i) ≤ +/// m[i][ī]` and `v(j) − v(j̄) = 2·v(j) ≤ m[j̄][j]`, so `2·(v(j) − v(i)) ≤ +/// m[i][ī] + m[j̄][j]`, hence `v(j) − v(i) ≤ ⌊·/2⌋` (the integer floor only +/// tightens, never drops a concrete point). The `/2` floor is via +/// `div_euclid`, matching [`bound_of`]'s projection rounding. +/// +/// Scope note: the tightening lands on DIFFERENCE/SUM entries, never on a +/// variable's own unary bound (`m[2k+1][2k]`, the source of the derivation), +/// so projecting a closed octagon back to per-variable intervals via +/// [`bound_of`] sees no change from strong vs. plain closure. The precision is +/// realised by consumers that read the relational (off-diagonal) bounds. +pub fn strong_close(o: &Octagon) -> Octagon { + let c = close(o); + if is_bottom(&c) { + return c; + } + let n = c.n(); + let mut r = c.clone(); + for i in 0..n { + let a = c.at(i, i ^ 1); // bounds −2·v(i) + for j in 0..n { + let b = c.at(j ^ 1, j); // bounds 2·v(j) + let t = sadd(a, b); + if t != INF { + let cand = t.div_euclid(2); // ⌊(a+b)/2⌋ — sound integer floor + if cand < r.at(i, j) { + r.set(i, j, cand); + } + } + } + } + // Propagate the tightened relational bounds. + close(&r) +} + /// `a ⊑ b` — the octagon partial order, i.e. `γ(a) ⊆ γ(b)`. Computed by /// closing `a` (so all implied bounds are explicit) and checking it is /// pointwise at least as tight as `b`. A bottom `a` is `⊑` everything. @@ -426,7 +471,11 @@ pub fn assign_add_const(o: &Octagon, k: u32, src: u32, c: i64) -> Octagon { /// iff the octagon is infeasible (⊥ — no concrete point, i.e. unreachable). /// Sound: the returned interval over-approximates `{ x_k : point ∈ γ(o) }`. pub fn bound_of(o: &Octagon, k: u32) -> Option<(i64, i64)> { - let c = close(o); + // Project through the STRONG closure (FEAT-016 slice-3): for the current + // analyzer this matches plain `close` on unary bounds, but it keeps the + // projection correct for any relational constraints a richer + // constraint-generation would add (sum/difference tightening). + let c = strong_close(o); if is_bottom(&c) { return None; } @@ -856,6 +905,65 @@ mod tests { assert_eq!(bound_of(&o, 0), None); } + /// Strong closure PRECISION: from the unary bounds `x_0 ≤ 10` and + /// `x_1 ≥ 0` it derives the difference bound `x_0 − x_1 ≤ 10`, which plain + /// Floyd–Warshall cannot (there is no edge between `x_0` and `x_1`). This + /// is the AC-011 win that distinguishes strong closure from `close`. + #[test] + fn strong_close_derives_difference_from_unary_bounds() { + let o = top(2); + let o = set_upper(&o, 0, 10); // x_0 ≤ 10 + let o = set_lower(&o, 1, 0); // x_1 ≥ 0 + // x_0 − x_1 = v(0) − v(2) ≤ c ⇒ cell m[2][0]. + let n = o.n(); + let cell = 2 * n; // m[2][0] + assert_eq!( + close(&o).m[cell], + INF, + "plain Floyd–Warshall cannot relate independent x_0, x_1" + ); + assert_eq!( + strong_close(&o).m[cell], + 10, + "strong closure must derive x_0 − x_1 ≤ 10 from x_0 ≤ 10 ∧ x_1 ≥ 0" + ); + } + + /// Strong closure is SOUND: like `close`, it only makes implied bounds + /// explicit — it never adds or drops a concrete point. Swept over a grid + /// of constraint systems (a difference + two unary bounds). + #[test] + fn strong_close_preserves_concretization() { + let base = { + let o = top(2); + let o = add_diff(&o, 0, 1, 4); // x_0 − x_1 ≤ 4 + let o = set_lower(&o, 0, -3); + let o = set_upper(&o, 0, 8); + let o = set_lower(&o, 1, -2); + set_upper(&o, 1, 6) + }; + let s = strong_close(&base); + for a in -6..=11 { + for b in -5..=9 { + assert_eq!( + gamma(&base, &[a, b]), + gamma(&s, &[a, b]), + "strong closure changed γ at ({a},{b})" + ); + } + } + } + + /// Strong closure of an infeasible system stays ⊥ (it detects the + /// contradiction via the underlying `close`). + #[test] + fn strong_close_keeps_bottom() { + let o = top(1); + let o = set_lower(&o, 0, 5); + let o = set_upper(&o, 0, 2); // 5 ≤ x_0 ≤ 2 : infeasible + assert!(is_bottom(&strong_close(&o))); + } + /// `narrow` recovers an `INF` bound (that widening discarded) from the /// re-applied candidate, while keeping already-finite bounds. #[test] diff --git a/proofs/rocq/BUILD.bazel b/proofs/rocq/BUILD.bazel index e444cc7..ed58e0f 100644 --- a/proofs/rocq/BUILD.bazel +++ b/proofs/rocq/BUILD.bazel @@ -149,3 +149,19 @@ rocq_proof_test( deps = [":octagonproject"], tags = ["rocq", "verification", "feat-016"], ) + +# FEAT-016 slice-3 (v1.9): mechanized soundness of the Miné STRONG-closure +# tightening step — deriving a difference bound `⌊(m[i][ī] + m[j̄][j])/2⌋` from +# two unary bounds never excludes a concrete integer point (AC-011). No admits, +# no axioms (DD-015). +rocq_library( + name = "octagonstrongclose", + srcs = ["OctagonStrongClose.v"], + tags = ["rocq", "verification", "feat-016"], +) + +rocq_proof_test( + name = "octagonstrongclose_test", + deps = [":octagonstrongclose"], + tags = ["rocq", "verification", "feat-016"], +) diff --git a/proofs/rocq/OctagonStrongClose.v b/proofs/rocq/OctagonStrongClose.v new file mode 100644 index 0000000..d8d8ba6 --- /dev/null +++ b/proofs/rocq/OctagonStrongClose.v @@ -0,0 +1,62 @@ +(** * FEAT-016 slice-3 — Soundness of the octagon strong-closure step (Rocq). + + v1.9 adds Miné's STRONG closure to `crates/scry-octagon` (AC-011): after the + Floyd–Warshall closure it tightens each DBM entry with + + m[i][j] := min( m[i][j], ⌊ (m[i][ī] + m[j̄][j]) / 2 ⌋ ) + + deriving a ±difference bound between two variables from their UNARY bounds + (e.g. `x ≤ 10 ∧ y ≥ 0 ⟹ x − y ≤ 10`), which plain Floyd–Warshall — having no + edge between `x` and `y` — cannot. The lattice laws and the closure's + concretization-preservation are falsified by the γ-sweep tests in that crate + (its established evidence kind). This file mechanizes the one NEW arithmetic + obligation the step introduces: that the tightened bound is SOUND — it never + excludes a concrete integer point. + + The encoding (Miné): variable `x_k` has a positive form (`v = x_k`) and a + negative form (`v = −x_k`). The cell `m[i][ī]` (ī flips the form) bounds + `v(ī) − v(i) = −2·v(i)`, and `m[j̄][j]` bounds `v(j) − v(j̄) = 2·v(j)`. So + with `a := m[i][ī]` and `b := m[j̄][j]`, the new bound on `v(j) − v(i)` is + `⌊(a + b)/2⌋`, and soundness is exactly that every integer `v(j) − v(i)` + consistent with the two unary bounds respects it. `⌊·/2⌋` is `Z.div _ 2` + (floor), matching the Rust `i64::div_euclid(2)`. No admits, no axioms (the + proof-in-slice gate, DD-015). + + Build: bazel build //proofs/rocq:octagonstrongclose + Test: bazel test //proofs/rocq:octagonstrongclose_test +*) + +From Stdlib Require Import ZArith. +From Stdlib Require Import Lia. + +Open Scope Z_scope. + +(** * The strong-closure tightening is sound. + + Let `vi`, `vj` be the concrete values of two octagon variable forms, and + `a`, `b` the unary bounds the DBM holds: `−2·vi ≤ a` (from `m[i][ī]`) and + `2·vj ≤ b` (from `m[j̄][j]`). Then the tightened difference bound + `⌊(a + b)/2⌋` over-approximates `vj − vi` — it is never violated by a + concrete point. Hence replacing `m[i][j]` with `min(m[i][j], ⌊(a+b)/2⌋)` + drops no concrete point: the strong closure is sound. *) +Theorem strong_close_step_sound : forall vi vj a b : Z, + -2 * vi <= a -> 2 * vj <= b -> vj - vi <= (a + b) / 2. +Proof. + intros vi vj a b Hi Hj. + (* 2·(vj − vi) ≤ a + b, then halve with the floor-division facts. *) + pose proof (Z.div_mod (a + b) 2 ltac:(lia)) as HM. + pose proof (Z.mod_pos_bound (a + b) 2 ltac:(lia)) as HB. + lia. +Qed. + +(** The floor is the TIGHTEST sound integer bound the halving admits: + `2·⌊(a+b)/2⌋ ≤ a + b`, so rounding down keeps the bound attainable rather + than over-tightening past an integer the constraints permit. *) +Theorem strong_close_floor_tight : forall a b : Z, + 2 * ((a + b) / 2) <= a + b. +Proof. + intros a b. + pose proof (Z.div_mod (a + b) 2 ltac:(lia)) as HM. + pose proof (Z.mod_pos_bound (a + b) 2 ltac:(lia)) as HB. + lia. +Qed.