fix: No unguarded reverse casts in the arguments type encoding - #1169
Open
erniecohen wants to merge 1 commit into
Open
erniecohen wants to merge 1 commit into
erniecohen wants to merge 1 commit into
Conversation
Fixes boogie-org#1168. Under /typeEncoding:a every value of a type that is not built in is erased to one sort U, and each built-in type B gets casts B_2_U and U_2_B with the left inverse U_2_B(B_2_U(x)) == x. Beside it the encoding emitted a reverse cast forall x: U :: {U_2_B(x)} B_2_U(U_2_B(x)) == x which says that every value of U is a B. For bool it says that U has at most two elements, while the left inverse for int makes U infinite, so the axioms had no model (boogie-org#1168). The predicates encoding guards the same axiom with type(x) == B; the arguments encoding has no type function to guard it with. The change is to TypeErasureArguments.cs. It changes /typeEncoding:a only, and it adds no axiom: 1. No reverse cast. TypeAxiomBuilderArguments.GenReverseCastAxiom returns true. The left inverses stay. 2. No retyping. TypeEraserArguments.Visit(VCExprQuantifier) no longer redoes a quantifier whose int (bool, ...) variable occurs in its triggers only under a cast, with that variable retyped to U (RedoQuantifier). The redone body is stated for every value of U, where the original speaks of the ints, and the two agree only under the reverse cast. So dropping the reverse cast alone is still unsound: from forall b: bool :: {Box(b)} Box(b) == Box(IdB(b)), the redone quantifier says that every value boxed at bool is a bool. The predicates encoding keeps RedoQuantifier, with a type premise on each retyped variable. 3. A cast normal form. OpTypeEraserArguments.AssembleOpExpression passes a value whose Boogie type is built in to a U parameter as B_2_U(U_2_B(t)) also when its translation t is already a U term: the result of a polymorphic function, or a map select, at B. In Boogie's semantics this is the identity. Without it the same value reaches a U position as t in one formula and as a cast in another, and only the reverse cast joined the two; Test/test21/Triggers1.bpl then loses assert f(m[x]) from axiom (forall x: int :: f(x)). Why the result is sound. Boogie's own axioms under the arguments encoding are now the numbering and the left inverses of the type constructors, the left inverses of the casts, and the map axioms. They hold in a model where U is a tagged union of the built-in values, the values of the other types and the map values, each cast injects into U and projects out of it, and a projection of a value of another type gives a default. The reverse casts were the only axioms of Boogie's own that no such model satisfies. With part 2 every quantifier is the plain erasure of the typed one, and part 3 replaces a term with one that denotes the same value in Boogie's semantics. Not changed: the arguments encoding still states a program's own axioms for every value of U, so a program axiom that bounds the size of a type, such as forall x, y: Unit :: x == y, still bounds U. That is the erasure of the program's axiom, not an axiom of Boogie's, and the predicates encoding guards it. The predicates and monomorphic encodings are unchanged. Tests: Test/test21/issue-1168.bpl is the issue's program, and issue-1168-redo.bpl shows that dropping the reverse casts alone is not enough. Each ends in "assert false" and runs the three encodings with model-based quantifier instantiation, bounded at 10 iterations. Both fail before this change, where /typeEncoding:a proves "assert false", and pass after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 tasks
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.
Fixes #1168.
The bug
Under
/typeEncoding:a, every value of a type that is not built in is erased to one sortU, and each built-in typeBgets castsB_2_UandU_2_Bwith the left inverseU_2_B(B_2_U(x)) == x. Beside it the encoding emitted a reverse castwhich says that every value of
Uis aB. Forboolit boundsUto two elements, while the left inverse forintmakesUinfinite, so Boogie's own axioms had no model. The new testTest/test21/issue-1168.bplis the issue's program ending inassert false: with model-based quantifier instantiation,/typeEncoding:aproves it onmaster.The change
One file,
Source/VCExpr/TypeErasure/TypeErasureArguments.cs. It changes/typeEncoding:aonly, and it adds no axiom.TypeAxiomBuilderArguments.GenReverseCastAxiomreturnstrue. The left inverses stay.TypeEraserArguments.Visit(VCExprQuantifier)no longer callsRedoQuantifier, which redid a quantifier whoseint(bool, ...) variable occurs in its triggers only under a cast, with that variable retyped toU. The redone body is stated for every value ofUwhere the original speaks of the ints, and the two agree only under the reverse cast. So part 1 alone is still unsound:issue-1168-redo.bplhasforall b: bool :: {Box(b)} Box(b) == Box(IdB(b)), whose redone form says that every value boxed atboolis abool, and with part 1 alone it still provesassert false. The predicates encoding keepsRedoQuantifier, with itstype(x) == Bpremise.OpTypeEraserArguments.AssembleOpExpressionpasses a value whose Boogie type is built in to aUparameter asB_2_U(U_2_B(t))also when its translationtis already aUterm: the result of a polymorphic function, or a map select, atB. In Boogie's semantics this is the identity. Without it the same value reaches aUposition astin one formula and as a cast in another, and only the reverse cast joined the two:test21/Triggers1.bplthen losesassert f(m[x])fromaxiom (forall x: int :: f(x)).The predicates and monomorphic encodings are unchanged, and so is every query that does not use
/typeEncoding:a.Why the result is sound
Boogie's own axioms under the arguments encoding are then the numbering and the left inverses of the type constructors, the left inverses of the casts, and the map axioms. They hold in a model where
Uis a tagged union of the built-in values, the values of the other types and the map values (finite-support functions), each cast injects intoUand projects out of it, and a projection of a value of another type gives a default. The reverse casts were the only axioms of Boogie's own that no such model satisfies. With part 2 every quantifier is the plain erasure of the typed one, and part 3 replaces a term with one that denotes the same value.This was also checked by machine on the queries of four small programs (the issue's,
issue-1168-redo.bpl's, and two built from the shapes of Dafny's prelude): with the model written as SMT-LIB datatypes and definitions, Z3 shows that every axiom and term the patched Boogie emits holds in it. Before the change, exactly the reverse casts and the retyped quantifiers fail that check.What it does not fix
The arguments encoding still states a program's own axioms for every value of
U. So a program axiom that bounds the size of a type still boundsU: withfunction F<T>(x: T): boolandaxiom (forall x, y: Unit :: {F(x), F(y)} x == y), two termsF(UnitType, int_2_U(k))refute it. That is the erasure of the program's axiom, not an axiom of Boogie's, and the predicates encoding guards it.The two fixes sketched in the issue
forall<T> x: T :: Lit(x) == xis erased toforall t: T, x: U :: Lit(t, x) == x, which makesLit(boolType, int_2_U(k))equal toint_2_U(k), not abool. The same holds forUnbox(Box(x)) == x. Dafny's prelude has both, and three such terms refute (a) in Dafny's own queries.Lit(boolType, int_2_U(0))refutes it. If the result's type is guarded by the arguments' types, every value ofUneeds a type, and that is the predicates encoding.Two sound variants of part 2 that keep the retyping were measured as well: the redone body let-bound at
U_2_B(x), or guarded by a predicateU_is_B(x)that holds of every cast. Each made one Dafny query run out of resources (at 16M) that needs under 1M before the change. Not retyping was the simplest of the three and the cheapest.Testing
Test/test21/issue-1168.bplandissue-1168-redo.bpl, in the style ofissue-735.bpl: each ends inassert false, and runs/typeEncoding:a,:pand:mwithsmt.mbqibounded at 10 iterations. Onmaster,/typeEncoding:aprovesassert falsein both, and the second also with part 1 alone (measured on v3.5.5, whoseTypeErasureArguments.csismaster's). With the change, no encoding proves it. The same holds with Z3 4.11.2 and 5.1.0 onmaster, and with 4.11.2, 4.12.1, 4.16.0 and 5.1.0 on v3.5.5, with and without batch mode.test.ymlandtest-lean-auto.yml, unchanged) passes on this branch in the fork: Boogie CI and LeanAuto CI./typeEncoding:a(40 of them, most intest21) gives the same output onmasterwith and without the change, with Z3 4.11.2.Effect on completeness
Dafny selects
/typeEncoding:a. A Dafny 4.11.0 built against this change (on v3.5.5, whereTypeErasureArguments.csis identical tomaster's) was compared with the same Dafny on v3.5.5. That Dafny also carries fixes for its own prelude's soundness issues (dafny-lang/dafny#6531 to #6537), in both builds.dafny0todafny4andgit-issues, at a resource limit of 16,000,000: 10 programs change, none by a new error. In 5 a proof now finishes within the limit (6 proofs in all), and in 5 one proof no longer does.This pull request was prepared by an AI, Claude (Anthropic's model, in Claude Code), for @erniecohen, who filed #1168 and asked for the fix to be offered here. The AI wrote the change and the tests and ran the checks and measurements described above. The same change, on v3.5.5, is the branch
review/3.5.5of the fork, whose CI runs this repository's test jobs and packs it.🤖 Generated with Claude Code