fold/docs/internal/library-expansion-matrix.md
2026-05-27 10:36:57 -04:00

160 lines
24 KiB
Markdown
Raw Permalink 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.

# Z3 + Clang library-expansion opportunity matrix
A scorecard of Z3 and Clang LibTooling features fold has either adopted,
rejected, or left as evidence-gated opportunities. Companion to
`docs/internal/perf-audit.md`; inherits the same "measure before optimising"
discipline that audit baked in after nine wrong-hypothesis pivots.
This is now primarily a decision log and routing table. Most rows have
already been audited; use the `Status` column to distinguish shipped work,
measured false positives, and genuinely unscheduled capability ideas. Active
loose ends live in `docs/internal/library-expansion-followups.md`.
When a row does ship, update its `Status` with the measurement artifact link
(e.g. `docs/internal/perf-audit/runs/matrix/<row>-<target>-YYYY-MM-DD.json`).
## Current usage snapshot
### Z3 (`z3++.h`)
| Surface | Files |
|---|---|
| BV-64 arithmetic + overflow predicates | `src/symbolic/encoder.cpp`, `include/fold/symbolic/encoder.hpp` |
| Boolean ops + `ite` | `src/symbolic/encoder.cpp` |
| Arrays (`select`/`store`/`const_array`) for buffer + heap | `src/symbolic/encoder.cpp`, `include/fold/symbolic/heap_model.hpp` |
| Quantifiers (`forall`/`exists`) — **loop-invariant inference only** | `src/symbolic/loop_invariant.cpp` |
| Uninterpreted functions for heap pointer identity | `include/fold/symbolic/heap_model.hpp` |
| Solver portfolio (QF_BV + simplify→bit-blast→SAT, `par_or`) | `src/symbolic/tactics.cpp` |
| Assumption-based `solver.check(expr_vector)` | `src/detect/{null_deref,div_by_zero,sign_conversion}.cpp` |
| Unsat-core caching (per-detector, member `unsat_cores_` + path-hash gate) | `src/detect/{null_deref,div_by_zero,oob_access,int_overflow,sign_conversion,int_truncation}.cpp` |
| Multi-model sampling + model→bytes for fuzz seeds | `src/fuzz/model_to_bytes.cpp`, `src/detect/null_deref.cpp` |
| `z3::fixedpoint` for synthesised loop invariants | `src/symbolic/loop_invariant.cpp` |
| Statistics aggregation (`solver.statistics()` → `SolverStats`) | `include/fold/symbolic/theory.hpp` |
| Shared `shared_ptr<z3::context>` for cheap env clones | `include/fold/symbolic/encoder.hpp` |
### Clang LibTooling / AST / Analysis
| Surface | Files |
|---|---|
| `RecursiveASTVisitor` (detectors + frontend) | `src/detect/*.cpp`, `src/frontend/{ast_visitor,clang_backend}.cpp` |
| `ParentMapContext` | `src/detect/{security_walker,null_deref,div_by_zero}.cpp` |
| `clang::CFG::buildCFG` + `CFGBlock` | `src/ir/cfg_walker.cpp`, `src/detect/dead_statement.cpp` |
| `ASTFrontendAction` + `ASTConsumer` | `src/frontend/{clang_backend,ast_visitor}.cpp` |
| `tooling::CompilationDatabase` + `ClangTool` | `src/cli/scan_runner.cpp`, `src/frontend/comp_db.cpp` |
| Type queries (`getTypeSizeInChars`, `Linkage`) | `src/symbolic/encoder_type_queries.cpp`, `src/detect/oob_access.cpp` |
| `SourceManager` / `SourceLocation` (ubiquitous) | every detector |
| Hand-built call graph (Tarjan SCC) — **not** `clang::CallGraph` | `src/ir/call_graph.cpp` |
| Hand-built CTU cache + persistence — **not** `clang::CrossTranslationUnitContext` | `src/detect/{cross_tu_cache,cross_tu_cache_io}.cpp` |
| Custom def-use chains — **not** `clang::dataflow` | `include/fold/ir/def_use.hpp` |
## Opportunity matrix
Columns:
- **Feature** — the Z3/Clang API
- **Insertion site** — file (path verified at write time)
- **Affects** — detectors / `encoder` / `frontend` / `infra`
- **Payoff** — precision / perf / capability / UX
- **Sizing** — qualitative S/M/L + the measurement that would
confirm it. The audit's lesson is to ALWAYS name that
measurement before writing code.
- **Risk** — semantic-drift / perf-regression / build-fragility
- **Deps** — other matrix rows or upstream features needed first
- **Status** — `shipped`, `unscheduled`, or retired with a named false-positive
reason plus links to artifacts where available.
### Z3 rows
| # | Feature | Insertion site | Affects | Payoff | Sizing | Complexity | Risk | Deps | Status |
|---|---|---|---|---|---|---|---|---|---|
| Z1 | Quantifiers + patterns (`forall :pattern`) for function-summary preconditions | `src/symbolic/encoder_summary_instantiate.cpp` | encoder, all summary-backed detectors | precision | hypothesis-FP — measured curl scan: **3,052 instantiate calls, 238 ms total = 0.042% of analyze_ms (570 s)**. Not a hot path. | M | semantic-drift (matching heuristics fire differently), z3 quantifier e-matching typically slower than direct substitution | none | **unscheduled — measured FP**. Z1 (matrix author's original hypothesis: summary_lookup_ms reduction) was based on the wrong timer — `summary_lookup_ms` measures `SummaryStore::lookup()` (hash-map), NOT the `parse_string+substitute` path that Z1 would replace. Instrumented the parse_string path directly on curl 2026-05-25: 0.042 % of total scan time. No room for a measurable win, and quantifier encoding would *add* semantic-drift risk. Skip. |
| Z2 | Datatypes / tuples for structured pointer metadata `(base, size, freed)` | `include/fold/symbolic/heap_model.hpp` | oob-access, null-deref | perf, precision | design-FP — `HeapRegion` is a C++ struct whose only z3-typed fields (`base`, `size`) are already z3 BV exprs; `freed`/`in_loop`/`alloc_id` are C++ scalars. The struct never crosses TUs (cross-TU cache serialises `body_smt` only), so a z3 tuple sort saves zero bytes. `mergeFrom` is hashmap union with `freed = lhs.freed \|\| rhs.freed` — no z3 queries fired, so "alias-query count" is not a real timer. | L | semantic-drift, build-fragility (Z3 datatypes need careful sort setup) | none | **unscheduled — design FP**. The real opportunity at this site is path-sensitive `freed` merge (currently a crude bool-or that loses path-sensitivity), which is a heap-model correctness fix, not a library-expansion row. Carved out as future work in `docs/internal/perf-audit/optimization-backlog.md` rather than wedged into Z2's framing. |
| Z3 | String / sequence theory for `strcpy`/`strlen` and taint flow | `src/symbolic/encoder.cpp:259, 541, 549, 559` | oob-access, taint-track | capability, precision | hypothesis-FP for the "byte-unroll elimination" framing — encoder doesn't unroll strcpy/strlen byte-by-byte; concrete-length cases use `recordKnownStringDestinationLength` and symbolic cases bail (`clear_dst()`). Real opportunity is *new capability* (symbolic-length string reasoning), not eliminating existing unrolls. | L | semantic-drift (Z3 strings theory is heavier than BV) | none | **unscheduled — current-state FP**. Reframable as pure capability addition (symbolic strlen/strcpy) but payoff is speculative findings on string-content patterns; deferred until there's a concrete missed-finding driving it. |
| Z4 | Array lambda for symbolic-length memcpy/memmove modelling | `src/symbolic/encoder.cpp:688` (modelMemcpyLikeCall) | encoder, oob-access | capability, perf | M (shipped) — replaced per-byte `store(store(...))` chain with `z3::lambda(i, ite(dst_off ≤ i < dst_off + copy_bytes, select(src, ...), select(orig, i)))`. Dropped the `!arg_values[2].is_numeral()` bail; symbolic copy_bytes now modelled. Kept `kMaxModeledCopyBytes=4096` cap on CONCRETE bounds. z3 simplifies the lambda away at concrete-index selects so QF_BV stays viable — no logic-change cascade. Curl baseline post-Z4: `analyze_ms=565020`, `null-deref` `wall_ms=344675` with 2,533 queries (see baseline JSON). | M | none observed — full unit suite (57/57) green; downstream detectors handle the lambda transparently | none | **shipped** — `docs/internal/perf-audit/runs/matrix/Z4-curl-2026-05-26.json`, `tests/unit/test_memcpy_lambda.cpp` |
| Z5 | `z3::optimize` for minimal counterexample selection | `src/fuzz/minimal_witness.{hpp,cpp}` (new), `src/detect/null_deref.cpp:1023` | fuzz seed quality | UX | S (shipped) — measured on synthetic `access_at_offset(int *p, int offset) { if (0 < offset && offset < 100) return p[offset]; }`: primary witness drops to the path minimum (current direct artifact: `offset` **64 → 1**, 98.4% smaller). `divide_by_count(int *p, int count) { if (count > 0) return *p / count; }` unchanged (path-min was already 1). Buffer-size framing in the original row was wrong — `buffer_bytes` is a C++ constant in `param_schema.cpp:84`, not a z3 var. The real measurable is integer-witness magnitude. | S | semantic-drift — only the *primary* witness is refined; `recordAlternateWitnesses` keeps the original model so the corpus still covers the equivalence class | none | **shipped** — opt-in via active recorder presence; no flag needed. Per-finding cost = one `z3::optimize.check()`. Synthetic primitive artifact: `docs/internal/perf-audit/runs/matrix/Z5-synthetic-2026-05-26.json`. Curl-scale validation deferred: `fuzz-prep` on curl crashes with SIGSEGV (pre-existing — verified Z5-OFF baseline also exit 139). |
| Z6 | Fixedpoint engine extension beyond loop invariants (mutual recursion, linear-arithmetic invariants) | `src/symbolic/loop_invariant.cpp`, `src/detect/security_walker.cpp:1604` | encoder | capability | infrastructure-FP — `inferCountedLoopInvariant` already runs; its result is only used for `spdlog::debug` at security_walker.cpp:1607. `refineRange`/`recordRangeInOctagon` run unconditionally and do NOT consume the invariant. Extending fixedpoint to mutual recursion before wiring the existing single-loop result back into analysis state would add capability nothing reads. | L | semantic-drift, perf-regression | Z1 (deferred — measured FP) | **unscheduled — infrastructure FP**. Reframable as: (a) first wire the existing loop invariant into octagon/range refinement (small fix), then (b) extend to mutual recursion. (a) is a precision-bug fix, not a library-expansion row. (b) without (a) ships dead code. |
| Z7 | Solver push/pop for incremental walkIf paths | `src/detect/security_walker.cpp::walkIf` (~L1500) | encoder, all walker-driven detectors | perf | S — `env_clone_count` reduction (already measured as <1µs each; bar is low) | M | semantic-drift (incremental solver state must match cloned-env semantics exactly) | none | unscheduled — env_clone wasn't the bottleneck (0ms / 770K clones) |
| Z8 | Generalise unsat-core caching to remaining detectors | `src/detect/sign_conversion.cpp`, `src/detect/int_truncation.cpp` | sign-conversion, int-truncation | perf | S — per-detector wall_ms in `--profile` | S | none — pattern is proven across four detectors | none | **shipped** — applied to sign-conversion + int-truncation; taint-track skipped (uses no solver, dataflow only) |
| Z9 | Proof + Craig interpolant generation for finding witnesses | finding emission in detectors; `src/witness/graphml.cpp` | capability, UX | infrastructure-absent FP — z3 4.13+ (fold's pinned version) does not export interpolant APIs. They were a 4.5-era research feature that was removed; `Z3_get_interpolant`/`mk_interpolant` are not in the linked headers. Even if available, existing `buildCounterexample` already emits concrete model values (`(e.g., when ptr=0)`) which are easier to read than formula slices. | L | perf-regression (proof generation is expensive) | upstream z3 reintroducing interpolants | **unscheduled — infrastructure absent**. Reconsider if z3 reintroduces the API; until then concrete-model counterexamples cover the same UX surface. |
| Z10 | `let`-bindings / macros for serialised-summary compaction | `src/symbolic/serialize.cpp:73` | encoder, summary-store size | perf | M — measured smaller serialised bodies on encoder-shaped n=100 corpora via `z3::set_param("pp.min-alias-size", 2)`; current direct artifact: **320.4 KB → 113.7 KB** (64.5% smaller). Earlier corpus cited 94.9 KB → 57.0 KB (39.9% smaller). | S | none | none | **shipped** — `src/symbolic/serialize.cpp`, `tests/unit/test_serialize_letbindings.cpp`, `docs/internal/perf-audit/runs/matrix/Z10-serialize-corpus-2026-05-26.json` |
### Clang rows
| # | Feature | Insertion site | Affects | Payoff | Sizing | Complexity | Risk | Deps | Status |
|---|---|---|---|---|---|---|---|---|---|
| C1 | AST matchers (`clang/ASTMatchers`) for targeted patterns | `src/detect/dead_branch.cpp`, `src/detect/security_walker.cpp` | detectors | UX (code clarity); some perf | no-forcing-function — surveyed sites: dead_branch has 7 dyn_cast uses, all single-line type-tag dispatch (`dyn_cast<CompoundStmt>(stmt)`); security_walker has 48 of the same shape. ASTMatchers would be MORE verbose for type-tag dispatch (require MatchFinder + bind + callback). They win only for pattern-structure queries (regex name + nth arg shape + …), which the existing detectors don't need. | S–M per call site | none for isolated replacements | none | **unscheduled — no measurable benefit-bearing site**. Reconsider when a concrete pattern-structure detector lands that would actually benefit (e.g. a "specific calling-convention misuse" check that's awkward to write with dyn_cast). |
| C2 | `clang::CallGraph` for built-in call resolution | `src/cli/scan_runner.cpp` → `ir::ProjectCallGraph` | infra | perf, capability (constructor/new/default-init/block edges) | M — virtual-call resolution count; CTU cache hit-rate stability | M | semantic-drift (clang's call graph has different node identity than `ir::ProjectCallGraph`); breaks cross-TU cache invariants | careful migration plan | **shipped selective adapter** — fold's `ProjectCallGraph` remains canonical; Clang TU-local edges are merged additively and persisted through the existing cross-TU cache contribution format |
| C3 | `clang::dataflow::Analysis` framework for pre-pass typestate | `src/symbolic/encoder.cpp` (env initialisation) | encoder, null-deref | precision, perf | L (not M as originally sized) — clang ships the framework but NO ready-made pointer-nullability analyzer. Bundled `UncheckedOptionalAccessModel` is for `std::optional`, not raw pointers. C3 would require writing a custom `DataflowAnalysis<NullPtrLattice>` with transfer functions for every pointer-touching expression — comparable cost to a new detector. Original "M — functions_prechecked_safe" sizing was wrong-tracked: that counter measures syntactic sink-absence (`precheckSecuritySinks`), not nullability. | L | wrong lattice = silent missed bugs (false-negatives). additive-pre-pass framing only holds if the lattice is genuinely sound. | none | **unscheduled — sized wrong + no forcing function**. fold already has multiple condition-based non-null pre-passes (`enclosingThenBranchProvesNonNull`, `ternaryArmProvesNonNull`, `priorFunctionTextProvesNonNull`, `staticParamContractProvesNonNull`, `current_debugassert_nonnull_`). Reconsider when a concrete null-deref FP catalog says clang dataflow would catch what these miss. |
| C4 | `clang::consumed::ConsumedAnalysis` for RAII | `src/detect/security_walker.cpp` | use-after-free path (future detector) | capability | wrong-tool-FP — ConsumedAnalysis is intra-procedural typestate for linear types with states (Unconsumed/Consumed/Unknown). It only activates on classes marked `[[clang::consumable]]` + member functions marked `[[clang::callable_when(state)]]` + `[[clang::return_typestate(state)]]`. Real-world C code (curl, yams, kernel-style) has none of these attributes; without auto-injection the analysis is silent. | M | none — but no useful signal on unannotated code | source annotations or attribute auto-injection | **unscheduled — wrong-tool FP**. A native UAF detector remains a real opportunity, but it's a CFG-walker shape (track malloc/free on tracked pointers + flag derefs after free), not ConsumedAnalysis. Carved out as a future detector row rather than wedged into C4's framing. |
| C5 | `UninitializedValues` analyzer → `uninit-local-read` detector | `src/detect/uninit_local_read.{hpp,cpp}` (new), `src/symbolic/uninit_pre_pass.{hpp,cpp}` (measurement) | new detector `--detectors uninit-local-read` | capability | S (shipped) — measurement wrapper + 5 unit tests on the detector entry point + end-to-end smoke on a synthetic fixture (`int x; int y = x; → finding @ line 3; int p; int y = p; → no finding`). Original "null-deref FP triage" framing was wrong-hypothesis (clang's analyzer flags LOCAL read-before-write, NOT parameter nullability) — C5 now ships *as* the local-uninit detector that calibration revealed. Curl baseline: 0 SARIF findings after report filtering, 68 ms detector wall. | S | none | none | **shipped** — library function + detector + registry + scan-runner dispatch + SARIF rule id; baseline `docs/internal/perf-audit/runs/matrix/C5-curl-2026-05-26.json` |
| C6 | `clang::Preprocessor` macro-expansion hook | `src/frontend/clang_backend.cpp` | dead-statement, dead-branch | precision | current-state-FP — `dead_statement.cpp:140` already filters `anchor->getBeginLoc().isMacroID()` (battle-tested in 2026-05-15 curl audit: doh.c 9,060→0 findings). Remaining FPs are CFG-construction artifacts (range-for desugaring, EH stubs), not preprocessor-induced. fold has no multi-config scan mode, so "variant -D" is not a live failure mode. dead-branch lacks the macro-ID filter but no documented FP drives the need. | M | build-fragility (Preprocessor hooks fire during parse; lifetime is delicate) | none | **unscheduled — current-state FP**. If a real dead-branch FP under macros surfaces, the fix is one-line (add the same `isMacroID()` filter to dead_branch.cpp), not a Preprocessor-hook row. |
| C7 | `AvailabilityAttr` / deprecation detection | `src/detect/deprecated_api.{hpp,cpp}` (new) | new detector (`--detectors deprecated-api`) | UX, capability | S (shipped) — 7 unit tests pin `[[deprecated]]`, `__attribute__((deprecated))`, message-payload propagation, decl-only-not-flagged, multi-callsite, method-call. Curl baseline: **52 real findings** with replacement guidance carried through ("Use curl_mime_init()", "first deprecated in macOS 10.11 — use OpenDirectory Framework"). Per-call rather than per-decl so refactors show monotone progress. | S | none | none | **shipped** — baseline `docs/internal/perf-audit/runs/matrix/C7-curl-2026-05-26.json` |
| C8 | `RawCommentList` for `/*@ requires */` user contracts | `src/symbolic/function_contracts.{hpp,cpp}` (new), `src/detect/security_walker.cpp` (apply hook), `src/frontend/comp_db.cpp` (`-fparse-all-comments`) | encoder, all detectors that consult `isDefinitelyNonNull` | capability | S (shipped) — 8 unit tests pin: `!= null` / `!= NULL` / `!= nullptr` shapes, multi-line, unknown-param reject, twin-flag negative. v1 grammar = `/*@ requires <param> != <null> */`. Suppresses **within-function** derefs on contracted params; synthetic twin baseline dropped null-deref findings **6 → 1** (5 contracted derefs suppressed; unannotated residual still fires). **Does not** suppress call-site findings (callers still need to prove non-null at the call). The call-site suppression is the "callee-standalone-context" gap from `docs/internal/perf-audit/fold-on-fold.md`, separate from C8. | S | none | none — but requires `-fparse-all-comments` (added to applyHostToolchain) | **shipped (within-function suppression)** — baseline `docs/internal/perf-audit/runs/matrix/C8-contracts-synthetic-2026-05-26.json` |
| C9 | `FixIts` for `fold --fix-output` (unified-diff patch) | `include/fold/report/{fix_it,patch_writer}.hpp` (new), `src/report/patch_writer.cpp` (new), `src/detect/{uninit_local_read,deprecated_api}.cpp` (consumers), `src/cli/main.cpp` (--fix-output flag) | UX (new capability — autopatch) | S (shipped) — 4 patch_writer tests + 4 FixIt-emission tests + end-to-end smoke on synthetic fixture (`int x; → int x = 0;` patch applies cleanly with `git apply --check`). Consumers: UninitLocalRead (scalar integer locals only — `int x;` → `int x = 0;`; non-scalar deferred); DeprecatedApi (regex on the message for `[Uu]se foo()` pattern; emits no FixIt when no replacement is parseable, rather than hallucinating). Patch writer now emits one coherent hunk per file so multiple same-file FixIts apply cleanly. Patch-only mode — no in-place rewrites — keeps the safety blast radius small. | M | semantic-drift mitigated by emitting **only** when transform is provably sound and never auto-applying | C8 contracts (deferred — could prove additional patch safety later) | **shipped** — unified-diff output via `fold scan --fix-output <path>`; baseline `docs/internal/perf-audit/runs/matrix/C9-synthetic-2026-05-26.json`. Multi-line replacements + non-scalar uninit + non-call-form deprecation replacements remain unscheduled. |
| C10 | `clang::CrossTranslationUnitContext` | `src/detect/cross_tu_cache.cpp` | infra | perf, precision | architectural-mismatch FP — clang CTU is an on-demand callee-AST importer (`ASTImporter`); fold's `CrossTuCache` is a property-summary cache (`CalleeProperties { derefed_params, divisor_params, ... }`). Different paradigms: clang CTU exists for analyses that walk callee STATEMENTS; fold deliberately avoids that (summary_encoder serialises bodies to SMT-LIB strings; detectors consume properties, not AST). Adopting clang CTU would mean redesigning the analysis paradigm, not extending it. | L | semantic-drift (would invalidate cross_tu_cache.v2 parity) | full analysis-paradigm redesign | **unscheduled — architectural-mismatch FP**. The matrix's original "selective adoption" framing implied a real selective use case; on inspection there isn't one. fold's summary-property approach is architecturally superior for its design (smaller serialised footprint, O(1) lookup, no per-call AST parse). |
## How to use this matrix now
The first sweep has already retired the original top picks (Z8, C5, C8)
and several adjacent rows. Treat this file as a compact map of what was
learned, not as a fresh backlog ordered by the old hypotheses.
### Status meanings
- **shipped** — implementation exists; prefer follow-up measurement or polish,
not another implementation pass.
- **unscheduled — measured/design/current-state/etc. FP** — the row's original
framing was audited and should not be re-opened without new evidence.
- **unscheduled — no forcing function / capability gap** — still possible, but
needs a concrete missed finding, benchmark, or user-facing workflow before it
is worth adding complexity.
### Active next work
Do not choose from the old Z/C row order by default. The actionable queue is in
`docs/internal/library-expansion-followups.md`:
1. **Commit/verify scan-runner wiring for C2 + C5** if those local hunks are
still present.
2. **Design a native `use-after-free` detector** (the real capability gap found
while retiring C4's `ConsumedAnalysis` framing).
3. **Capture missing per-row baselines** for shipped rows that predate the
per-row artifact convention.
### Re-opening a retired row
Only re-open a retired row when the session starts with at least one of:
- a concrete false-positive / false-negative example the row would address;
- a profile artifact showing the cited path is now hot;
- an upstream/library change that removes the reason the row was retired;
- a user-facing workflow that justifies a capability row despite no perf win.
If none of those exists, create a follow-up entry instead of expanding the
matrix row. This keeps the matrix from accumulating speculative implementation
plans that have already failed the audit discipline.
### When to pick differently
The follow-up queue assumes you want the least surprising path to completion. If
the next session's goal is instead:
- **New source detector capability** → start from the native UAF follow-up, not
C4's `ConsumedAnalysis` wording.
- **Fuzz-seed quality** → Z5 has a direct synthetic primitive baseline; require a fresh fuzz-prep baseline before extending it into curl-scale workflow changes.
- **Documentation-quality findings** → revisit Z9 only if the pinned Z3 exposes
proof/interpolant APIs again, or scope a non-interpolant explanation feature.
- **OSS readability** → wait for a detector whose pattern structure is actually
awkward with `dyn_cast` before using C1 / AST matchers.
## Out of scope
- Implementing any of the 20 rows. Each row is a separate
session.
- Bulk replacement of `RecursiveASTVisitor` with AST matchers
(C1 applies to specific isolated sites).
- Wholesale swap of `ir::ProjectCallGraph` for `clang::CallGraph`
(C2 shipped only as selective additive adoption; clang's graph lacks
the cross-TU provenance the persistence layer depends on).
- Z3 features requiring Z3 > 4.13.4 (the Conan pin). Bumping
Z3 is its own decision.