Fix soundness holes in the bounds/divisibility dataflow analyses#265
Draft
AntonOresten wants to merge 1 commit into
Draft
Fix soundness holes in the bounds/divisibility dataflow analyses#265AntonOresten wants to merge 1 commit into
AntonOresten wants to merge 1 commit into
Conversation
Batch of independent fixes to the forward dataflow framework and its two integer analyses: - ForOp IV upper bound: `upper - step` is only the last IV when the trip length divides the step; slt/sle-promoted while loops with step > 1 overshoot it (0:4:<10 visits 8, not 6). Use the always-sound exclusive bound `upper - 1`, keeping the sharp bound when lower/ upper/step are exact and divisibility holds. Unknown-step loops with a known upper bound now get a bound at all. - Never-visited statements no longer linger at ⊥: bottom is the merge identity, so a ⊥ operand at a join (loop carry, IfOp yield) let the other side's facts survive unrefuted — e.g. a PiNode-fed loop carry kept its init value's divisibility. PiNodes pass their operand through; anything else unmodeled records ⊤, as do unresolvable calls, unmodeled IfOp shapes, and the loop ops' own SSA results. The default `operand_value` now treats unrecognised literals as ⊤ too. - ForOp back-edge merges use `reachable_terminators`, matching the LoopOp handler and DCE: a ContinueOp behind a nested IfOp branch is a back-edge like any other. - `Intrinsics.assume` predicates from user-written kernels arrive as a QuoteNode (or const-inferred SSAValue), not the raw embedded value a pass constructs; resolve through the new `constant_operand` before the predicate-kind tests so hand-written assumes refine. - `exti` consults its signedness operand: zero-extension is only value-preserving for provably non-negative ranges (bounds) and reduces divisors to gcd(d, 2^srcbits) (divisibility). - Arithmetic interval transfers clamp to the result element width: the lattice computes over ℤ but the hardware op wraps, so ranges that can escape [typemin(T), typemax(T)] degrade to ⊤ instead of feeding false `Bounded` predicates or nsw/nuw flags. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batch of independent soundness fixes to the forward dataflow framework (
analysis/dataflow.jl) and its two integer analyses (analysis/bounds.jl,analysis/divisibility.jl).Fixes
ForOp IV upper bound wrong for
step > 1(P0).compute_iv_rangeusedupper.hi - step.loas the last IV, but slt/sle-promoted while loops don't guarantee the trip length divides the step:i = 0; while i < 10; i += 4visits 0, 4, 8 — the analysis claimedi ≤ 6. Now uses the always-sound exclusive boundupper.hi - 1, keeping the sharpupper - steponly when lower/upper/step are exact and the divisibility holds. Loops with an unknown step but known upper bound now get a bound at all (previously none).Never-visited statements lingered at ⊥ and acted as best-info at joins (P0). ⊥ is the merge identity, so a PiNode-fed loop carry (walk! skipped non-
Exprstatements) kept its init value's fact — e.g. claiming div-by-16 across iterations that replace the value. PiNodes now pass their operand's lattice element through; other unmodeled statements, unresolvable calls, unmodeled IfOp shapes, and the loop ops' own SSA results record ⊤. The framework-defaultoperand_valuelikewise now returns ⊤ (not ⊥) for unrecognised literals.User-written
Intrinsics.assumepredicates were silently ignored. Pass-constructed assumes embed theAssumePredicatevalue raw in the operand list, but user-written kernels arrive with it as aQuoteNode(or const-inferred SSAValue), failing thepred isa DivBy/Boundedtests. Newconstant_operandhelper (QuoteNode unwrap +singleton_typechase, mirroring codegen'sget_constant) is used by both analyses.extiignored its signedness operand; arithmetic ignored wraparound. Zero-extension is only value-preserving for provably non-negative ranges (bounds) and reduces divisors togcd(d, 2^srcbits)(divisibility) — the comment's claim that zext "doesn't change the mathematical value" is false for negative inputs. Arithmetic interval transfers now clamp to the result element width: the lattice computes over ℤ but the hardware op wraps, so ranges that can escape[typemin(T), typemax(T)]degrade to ⊤ instead of feeding falseBoundedpredicates ornsw/nuwflags.ForOp back-edge merges only looked at the direct body terminator. A
ContinueOpbehind a nested IfOp branch is a back-edge like any other; both ForOp handlers now usereachable_terminators, matching the LoopOp handler, DCE, and IR validation.Tests
Direct-API tests for each fix in
test/analysis/dataflow.jl(PiNode carry, nested-continue merge, QuoteNodeDivBy, divisibility zext) andtest/codegen/bounds.jl(step-4 IV bound, exti signedness, width clamp, QuoteNodeBounded).Verified: 68/68 in the directly affected files (
analysis/dataflow,codegen/{bounds,assume,no_wrap}), 297/297 regression sweep overcodegen/{views,slice,cse,integration,operations}— no output changes from the clamping or theoperand_valuedefault flip.Follow-up (not in this PR)
bitcastpass-through has the same signed↔unsigned reinterpretation issue asextifor same-width casts (e.g.Int32(-1)→UInt32changes the mathematical value); left untouched to keep this batch scoped.🤖 Generated with Claude Code