3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-03-23 12:59:12 +00:00
z3/research/docs/nseq-issues/01-soundness-replace-empty-string.md
Copilot f518faac9b
Add nseq issue drafts from Ostrich benchmark analysis (discussion #9071) (#9073)
Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/04321ea7-2a53-4ed5-9f43-816dc6f7476b

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-20 19:07:51 -07:00

78 lines
2.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# [nseq] Soundness bug: str.replace with empty-string input returns sat instead of unsat
**Labels**: bug, c3, nseq, soundness
## Summary
The nseq solver returns `sat` on benchmarks where the correct answer is `unsat` when
`str.replace` is applied to an empty input string and the result is constrained to an
impossible value. The seq solver correctly returns `unsat` for all of these cases.
## Affected benchmarks (Ostrich suite, c3 branch)
| File | seq verdict | nseq verdict |
|------|-------------|--------------|
| `replace-special.smt2` | unsat | **sat** (WRONG) |
| `replace-special-4.smt2` | unsat | **sat** (WRONG) |
| `replace-special-5.smt2` | unsat | **sat** (WRONG) |
| `simple-replace-4b.smt2` | unsat | **sat** (WRONG) |
Data from: https://github.com/Z3Prover/z3/discussions/9071
## Reproducing examples
```smt2
; replace-special.smt2 — EXPECTED: unsat, nseq returns: sat
(set-logic QF_SLIA)
(declare-fun x () String)
(assert (not (= (= "B" (str.replace "" x "A")) false)))
(check-sat)
```
```smt2
; replace-special-4.smt2 — EXPECTED: unsat, nseq returns: sat
(set-logic QF_SLIA)
(declare-fun x () String)
(declare-fun y () String)
(assert (not (= (str.replace "" (str.replace "" x "A") "B")
(str.substr "B" 0 (str.len x)))))
(check-sat)
```
```smt2
; simple-replace-4b.smt2 — EXPECTED: unsat, nseq returns: sat
(set-logic QF_S)
(declare-fun x_7 () String)
(declare-fun x_11 () String)
(declare-fun x_12 () String)
(assert (= x_7 (str.replace (str.++ x_11 x_12) "e" "a")))
(assert (= x_7 "hello"))
(check-sat)
```
## Analysis
`replace_axiom` in `src/ast/rewriter/seq_axioms.cpp` generates clauses for
`r = str.replace u s t`:
- `~(u="") (s="") (r=u)` — empty input without empty pattern gives `r = u`
- `~(s="") (r = t ++ u)` — empty pattern gives `r = t ++ u`
- `contains(u,s) r = u` — no occurrence gives `r = u`
For `replace-special.smt2` with `u=""`:
- If `s=""`: `r = "A" ++ "" = "A"`, but `r = "B"` → contradiction → unsat
- If `s≠""`: `r = u = ""`, but `r = "B"` → contradiction → unsat
The clauses are individually correct, but the nseq Nielsen graph apparently fails to
derive the contradiction when the result `r` is constrained via equality to an impossible
constant. The likely root cause is incomplete propagation of the result equality through
the egraph into the replace axiom conclusions.
For `simple-replace-4b.smt2`, the issue is that `str.replace (x_11 ++ x_12) "e" "a" = "hello"`
is unsatisfiable because "hello" contains 'l', which cannot be produced by replacing 'e'→'a' in
any string, yet nseq claims sat.
## Files to investigate
- `src/ast/rewriter/seq_axioms.cpp``replace_axiom`
- `src/smt/theory_nseq.cpp``dequeue_axiom`, `populate_nielsen_graph`