Skip to content

Commit e9a8eb8

Browse files
Escape SMT-LIB set-info strings per SMT-LIB 2.6+ (doubled quotes) (#1112)
`Verifier.encodeCore` was rendering `(set-info :final-message "...")` with C-style escaping (backslash + quote) for embedded double quotes and backslashes in the property summary. That is invalid in SMT-LIB 2.6+: the backslash has no special meaning inside a string literal (except `\u{XXXX}` for Unicode escapes), so for input Expected len(kwargs["JobName"]) >= 1, got stringLen(kwargs[JobName]) the encoder emitted (set-info :final-message "Expected len(kwargs[\"JobName\"]) >= 1, got stringLen(kwargs[JobName])") which an SMT-LIB parser reads as a string that ends after the first `\"` (a literal backslash followed by a string-closing quote), with the rest (`JobName\"...`) sitting outside any command and triggering a parse error. Per the spec (§3.1.2), a literal double quote inside an SMT-LIB string must be written as two consecutive double quotes. An `escapeSMTStringLit` helper that already does this (and also handles non-printable characters via `\u{XXXX}`) existed in `Strata/DDM/Util/String.lean` and is already used by the user-error path in `StrataMain.lean`. Wire it into the `Solver` API as a typed `setInfoString` wrapper and use it everywhere a string-valued `set-info` attribute is emitted: - `Verifier.lean`: the `:final-message` site (the site the reported failure hit) plus the `:sat-message` / `:unsat-message` sites in `addLocationInfo`. The `sat-message` / `unsat-message` callers previously pre-quoted their argument; they now pass the raw Lean string. - `SMTUtils.lean`: the `:file` site and the forwarded application- supplied `(name, value)` message. The latter's contract changes from "pre-quoted value" to "raw Lean string" accordingly. The plain `setInfo` remains available for attribute values that are already valid SMT-LIB tokens (integers, s-expressions), and is still used for `:start` / `:stop`. Regression tests in `SMTEncoderTests.lean` cover both the reported case (embedded double quotes escaping to `""`) and the complementary case (embedded backslash staying literal, no escape). Each fails on origin/main and passes here. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
1 parent ebb7a48 commit e9a8eb8

4 files changed

Lines changed: 110 additions & 8 deletions

File tree

Strata/DL/Imperative/SMTUtils.lean

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,17 +275,21 @@ def solverResult {P : PureExpr} [ToFormat P.Ident]
275275
| none =>
276276
return .error (mkError output)
277277

278+
/-- Emit a block of `set-info` attributes describing the source location and
279+
an optional application-specific `(name, rawValue)` pair. `message.snd` is
280+
a raw Lean string; it will be quoted and SMT-LIB-escaped (doubled `""`)
281+
before emission. -/
278282
def addLocationInfo {P : PureExpr} [BEq P.Ident]
279283
(md : Imperative.MetaData P) (message : String × String)
280284
: Strata.SMT.SolverM Unit := do
281285
match Imperative.getFileRange md with
282286
| .some fileRange => do
283-
Strata.SMT.Solver.setInfo "file" s!"\"{format fileRange.file}\""
287+
Strata.SMT.Solver.setInfoString "file" (toString (format fileRange.file))
284288
Strata.SMT.Solver.setInfo "start" s!"{fileRange.range.start}"
285289
Strata.SMT.Solver.setInfo "stop" s!"{fileRange.range.stop}"
286290
-- TODO: the following should probably be stored in metadata so it can be
287291
-- set in an application-specific way.
288-
Strata.SMT.Solver.setInfo message.fst message.snd
292+
Strata.SMT.Solver.setInfoString message.fst message.snd
289293
| .none => pure ()
290294

291295
/--

Strata/DL/SMT/Solver.lean

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public import Strata.DL.SMT.Term
1010
public import Strata.DL.SMT.TermType
1111
public import Strata.Languages.Core.Options
1212
import Strata.DDM.Format
13+
public import Strata.DDM.Util.String
1314
import Std.Data.HashMap
1415

1516
/-!
@@ -156,6 +157,15 @@ def setOption (name value : String) : SolverM Unit :=
156157
def setInfo (name value : String) : SolverM Unit :=
157158
emitln s!"(set-info :{name} {value})"
158159

160+
/-- Emit `(set-info :name "...")` with the given *raw* Lean string as the
161+
attribute value. The string is quoted and escaped per SMT-LIB 2.6+ rules
162+
(via `Strata.escapeSMTStringLit`): embedded double quotes are doubled
163+
(`""`) and non-printable characters use `\u{XXXX}` escapes. Callers must
164+
NOT pre-quote or pre-escape the argument — use `setInfo` for already-
165+
formatted attribute values (integers, s-expressions, etc.). -/
166+
def setInfoString (name value : String) : SolverM Unit :=
167+
emitln s!"(set-info :{name} {Strata.escapeSMTStringLit value})"
168+
159169
def comment (comment : String) : SolverM Unit :=
160170
let inline := comment.replace "\n" " "
161171
emitln s!"; {inline}"

Strata/Languages/Core/Verifier.lean

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,36 +103,39 @@ def encodeCore (ctx : Core.SMT.Context) (prelude : SolverM Unit)
103103
-- Satisfiability check: P ∧ Q satisfiable?
104104
Solver.comment "Satisfiability"
105105
Imperative.SMT.addLocationInfo (P := Core.Expression) (md := md)
106-
(message := ("sat-message", s!"\"Property can be satisfied\""))
106+
(message := ("sat-message", "Property can be satisfied"))
107107
let obligationStr ← Solver.termToSMTString obligationId
108108
let _ ← Solver.checkSatAssuming [obligationStr] ids
109109

110110
-- Validity check: P ∧ ¬Q satisfiable?
111111
Solver.comment "Validity"
112112
Imperative.SMT.addLocationInfo (P := Core.Expression) (md := md)
113-
(message := ("unsat-message", s!"\"Property is always true\""))
113+
(message := ("unsat-message", "Property is always true"))
114114
let negObligationStr := s!"(not {obligationStr})"
115115
let _ ← Solver.checkSatAssuming [negObligationStr] ids
116116
else
117117
if satisfiabilityCheck then
118118
-- P ∧ Q satisfiable?
119119
Solver.comment "Satisfiability"
120120
Imperative.SMT.addLocationInfo (P := Core.Expression) (md := md)
121-
(message := ("sat-message", s!"\"Property can be satisfied\""))
121+
(message := ("sat-message", "Property can be satisfied"))
122122
Solver.assert obligationId
123123
let _ ← Solver.checkSat ids
124124
else if validityCheck then
125125
-- P ∧ ¬Q satisfiable?
126126
Solver.comment "Validity"
127127
Imperative.SMT.addLocationInfo (P := Core.Expression) (md := md)
128-
(message := ("unsat-message", s!"\"Property is always true\""))
128+
(message := ("unsat-message", "Property is always true"))
129129
Solver.assert (← encodeTerm (Factory.not obligationTerm) |>.run estate).1
130130
let _ ← Solver.checkSat ids
131131

132132
-- Emit the property summary (or label) as the final message in the SMT-LIB output.
133+
-- Use `setInfoString` so the value is quoted and escaped per SMT-LIB 2.6+
134+
-- (doubled `""` for embedded quotes). C-style `\"` escaping would be rejected
135+
-- by SMT-LIB consumers: backslash is a literal character in string contexts,
136+
-- and the following `"` would close the string.
133137
let rawMsg := md.getPropertySummary.getD label
134-
let escaped := rawMsg.replace "\\" "\\\\" |>.replace "\"" "\\\""
135-
Solver.setInfo "final-message" s!"\"{escaped}\""
138+
Solver.setInfoString "final-message" rawMsg
136139

137140
return (ids, estate)
138141

StrataTest/Languages/Core/Tests/SMTEncoderTests.lean

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,91 @@ info: (set-logic ALL)
370370
else ""
371371
IO.print smt
372372

373+
/-! ## Regression: `:final-message` / `:sat-message` / `:unsat-message`
374+
must escape embedded double quotes by doubling them (`""`) per
375+
SMT-LIB 2.6+, not with C-style `\"` escaping.
376+
377+
Before the fix, a property summary containing `"` would render as
378+
`(set-info :final-message "... \"FOO\" ...")` which SMT-LIB parsers
379+
reject: the backslash is a literal character in string contexts, and
380+
the following `"` closes the string, leaving `FOO\"...` outside as
381+
unexpected tokens. See
382+
https://smtlib.cs.uiowa.edu/papers/smt-lib-reference-v2.6-r2021-05-12.pdf
383+
§3.1.2. -/
384+
385+
/-- Run `encodeCore` on a trivial `true` obligation with the given metadata
386+
and check flags, and return the resulting SMT-LIB text. -/
387+
private def captureEncodeCore (md : Imperative.MetaData Core.Expression)
388+
(satCheck validityCheck : Bool) (label : String := "test") : IO String := do
389+
let ctx : SMT.Context := SMT.Context.default
390+
let obligationTerm := Term.prim (.bool true)
391+
let b ← IO.mkRef { : IO.FS.Stream.Buffer }
392+
let solver ← Strata.SMT.Solver.bufferWriter b
393+
let _ ←
394+
Strata.SMT.SolverM.run solver
395+
(Strata.SMT.Encoder.encodeCore ctx (pure ()) [] obligationTerm md
396+
(satisfiabilityCheck := satCheck) (validityCheck := validityCheck) (label := label))
397+
let contents ← b.get
398+
return if h : contents.data.IsValidUTF8
399+
then String.fromUTF8 contents.data h
400+
else ""
401+
402+
/-- Metadata carrying only a property summary (no file range). -/
403+
private def summaryMd (summary : String) : Imperative.MetaData Core.Expression :=
404+
Imperative.MetaData.empty.withPropertySummary summary
405+
406+
/-- Metadata carrying only a file range (no property summary); used to
407+
exercise `addLocationInfo`. -/
408+
private def fileRangeMd (file : String) : Imperative.MetaData Core.Expression :=
409+
let fr : Strata.FileRange := ⟨.file file, Strata.SourceRange.none⟩
410+
Imperative.MetaData.empty.pushElem Imperative.MetaData.fileRange (.fileRange fr)
411+
412+
/-! Embedded double quotes in the property summary must be doubled (`""`). -/
413+
/--
414+
info: (set-logic ALL)
415+
; Validity
416+
(assert false)
417+
(check-sat)
418+
(set-info :final-message "Expected len(kwargs[""JobName""]) >= 1, got stringLen(kwargs[JobName])")
419+
-/
420+
#guard_msgs in
421+
#eval show IO _ from do
422+
let smt ← captureEncodeCore
423+
(summaryMd "Expected len(kwargs[\"JobName\"]) >= 1, got stringLen(kwargs[JobName])")
424+
false true
425+
IO.print smt
426+
427+
/-! A backslash in the property summary is a *literal* character in SMT-LIB
428+
2.6+ strings (no special meaning), so no escape is needed. -/
429+
/--
430+
info: (set-info :final-message "path/with\backslash")
431+
-/
432+
#guard_msgs in
433+
#eval show IO _ from do
434+
let smt ← captureEncodeCore (summaryMd "path/with\\backslash") false true
435+
for line in smt.splitOn "\n" do
436+
if line.startsWith "(set-info :final-message" then
437+
IO.println line
438+
439+
/-! In full-check mode (both satisfiability and validity), `addLocationInfo`
440+
emits `:sat-message` and `:unsat-message`. These values must not carry
441+
pre-wrapping literal quote characters — before the fix, the
442+
`bothChecks` branch passed `"\"Property can be satisfied\""` which, once
443+
`setInfoString` re-quoted it, rendered as `"""Property..."""` (a
444+
well-formed SMT-LIB string whose content has literal leading and
445+
trailing `"`). -/
446+
/--
447+
info: (set-info :sat-message "Property can be satisfied")
448+
(set-info :unsat-message "Property is always true")
449+
-/
450+
#guard_msgs in
451+
#eval show IO _ from do
452+
let smt ← captureEncodeCore (fileRangeMd "test.st") true true
453+
for line in smt.splitOn "\n" do
454+
if line.startsWith "(set-info :sat-message"
455+
∨ line.startsWith "(set-info :unsat-message" then
456+
IO.println line
457+
373458
/-! ## SMT encoding of str.prefixof / str.suffixof -/
374459

375460
/--

0 commit comments

Comments
 (0)