Skip to main content
The Logic Engine uses Z3, a Satisfiability Modulo Theories (SMT) solver from Microsoft Research, to verify logical constraints.
The legacy VerificationEngine.verify_logic_rule() method has been removed as of v5.1.0. It now raises NotImplementedError. Use LogicVerifier from qwed_new.core.logic_verifier or the SDK’s client.verify_logic() method instead. See the changelog for migration details.

Capabilities


Quick start


QWED-Logic DSL

QWED uses a secure S-expression DSL for logic expressions.

Operators

The parser accepts only the operator names in this table. Aliases such as NE, GE, LE, and MUL are not on the whitelist, so the engine returns BLOCKED with Unknown operator. Use NEQ, GTE, LTE, and MULT.

Type rules

Every variable has a sort: Int, Real, or Bool. An undeclared variable is an Int. Declare other sorts in the variables argument of DSLLogicVerifier.verify_from_dsl(), for example variables=[{"name": "flag", "type": "Bool"}]. On POST /verify/logic the query is natural language, and the translator declares the variables it emits. A (VAR flag Bool) form inside a larger expression does not compile, even though the compiler’s error message suggests it. The compiler rejects mixed Boolean and numeric operands instead of coercing True to 1. A rejected program returns ERROR, never SAT or VERIFIED. QWED does not infer the sort of an undeclared name from a Boolean literal, because that would hide a translation that forgot the declaration.

Examples


Satisfiability checking

Basic check

Finding all solutions

The SDK client’s verify_logic() returns a single verdict with one model. For all-solutions enumeration, iterate with a low-level solver (e.g. z3) or use the LogicVerifier low-level API and extend constraints yourself:

Business rules

These rules compare flags such as has_id with True, so each flag must be declared Bool, or the compiler returns ERROR (see Type rules). The examples here and under Quantifiers run the DSL directly with DSLLogicVerifier, which takes the declarations in variables. verdict() returns SAT or UNSAT and fails closed on anything else:

Age verification

Discount eligibility

Approval workflow


Quantifiers

FORALL and EXISTS take exactly two arguments: a binder and a body.
  • Binder. The first argument is a single plain variable name. The parser returns BLOCKED for any other binder, such as a number, an operator name, True/False, a list like (x), or an expression.
  • Sort. The bound variable takes its declared sort. If it isn’t declared, it is an Int.
  • Scope. The binding applies inside the body only. A free variable with the same name outside the quantifier is a separate variable.
  • Body. The body must be Boolean, such as a comparison or a logical expression. A numeric body returns ERROR.
  • Solver limits. Quantified formulas can be hard for Z3. If Z3 returns unknown, for example on a timeout, verify_from_dsl() returns UNKNOWN and POST /verify/logic returns BLOCKED. Neither is verified.
To quantify over a collection, quantify over a numeric variable and state the property with arithmetic and comparisons. The DSL has no uninterpreted functions, so a form like (quantity item) returns BLOCKED with Unknown operator.

Universal (FORALL)

Existential (EXISTS)

Bound and free variables


Security: operator whitelist

The DSL uses a strict whitelist — only approved operators are allowed:

Fail-closed constraint parsing

The logic engine uses SafeEvaluator for all constraint parsing. If SafeEvaluator is unavailable, the engine raises a RuntimeError instead of falling back to raw evaluation. This fail-closed design ensures that untrusted input is never passed to Python’s eval().

Provider tracking

When you verify a natural-language logic query, QWED translates it to DSL using the configured LLM provider. The response includes a provider_used field so you can see which provider handled the translation — even when the request falls back to a different provider or ends in an error.
This field also appears in error responses, which helps you debug provider-related issues without inspecting server logs.

Python-style constraints

QWED also converts Python-style constraints from a structured logic task into DSL. It maps each operator to its whitelisted DSL name: Constraints such as x >= 5 or x * 2 <= 10 compile and return SAT or UNSAT. Earlier releases emitted GE, LE, NE, and MUL, so these constraints came back BLOCKED. You can join conditions with infix and/or/not in any case, or with the call forms And(...) and Or(...):
Unknown operators, unsupported Python syntax, and solver errors still fail closed.

Categorical string constants fail closed

The natural-language bridge rejects string literals such as tier == 'gold'. The solver has no string type, so QWED can’t soundly model categorical string constants. A constraint that contains one returns UNVERIFIABLE (or an error) instead of a verdict. Floating-point literals are rejected the same way. Encode categories as explicit integers instead:
Before v7.3.0, the bridge turned each string into a free integer variable, so contradictory categorical claims could come back satisfiable. See GHSA-g25v-44j8-gg7x.

Error handling


LogicVerifier returns DiagnosticResult (v6.0.0)

Introduced in v6.0.0. LogicVerifier is the second engine — after FactVerifier — to conform to the unified DiagnosticResult contract. The legacy LogicResult dataclass has been removed from the verifier’s return surface. The /verify/logic endpoint, and so client.verify_logic(), maps the DSL verdict instead: SAT is VERIFIED with the model as evidence, UNSAT is UNVERIFIABLE, and anything else (UNKNOWN, ERROR, BLOCKED) is BLOCKED. DSLLogicVerifier.verify_from_dsl() returns the raw SAT, UNSAT, UNKNOWN, ERROR or BLOCKED.
If you call the low-level LogicVerifier class directly, every one of its nine public methods now returns a DiagnosticResult:
  • verify_logic
  • verify_with_quantifiers
  • verify_bitvector
  • verify_array
  • prove_theorem
  • check_implication
  • check_equivalence
  • verify_optimization
  • check_vacuity

Reading a result

The three diagnostic layers — agent_message, developer_fields, and proof_ref — are populated on every result. proof_ref is computed from the Z3 solver’s assertion stack and is present only when status == "VERIFIED".

SAT/UNSAT status matrix

Z3’s sat/unsat outcome has different meanings depending on the method. The engine disambiguates by mapping each per-method outcome to a DiagnosticResult status and recording the raw Z3 verdict under developer_fields.deterministic_verdict.
Always branch on result.is_verified (or result.proof_ref is not None) for control flow. Read developer_fields["deterministic_verdict"] for diagnostic context — never for authorization.

symbol_table on every VERIFIED result

developer_fields.symbol_table is a sorted list of every declared variable and its type. It is included on every VERIFIED result so audit logs record exactly what was proven:

New BLOCKED constraint IDs

Two fail-closed behaviors ship with the migration: Other constraint_ids used by the engine: logic_verifier.invalid_constraint, logic_verifier.unknown_quantifier, and logic_verifier.execution_error.

Migrating from LogicResult

Status field
Proving a theorem
Variable declarations are now mandatory
Malformed BitVec declarations fail closed

Performance


Next steps