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

2.7 KiB
Raw Blame History

[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

; 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)
; 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)
; 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.cppreplace_axiom
  • src/smt/theory_nseq.cppdequeue_axiom, populate_nielsen_graph