Skip to content

FEAT-057 slice 2a: polyhedra wired into the fixpoint, wrap-gated — and measurably inert for the target class - #201

Merged
avrabe merged 1 commit into
mainfrom
feat057-slice2a-poly-fixpoint
Sep 1, 2026
Merged

FEAT-057 slice 2a: polyhedra wired into the fixpoint, wrap-gated — and measurably inert for the target class#201
avrabe merged 1 commit into
mainfrom
feat057-slice2a-poly-fixpoint

Conversation

@avrabe

@avrabe avrabe commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Slice 2a wires scry-sai-poly (shipped in 3.2.1, never connected) into the analyzer's structured-CFG fixpoint. It is sound, tested, and currently produces zero octagon-beating constraints on a real module. That negative result is the most valuable thing in this PR, and it is measured rather than inferred.

The number that matters

Run examples/poly_surface.rs (on main since #199) against scry_mcdc.wasm:

--- SHARPENED: points where polyhedra COULD exceed the octagon ---
  points with >=3 mutually-constrained locals : 2512  (10.57% of all points)

--- FEAT-057 slice 2a REALIZED: points carrying `linear` facts ---
  points with >=1 linear constraint (>=2 locals) : 1905 / 23772  (8.01%)
  total linear constraints                       : 5138
  points with a >=3-local linear constraint      : 0
  functions with >=1 linear point                : 130
  linear points among the >=3-mutually-constrained ceiling set : 1049 / 2512

The domain is live — 5,138 constraints across 130 functions — but every one is ≤2-local, i.e. nothing the octagon could not already express.

Why, instrumented rather than guessed

94 z := x + y sites reach the transfer per analysis; 0 have a provable no-wrap. Real LLVM operands are interval-⊤ at those sites (unbounded params), and LLVM emits unsigned bounds checks, which guard_op soundly refuses to refine.

This is exactly what FEAT-057's own 2026-07-14 reframing predicted: polyhedra's value is general-coefficient facts, which over wrapping i32 are unsound without value bounds, which come from guards. The blocker is unsigned-guard refinement / bits-fed bounds / param_ranges seeding — a slice-3+ lever, now recorded in the residual with the numbers.

Cost is within run noise (~21.2–22.9s, poly vs poly-inert baseline).

Verified independently, not taken on trust

check result
cargo test -p scry-sai-core -p scry-sai-poly 160 + 12 pass, 0 failed
cargo fmt --all -- --check clean
RelationalConstraint / snapshot_relational untouched (FEAT-041 output contract)
FEAT-057 status still proposed — slice 2a cannot satisfy AC3
wrap-gate mutant (if no_wrapif true, 3 sites, applied-count asserted) RED on feat057_wrap_gate_never_admits_unproven_equality; control restores 160 pass

The discriminating oracle is real: feat057_linear_beats_octagon_on_three_var_relation asserts both directions — that the witness (100,100,0) satisfies every surfaced octagon constraint and every interval bound (so the "octagon cannot express it" half is itself tested, not assumed), and that the linear output excludes it.

The wrap-gate test pins both sides — the wrapping case must be rejected and a no-wrap twin must be admitted, because "a gate that never admits an equality would be inert, not sound."

What was built

  • Poly::project (Fourier–Motzkin elimination), with every bail direction stated: overflow and the pos×neg cap drop constraints (enlarging — sound for projection); keeping a row that mentions the eliminated variable is the unsound direction and is structurally impossible. γ-swept, plus a transitive-consequence test (x≤2 ∧ y≤x ⊢ y≤2) so a drop-everything implementation cannot pass.
  • poly: Poly on FuncCtx, mirrored against mem site-for-site — record ×4, block merge ×2, loop entry/header/back-edge-join/widen/leq/cap/narrow/final/post, scrub, havoc-project. The two remaining mem-only resets are call sites (memory-only effects; Wasm locals are callee-inaccessible, as the octagon already relies on).
  • Wrap-gated transfers on i32_add/i32_sub ≠ ⊤; self-referencing shapes only project.
  • refine_poly_rel mirroring refine_octagon_rel on both br_if edges.
  • New additive ProgramPoint.linear (LinearConstraint/LinearTerm).

Known asymmetry, deliberate

The octagon narrows after widening; the poly (like mem) does not. Conservative-only.

A vacuous-test bug caught mid-work

The first loop/merge fixtures used drop — an unsupported op that scrubs the function, so the post-merge assertions were checking zero points. The committed tests assert per-pc point existence before asserting anything about it.

Refs: FEAT-057

🤖 Generated with Claude Code

https://claude.ai/code/session_01KkNzkNYzPh7366DkNijeNc

Poly rides FuncCtx in LOCKSTEP with the octagon and the segmentation
(mirrored site-for-site): joined at every merge (block exit, break-state
record), widened at every loop header, reset on degrade, and PROJECTED
(new Poly::project, Fourier-Motzkin elimination) on every write the
transfer does not model — havoc'd write-sets included.

THE WRAP GATE: over Wasm's wrapping i32, an assignment equality
(z = x + y) is added ONLY when the interval domain proves no-wrap
(scry_interval::i32_add / i32_sub return TOP exactly when wrap is
possible); otherwise the assigned variable is projected. Never an
unguarded equality. Two-local signed guards refine the poly on both
edges, mirroring refine_octagon_rel.

Output: NEW additive ProgramPoint.linear (constraints over >=2 locals);
RelationalConstraint / snapshot_relational untouched (FEAT-041 contract).

Poly::project bails (coefficient overflow, pos*neg cap) all DROP
constraints — the sound direction for a projection; each direction is
stated in comments. Gamma-swept, plus a transitive-consequence
precision test so a lazy drop-everything implementation cannot pass.

Oracles (every one observed RED under a targeted mutant):
  - 3-var relation z = x + y the octagon provably cannot express:
    linear output must ENTAIL it AND exclude the witness (100,100,0),
    which satisfies every octagon relational + interval fact at that pc.
  - wrap fixture (i32::MAX + 1): concrete-run gamma-check at every pc;
    no-wrap twin (5 + 7) pins the gate's other side (not inert).
  - merge-join, loop-back-edge, and guard-direction gamma-checks with
    per-path concrete valuations.
Mutants killed: forced equality (M1), dropped merge join (M2), dropped
back-edge join (M3), swapped guard edges (M4), no-project-on-write
(M5), project-keeps-eliminated-variable (P1).

MEASURED REALIZATION on scry_mcdc.wasm (poly_surface extended, 25,144
points): 2,020 points (8.03%) carry linear facts — all 2-variable; the
>=3-local count is ZERO. Instrumented why: 94 z := x + y sites reach
the transfer, 0 with provable no-wrap (operands interval-TOP; LLVM
bounds checks are unsigned compares guard_op soundly refuses). Wired
and sound, but INERT for the octagon-beating class on this corpus;
cost within run noise. AC#3 stays OPEN (slice 3) — FEAT-057 remains
`proposed`; residual updated with the numbers and the levers.

Refs: FEAT-057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkNzkNYzPh7366DkNijeNc
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

📐 rivet artifact delta

PR: #201 Base SHA: 8a7de290

Validation

head — `rivet validate` result
  SR-11 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-12 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-13 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-2 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-3 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-4 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-5 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-6 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-7 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-8 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-9 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SYS-1 (system-req, status: accepted) — missing: sys-integration-verification
  SYS-2 (system-req, status: accepted) — missing: sys-integration-verification
  SYS-3 (system-req, status: accepted) — missing: sys-integration-verification
  SYS-4 (system-req, status: accepted) — missing: sys-integration-verification
  SYS-5 (system-req, status: accepted) — missing: sys-integration-verification
  → run `rivet validate --explain SR-1` to see which link type and source types satisfy a gap

Result: PASS (188 warnings)
Schemas: common@0.3.0 (embedded), dev@0.3.0 (embedded), research@0.1.0 (embedded), research-ext@0.1.0 (on-disk), dev-ext@0.1.0 (on-disk), safety-case@0.1.0 (embedded), aspice@0.2.0 (embedded)
base — `rivet validate` result (for comparison)
  SR-11 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-12 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-13 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-2 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-3 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-4 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-5 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-6 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-7 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-8 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SR-9 (sw-req, status: accepted) — missing: sw-integration-verification, unit-verification
  SYS-1 (system-req, status: accepted) — missing: sys-integration-verification
  SYS-2 (system-req, status: accepted) — missing: sys-integration-verification
  SYS-3 (system-req, status: accepted) — missing: sys-integration-verification
  SYS-4 (system-req, status: accepted) — missing: sys-integration-verification
  SYS-5 (system-req, status: accepted) — missing: sys-integration-verification
  → run `rivet validate --explain SR-1` to see which link type and source types satisfy a gap

Result: PASS (188 warnings)
Schemas: common@0.3.0 (embedded), dev@0.3.0 (embedded), research@0.1.0 (embedded), research-ext@0.1.0 (on-disk), dev-ext@0.1.0 (on-disk), safety-case@0.1.0 (embedded), aspice@0.2.0 (embedded)

Artifact stats

base head
Total artifacts 267 267
full stats — head
Artifact summary:
  academic-reference               24
  competitive-analysis             11
  design-decision                  22
  feature                          97
  market-finding                    7
  requirement                      21
  safety-context                    3
  safety-goal                       5
  safety-justification              4
  safety-solution                   6
  safety-strategy                   1
  stakeholder-req                   3
  sw-req                           13
  sw-verification                  13
  sys-verification                  5
  system-req                        5
  technology-evaluation            12
  verification                     15
  TOTAL                           267

Orphan artifacts (no links): 12
  CA-001
  CA-002
  CA-003
  CA-004
  CA-005
  CA-006
  CA-007
  CA-008
  CA-009
  CA-010
  CA-011
  FEAT-078

Diagnostics: 0 error(s), 188 warning(s), 14 info(s)

Diff (base → head)

~ FEAT-057
  field changed: residual

0 added, 0 removed, 1 modified, 266 unchanged

AADL model — head

spar/scry.aadl: OK

Posted by the rivet-delta workflow. Informational only — does not gate the PR.

@avrabe
avrabe merged commit 3444b75 into main Sep 1, 2026
13 checks passed
@avrabe
avrabe deleted the feat057-slice2a-poly-fixpoint branch September 1, 2026 21:16
avrabe added a commit that referenced this pull request Sep 1, 2026
…ice 2a inert (#202)

FEAT-057 slice 2a (#201) shipped polyhedra into the fixpoint and
MEASURED ITSELF INERT for the class it exists to serve: 5,138 linear
constraints across 130 functions on scry_mcdc.wasm, and ZERO involving
three or more locals -- nothing the octagon could not already express.
Instrumented, not guessed: of 94 `z := x + y` sites reaching the
transfer, ZERO have a provable no-wrap, because the operands are ⊤.

The operands are ⊤ because the guard that would bound them is declined.
`guard_op` maps only SIGNED comparisons; every `*U` variant returns
`None`, and the code says why -- unsigned comparisons wrap, so refining
with a signed constant is not sound. That refusal is CORRECT. It is also
the most consequential precision decision in the analyzer, because LLVM
emits array bounds checks as `i32.lt_u index, len`: the one idiom that
would bound an index is the one shape scry declines to read.

THE REFINEMENT IS AVAILABLE UNDER A SIDE CONDITION, and the side
condition is exactly the bounds-check shape. For `x <u c` with
`0 <= c <= 2^31`, the SIGNED interval `[0, c-1]` is sound -- unsigned
`x < c <= 2^31` puts x in `[0, 2^31-1]`, representable as non-negative
signed, so no wrap into the negative half.

VERIFIED EXHAUSTIVELY at w=8 over all (c, x) pairs BEFORE filing, in both
directions. Zero counterexamples for `0 <= c <= 2^(w-1)`. And 127
constants ABOVE it where the signed reading FAILS -- so the side
condition is load-bearing, not decorative, and a version that dropped it
would be unsound. A rule that held for every c would have meant I had
mis-stated it.

NOT A POLYHEDRA FEATURE. Bounding locals feeds the interval domain, the
octagon, the no-wrap gate, the trap checks, and the OOB proven rate
(FEAT-069 measured 0.67% and diagnosed PRECISION, not format). Filed as
its own artifact rather than buried in FEAT-057's residual because it
blocks several things and, like FEAT-095, nothing tracked it.

Typed `depends-on FEAT-057 -> FEAT-098` so the dependency is machine-
visible rather than prose (the FEAT-096 lesson). Both are v3.4.0, so the
ordering gate stays green -- CONFIRMED IT IS ACTUALLY READ by mutating
FEAT-098 into v3.5.0: the gate fired naming the new pair, then went green
on revert. First inversion the gate has caught that I did not seed at
build time.

AC#4 and AC#5 demand the MEASURED delta after implementation -- the
>=3-local count (0 today), the no-wrap-provable share of 94 sites (0
today), and FEAT-069's 0.67%. "The number did not move" is written to be
a reportable outcome rather than a silent one.

Refs: FEAT-098


Claude-Session: https://claude.ai/code/session_01KkNzkNYzPh7366DkNijeNc

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@avrabe

avrabe commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: what came of the inert result. This PR merged reporting zero octagon-beating constraints, and that number has now been chased to a named cause and filed. Recording it here so this PR does not read as a dead end.

The chain, end to end

step measured
points where polyhedra could exceed the octagon (≥3 mutually-constrained locals) 2,512 (10.57%)
points this slice actually gives linear facts 1,905 (8.01%), 5,138 constraints, 130 functions
of those, with a ≥3-local constraint 0
z := x + y sites reaching the transfer 94
of those, with a provable no-wrap 0

Every linear constraint emitted is ≤2-local — nothing the octagon could not already express.

Root cause

The operands are ⊤ because the guard that would bound them is declined. guard_op maps only signed comparisons to a GuardOp; every *U variant returns None, and the source says why:

"unsigned comparisons wrap, so refining their bounds with a signed constant is not sound"

That refusal is correct. It is also the most consequential precision decision in the analyzer, because LLVM emits array bounds checks as i32.lt_u index, len — so the one idiom that would bound an index is the one shape scry declines to read.

This is exactly what FEAT-057's own 2026-07-14 reframing predicted: polyhedra's value is general-coefficient facts, which over wrapping i32 are unsound without value bounds, which come from guards.

Filed as FEAT-098 (#202), with the lever

For x <u c with a constant 0 ≤ c ≤ 2^31, the signed interval [0, c-1] is sound — unsigned x < c ≤ 2^31 puts x in [0, 2^31-1], representable as non-negative signed, so no wrap into the negative half.

Verified exhaustively at w=8 over every (c, x) pair before filing, in both directions: 0 counterexamples below the boundary, and 127 constants above it where the signed reading fails — so the side condition is load-bearing, not decorative.

FEAT-057 --depends-on--> FEAT-098 is now a typed link, and I confirmed the ordering gate actually reads it by mutating FEAT-098 into a later release and watching it fire.

What would change the number

Re-run the harness this PR extended:

cargo run --release -p scry-sai-core --example poly_surface -- <module.wasm>

FEAT-098's AC#4 requires reporting the delta on the two numbers above (0 ≥3-local, 0/94 no-wrap) after it lands — written so that "the number did not move" is a reportable outcome rather than a silent one, because the whole feature rests on this causal claim.

What this PR got right, for the record

The slice is sound and the oracles are real, not just green:

  • the discriminating test asserts the witness (100,100,0) satisfies every surfaced octagon constraint and every interval bound — so the "octagon cannot express it" half is tested, not assumed — and that the linear output excludes it;
  • the wrap-gate test pins both sides, on the reasoning that a gate which never admits an equality would be inert rather than sound;
  • I re-ran the wrap-gate mutant independently (if no_wrapif true, 3 sites, applied-count asserted): red on exactly the right test, control back to 160 passing.

Refs: FEAT-057 · FEAT-098

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant