gargamelle-fol-0.1.0.0: src/Gargamelle/FOL/Z3.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
module Gargamelle.FOL.Z3
( SMTResult(..)
, solveZ3Raw
, formulaToSMTLib
, checkFormulaZ3
, verifyValidityZ3
, parseZ3Model
) where
import Control.Exception (SomeException, try)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import System.Exit (ExitCode)
import System.Process (readProcessWithExitCode)
import GHC.TypeLits (symbolVal)
import FirstOrder (Formula(..), Term(..))
import Gargamelle.Model.Satisfaction (Evidence(..))
-- | Result of an SMT query.
data SMTResult
= Sat (Map.Map String String)
| Unsat
| Unknown String
deriving (Eq, Show)
-- | Collect all atomic predicate/constant names in a Formula to declare them in SMT-LIB2.
collectSymbols :: Formula syms -> Set.Set String
collectSymbols = \case
TrueF -> Set.empty
FalseF -> Set.empty
Pred p args -> Set.insert (symbolVal p) (Set.unions (map collectTermSymbols args))
Not f -> collectSymbols f
And f1 f2 -> collectSymbols f1 `Set.union` collectSymbols f2
Or f1 f2 -> collectSymbols f1 `Set.union` collectSymbols f2
Impl f1 f2 -> collectSymbols f1 `Set.union` collectSymbols f2
Forall _ f -> collectSymbols f
Exists _ f -> collectSymbols f
collectTermSymbols :: Term -> Set.Set String
collectTermSymbols = \case
Var v -> Set.singleton v
Const c -> Set.singleton c
Func f ts -> Set.insert f (Set.unions (map collectTermSymbols ts))
-- | Translate a Term to SMT-LIB2.
termToSMT :: Term -> String
termToSMT = \case
Var v -> v
Const c -> c
Func f [] -> f
Func f ts -> "(" ++ f ++ " " ++ unwords (map termToSMT ts) ++ ")"
-- | Translate a Formula to an SMT-LIB2 boolean expression.
exprToSMT :: Formula syms -> String
exprToSMT = \case
TrueF -> "true"
FalseF -> "false"
Pred p [] -> symbolVal p
Pred p args -> "(" ++ symbolVal p ++ " " ++ unwords (map termToSMT args) ++ ")"
Not f -> "(not " ++ exprToSMT f ++ ")"
And f1 f2 -> "(and " ++ exprToSMT f1 ++ " " ++ exprToSMT f2 ++ ")"
Or f1 f2 -> "(or " ++ exprToSMT f1 ++ " " ++ exprToSMT f2 ++ ")"
Impl f1 f2 -> "(=> " ++ exprToSMT f1 ++ " " ++ exprToSMT f2 ++ ")"
Forall p f -> "(forall ((" ++ symbolVal p ++ " Bool)) " ++ exprToSMT f ++ ")"
Exists p f -> "(exists ((" ++ symbolVal p ++ " Bool)) " ++ exprToSMT f ++ ")"
-- | Format a formula into a complete self-contained SMT-LIB2 query script.
formulaToSMTLib :: Formula syms -> String
formulaToSMTLib f =
let syms = collectSymbols f
decls = unlines [ "(declare-const " ++ s ++ " Bool)" | s <- Set.toList syms ]
assertion = "(assert " ++ exprToSMT f ++ ")\n"
in "(set-logic ALL)\n" ++ decls ++ assertion ++ "(check-sat)\n(get-model)\n"
-- | Parse the output model from Z3 into a map of variable assignments.
parseZ3Model :: String -> Map.Map String String
parseZ3Model out =
let extract ("(define-fun" : name : "()" : _type : val : rest) =
let cleanVal = filter (`notElem` "()") val
in (name, cleanVal) : extract rest
extract (_ : rest) = extract rest
extract [] = []
tokens = words out
in Map.fromList (extract tokens)
{- Note [Streaming Z3 SMT-LIB2 Piping and Model Extraction]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[tag:note-z3-smt-pipe-extraction]
In formal verification engines and multi-agent synthesis loops, interacting with an
automated theorem prover like Z3 can be implemented either via Foreign Function Interface
(FFI C-bindings) or via streaming standard SMT-LIB2 over standard POSIX process pipes.
Gargamelle intentionally chooses the streaming SMT-LIB2 pipe architecture for four reasons:
1. ABI & Portability Invariance: FFI bindings to C++ libraries like libz3 are notoriously
fragile across disparate GHC versions (9.6 through 9.14) and Linux distribution package
managers. Streaming pipes eliminate all FFI runtime crashes and garbage collector pauses.
2. Sandbox Isolation: In untrusted synthesis environments, executing solvers within Bubblewrap
containers or remote execution sandboxes requires process separation. A streaming text pipe
naturally crosses process, namespace, and network boundaries.
3. Inspectability & Reproducibility: Every SMT query generated by 'formulaToSMTLib' is an
immutable, human-readable SMT-LIB2 script that can be logged directly into Beam SQLite
records and inspected offline.
4. Sound Dual Refutation Verification:
To prove validity of a specification formula φ, 'verifyValidityZ3' submits the negation
¬φ to Z3:
- UNSAT => ¬φ has no models, hence φ is universally valid; yields 'VerifiedSection'.
- SAT => Z3 synthesizes a concrete satisfying assignment for ¬φ; 'parseZ3Model' parses
these assignments into a 'Counterexample' witness containing the exact falsifying
variable bindings.
- UNKNOWN => Solvers timing out or exceeding resource bounds soundly fail-safe by
emitting a non-validity witness.
-}
-- | Execute a raw SMT-LIB2 query script by piping directly into host Z3.
--
-- See Note [Streaming Z3 SMT-LIB2 Piping and Model Extraction] [ref:note-z3-smt-pipe-extraction].
solveZ3Raw :: String -> IO SMTResult
solveZ3Raw script = do
res <- (try (readProcessWithExitCode "z3" ["-in", "-smt2"] script) :: IO (Either SomeException (ExitCode, String, String)))
case res of
Left err ->
pure $ Unknown ("Z3 process invocation failed (is z3 installed?): " ++ show err)
Right (_code, stdoutStr, stderrStr) ->
let cleanOut = filter (not . null) (lines stdoutStr)
in case cleanOut of
("sat" : _) -> pure $ Sat (parseZ3Model stdoutStr)
("unsat" : _) -> pure Unsat
("unknown" : _) -> pure $ Unknown "Z3 returned unknown"
_ -> pure $ Unknown ("Z3 error: " ++ stderrStr ++ " | out: " ++ stdoutStr)
-- | Check satisfiability of a First-Order formula using host Z3.
--
-- See Note [Streaming Z3 SMT-LIB2 Piping and Model Extraction] [ref:note-z3-smt-pipe-extraction].
checkFormulaZ3 :: Formula syms -> IO SMTResult
checkFormulaZ3 f = solveZ3Raw (formulaToSMTLib f)
-- | Formally verify validity of a formula:
-- Checks whether (not f) is unsatisfiable. If unsat, returns VerifiedSection.
-- If sat, the countermodel is returned as Evidence.
--
-- See Note [Streaming Z3 SMT-LIB2 Piping and Model Extraction] [ref:note-z3-smt-pipe-extraction].
verifyValidityZ3 :: Formula syms -> IO Evidence
verifyValidityZ3 f = do
let negated = Not f
checkFormulaZ3 negated >>= \case
Unsat -> pure VerifiedSection
Sat model -> pure $ Counterexample "Z3-Countermodel" (show model)
Unknown err -> pure $ Counterexample "Z3-Unknown" err