Repository navigation
Consistent bit-vector encoding of comparisons, string lengths and char codes - #81
Closed
nils-loose wants to merge 3 commits into
Closed
nils-loose wants to merge 3 commits into
nils-loose wants to merge 3 commits into
Conversation
…it-vectors DCMP/FCMP/LCMP and boolean-to-int built their -1/0/1 results as Integer ite terms wrapped in int_to_bv. Z3 does not return on queries that mix int_to_bv with FP comparisons (a two-branch DCMP query that is UNSAT in 0.0 s over bit-vector constants stays unknown after minutes). Build those results over bit-vectors directly, and route every Integer-to-bit-vector constructor (Int/Short/Byte/CharValue, int-to-FP casts) through BitvectorEncoding.toBitvector, which rebuilds numerals, ite, +, -, * and bv2int over bit-vectors and keeps int_to_bv only for terms it cannot rewrite (string lengths, str.to_code, division). The result equals int_to_bv in every model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r the Integers String lengths, indices and char codes are Integers in the string theory, while Java ints and chars are bit-vectors, so s.length() > 2 reaches Z3 as (bvsgt ((_ int_to_bv 32) (str.len s)) #x00000002). Z3 runs for minutes on small satisfiable queries of that kind. Before each check, rewrite every bit-vector comparison between int_to_bv of a bounded Integer term (str.len, str.indexof, str.to_code, ubv_to_int, numerals, + - * ite of those) and another such term into the Integer comparison of their values. Where the range does not fit the bit-vector, the value keeps the wrap as an ite (modulo only for char codes), so the rewrite is exact. The ranges rely on strings having at most 2^31 - 1 chars, as Java strings do; that bound is added to the query for each string it relied on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FCMPG/FCMPL returned a concrete result when an operand was NaN, and a symbolic result that ignored NaN otherwise (-1 for both instructions); the concrete result used Float.compareTo, which orders -0.0 below 0.0. Encode them like DCMP: nanResult if either operand is NaN, else the FP three-way comparison. Double/Float.compare and Double/Float.compareTo now share BitvectorEncoding.javaCompare, which follows the JDK: NaN equals itself and is greater than everything, -0.0 is less than 0.0. The compareTo models had neither. With bit-vector comparison results the solver picks NaN and signed zeros more often, which made these models diverge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose
added a commit
that referenced
this pull request
Oct 19, 2026
FloatValue.java: one FCMP, #81's bit-vector ite version; it has the same NaN, signed-zero and ordering cases as #74's (FloatValueTest and BitvectorEncodingTest pass). SolverHandler.py: #81's int_to_bv lifting of the query, plus #74's extra_exprs. DoubleInvocation/LongValue/ShortValue.java: imports of both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
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.
What changes
Z3 decides queries that mix Integer and bit-vector sorts very badly. A two-branch DCMP query that is UNSAT in 0.0 s over bit-vector constants stays
unknownafter minutes when the -1/0/1 result isint_to_bv(ite(..., 1, -1)). This PR makes the encoding consistent and keeps the semantics exact.ites.Int/Short/Byte/CharValue(…, IntegerFormula), int-to-FP casts) goes through the newBitvectorEncoding.toBitvector. It rebuilds numerals,ite,+ - *andbv2intover bit-vectors, and keepsint_to_bvonly where it cannot rewrite (string lengths,str.to_code, division).int_to_bvin every model, becauseint_to_bvis a ring homomorphism mod 2^w.String*classes without editing them.solver/Int2BvLifting.py).int_to_bvof a bounded Integer term and another such term becomes an Integer comparison.str.len,str.indexof,str.to_code,ubv_to_int, numerals, and+ - * iteof those.ite. Only char codes use a modulo. The rewrite is exact.FCMPG/FCMPLreturned a concrete result when an operand was NaN, and a symbolic result that ignored NaN. Their concrete result also usedFloat.compareTo, so -0.0 vs 0.0 gave -1 where the instructions give 0. They are now encoded like DCMP.Double/Float.compareandDouble/Float.compareToshareBitvectorEncoding.javaCompare, which follows the JDK: NaN equals itself and is greater than everything, and -0.0 < 0.0. ThecompareTomodels had neither, which caused 2 regressions (divergences) before this fix.Per-query solver timeout: I recommend leaving it off by default (no change in this PR).
--solver-rlimit(Static pre-analysis pruning, with soundness fixes #65) only reorders the search: the last pending branch always runs unlimited, and nothing is given up on beforeMAX_RLIMIT.--solver-rlimit 25000000changed no verdict (0 points) and saved about 10% CPU.test_solver_rlimit).Measurement
--sv-comp, 300 s CPU, 15 GB, no SA, compared withbaseline-results.jsonl.int2bv-mixed-sort-encoding,string-char-int2bv-encoding-slow,string-length-int2bv-unbounded-query,solver-hard-query,no-solver-query-timeout,uf-global-constraints-unsliced,solver-query-growth-deep-unrolling);Notes on the flips:
ProjectionGridRoundTripper_exceptionless(no-runtime-exception) became SAFE at 265 s, but timed out when re-run, so it is not counted.AbstractChart_exceptionless(no-runtime-exception) is confirmed on re-run. FCMP on NaN is now symbolic, so the search explores the NaN sides (25 rounds instead of 9), and the run times out on hard FP queries. Before, those sides were never explored. The old SAFE relied on that imprecision.String.lastIndexOf(String)(valid-assert) is model luck. The new model contains a surrogate pair, which does not survive the input round trip (divergence → UNKNOWN). That is an input-encoding issue, not this change.float_unboundedloopruns drop from about 100 s of solver time to about 0 s, but still time out on path explosion.Tests
BitvectorEncodingTest.int_to_bvunder sampled assignments (a universal proof is exactly the kind of query Z3 hangs on).javaCompareis checked againstDouble.compare/Float.compareon a grid that includes NaN, ±0 and ±∞.../gradlew test: 8761 tests, 0 failures.tests/test_int2bv_lifting.py.length()+3, the signedbv2intpattern, and leaving pure bit-vector terms alone.test_solver_rlimit,test_sa_walkandtest_divergencepass.Known limits
str.replacechains (Basic6:unknown, "max unfolding"). They now end UNKNOWN quickly instead of timing out.indexOf(int)encodes the char as a decimal string (indexof-char-arg-encoded-as-decimal).uf-global-constraints-unsliced);solver-query-growth-deep-unrolling);solver-hard-query), which stay hard.StringValue/StringBuilderValue/StringInvocationare unchanged. TheirIntValue(…, IntegerFormula)calls benefit through the shared constructor.fix/executor-small-bugsmay also touch FCMP NaN handling.🤖 Generated with Claude Code