Repository navigation
Model StringBuffer, ignore-case comparisons, simple regexes and more String methods - #90
Closed
nils-loose wants to merge 4 commits into
Closed
nils-loose wants to merge 4 commits into
nils-loose wants to merge 4 commits into
Conversation
… regexes exactly - StringBuffer is shadowed by StringBuilderValue (same methods), also when recovered from the heap. - toLowerCase/toUpperCase (default locale, or a known Locale other than tr/az/lt): char by char from tables of the running JVM, surrogate pairs as code points, by the pinned length. Lower case of a string with 'Σ' (final sigma) stays unmodelled, behind a branch. - equalsIgnoreCase, regionMatches(ignoreCase, ...), compareToIgnoreCase: against a concrete string without surrogates as a regex of the case classes; otherwise Java's comparison loop unrolled over the pinned lengths, surrogate pairs included. Replaces the equalsIgnoreCase UF, which assumed ASCII input (an input restriction) and was cut at 50 chars. Boolean.parseBoolean uses the regex. - matches(regex) for simple regexes (literals, positive classes, groups, alternation, greedy/reluctant quantifiers) as SMT regexes; replaceFirst with a literal regex exactly; replaceAll with a literal regex as replace (precision loss). - codePointAt/codePointBefore/codePointCount, repeat, String.join(array), new String(char[], int, int), String.valueOf(StringBuilder/null), StringBuilder.reverse of symbolic chars, with their index checks and pins. - Character.digit with a symbolic radix and Character.forDigit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unrolled over a string, the full Unicode case tables (a few hundred ranges per char) give queries Z3 does not decide, even simple ones time out. The models now separate the ASCII strings by a branch and leave the others unmodelled. The ignore-case comparison against a concrete string stays a regex of the case classes, for all chars. Splits (a model's cases, which both return) are told apart from index checks, so a call that took a split's other side still records its branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore the ASCII split Unrolled over a pinned length, the search reached hardly any other length (the securibench violations were then missed), and unrolled up to a bound Z3 took seconds per query. A concrete string is mapped concretely. The ignore-case comparisons pin the length before they split off the ASCII strings: with the length fixed, Z3 decides the other side of the split at once, where the int2bv of a free string length made it hang. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model is exact for the pinned length; the test now takes the call's branches and asks for other strings of that length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose
added a commit
that referenced
this pull request
Oct 19, 2026
Combining #83 and #90: #90's library-call branches say whether the call throws when one fails (a check) or both sides return (a split). The NumberFormatException and radix-range branches of #83, and the radix pin, are checks; whether a parsed float is in the exact fragment is a split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose
added a commit
that referenced
this pull request
Oct 19, 2026
…tates to every branch Combining #72, #82 and #84: - #84 pruned a subtree below a repeated loop-head state by returning from the recursive dfs. In #72's iterative walk that return ended the whole walk, so the open branches of every later subtree were never offered, and an exhausted search could report SAFE. It now skips that subtree only (regression test added). - The pruned-subtree count is reset for each walk, as #82 walks the persistent tree instead of a copy. - The choice branches (#69/#76) and library-call branches (#75/#90) take the pending loop-head states too, so a state always goes with the branch right after it, which its position assumes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
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.
Builds on #75. It models the String, StringBuffer and Character methods that #75 still left unmodelled. Each new model is exact. Where an exact model is impractical, the call stays unmodelled, so it records a context loss.
What changed
StringBuffer
StringBuilderValue, with the same models. This coversnew, values recovered from the heap, andStringBufferarguments ofString/StringBuildermethods.Ignore-case comparisons (
CaseMapping)equalsIgnoreCaseagainst a concrete string without surrogates is a regular expression: at each position, the chars with the same keytoLowerCase(toUpperCase(c))as the concrete char. The key is tabulated from the running JVM. The model is exact for all chars, andBoolean.parseBooleanuses it too.equalsIgnoreCase,regionMatches(true/symbolic, ...)andcompareToIgnoreCase:equalsIgnoreCaseUF is removed. It added "every char is printable ASCII" as a global constraint (an input restriction, unsound for SAFE), was cut at 50 chars, and its table was only added to the first trace.Regular expressions (
JavaRegex)matches(regex): a concrete regex of the simple kind becomes an SMT regex. The simple kind is literals, escaped metacharacters, positive classes ([a-z],\d,\s,\w), groups,|, and the greedy and reluctant quantifiers. These atoms never match a surrogate, so the char-level SMT semantics equal Java's code-point semantics.., negated classes, anchors, back references, possessive quantifiers, flags, ...), and such a call stays unmodelled.replaceFirstwith a literal regex and replacement isstr.replace, which is exact.replaceAllwith a literal regex is Make the String and StringBuilder models sound #75'sreplace, which records precision loss.Other models
codePointAt,codePointBeforeandcodePointCount, with their index checks (codePointCountpins the range length).repeat, with thecount >= 0check andcountpinned.String.join(delimiter, array), andString.valueOfof a StringBuilder or of null.new String(char[], offset, count), with its bounds check and pins.StringBuilder.reverseof symbolic chars, by the pinned length, with Java's restoration of surrogate pairs.Character.digitwith a symbolic radix, andCharacter.forDigit.StringCallBranchesBranch id's dont matchin the explorer).Measured
Config: nosa, sv-comp.cfg, 300 s, mem15g. 1168 runs:
The base is #75's final results, plus 12 KB runs that #75 had not measured; I ran those on #75's jar.
new String(char[], int, int))matches)Tests
StringCaseRegexModelExactnessTest(L0, about 1300 cases) is in Make the String and StringBuilder models sound #75's style: symbolic strings and ints whose concrete values are the sample, with the call's branches as they hold for the sample. With the variables fixed to the sample, only Java's result is admitted.Character.digit(c, r) == (digit(c, 36) < r ? digit(c, 36) : -1)for every char and radix.StringModelExactnessTestpasses. One case now uses an unmodelled insert instead ofreverse, which is modelled.Known limits
toLowerCase/toUpperCasestay unmodelled, as a context loss. I tried three exact encodings:seq.map/seq.foldl: hangs, and once gave an unsound model.StringCompare03(NRE) is still UNKNOWN.split,chars(),codePoints()andStringTokenizerare not modelled.str.len. I saw this on securibench Inter10 during development, with the ASCII split before the pin. This is why the pins come first.🤖 Generated with Claude Code