원클릭으로
smtprofiling
Debug F* queries sent to Z3, diagnosing proof instability and performance issues
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Debug F* queries sent to Z3, diagnosing proof instability and performance issues
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | smtprofiling |
| description | Debug F* queries sent to Z3, diagnosing proof instability and performance issues |
This skill is used when:
Wrap the part of the program to diagnose proof failures with
#push-options "--log_queries --z3refresh --query_stats --split_queries always"
let definition_to_be_debugged ...
#pop-options
Run F* on the file (with appropriate include paths)
fstar.exe Module.fst
The log messages will show the name of the .smt2 file logged for each proof obligation.
Using --z3refresh together with --log_queries produces a
self-contained .smt2 file per query (each query starts with a fresh
Z3 process). This is critical for isolating expensive sub-goals: each
.smt2 file can be profiled independently.
Using --split_queries always decomposes compound VCs into individual
sub-goals. This lets you identify exactly which conjunct of a
proof obligation is expensive. Cross-reference the query number in the
Query-stats output with the .smt2 file number to map each timing
to its isolated file.
Find z3 in the path. It might be named z3-4.13.3, z3-4.15.1 etc., with a version number suffix
z3 queries-myquery.smt2
You can add z3 options like smt.qi.profile=true to see which quantifiers were firing
too much and what to do about it
The Z3 quantifier profile is printed to stderr. Capture it with:
z3 smt.qi.profile=true queries-myquery.smt2 2> qi_profile.txt
Parse the output to get per-quantifier totals:
awk '/\[quantifier_instances\]/ {
name = $2; count = $4; total[name] += count;
} END {
for (n in total) printf "%8d %s\n", total[n], n;
}' qi_profile.txt | sort -rn | head -20
Some quantifiers in F*'s SMT encoding always fire a lot---this is by design. Typically, quantifiers with unqualified names (Box_bool_proj_0) or those with names in Prims, or FStar. Pervasives will fire a lot.
Look for quantifiers in modules that are in files that you authored that are firing a lot---these might signify that you should write a pattern for those quantifiers, or control their instantiation explicitly.
A quantifier cascade is when one quantifier instantiation produces terms that trigger another quantifier, which produces more terms, creating a positive feedback loop. This is the most common cause of queries taking minutes instead of seconds.
Signs of a cascade:
[SMTPat (f x y)]
and its conclusion introduces new terms that match the pattern of
another quantifierExample cascade found in EverParse:
cbor_map_equal unfolds to forall k. cbor_map_get m1 k == cbor_map_get m2 kcbor_map_get produces cbor_map_defined termscbor_map_defined_alt has [SMTPat (cbor_map_defined k f)] and
introduces exists v. cbor_map_mem (k,v) fOption 1: Remove the SMTPat and add a bring_ helper
Remove [SMTPat ...] from the offending lemma. Add a wrapper that
re-introduces the quantifier with its pattern in a controlled scope:
// Original: always active, causes cascades
let problematic_lemma (x: t) (y: t)
: Lemma (some_prop x y)
[SMTPat (f x y)] // REMOVE THIS
= ...
// New: call bring_ only where needed
let bring_problematic_lemma ()
: Lemma (forall (x: t) (y: t) . {:pattern (f x y)} some_prop x y)
= Classical.forall_intro_2 problematic_lemma
Then call bring_problematic_lemma () only in the specific proofs
that need it, rather than polluting all proofs globally.
Option 2: Use --using_facts_from to prune per-function
Surgically remove a lemma from Z3's context for a specific function without modifying the lemma itself:
#push-options "--using_facts_from '* -Module.Name.problematic_lemma'"
let my_expensive_function ...
#pop-options
This is useful for quick experimentation before committing to
Option 1. Verify the quantifier is actually removed by checking the
.smt2 file.
Option 3: Use multi-pattern triggers
Change a single-term pattern to a conjunctive multi-pattern so the lemma only fires when multiple terms are present:
// Before: fires whenever (f x y) appears
[SMTPat (f x y)]
// After: fires only when both (f x y) AND (g x) are present
[SMTPat (f x y); SMTPat (g x)]
For large projects, profile the entire build:
# 1. Full build with --query_stats
make -j$(nproc) OTHERFLAGS="--query_stats" 2>&1 | tee build_profile.log
# 2. Find top files by SMT time
grep -a "Query-stats" build_profile.log | grep "succeeded" \
| sed -n 's/(\([^(]*\)\.\(fst\|fsti\)([^)]*)).*succeeded in \([0-9]*\) milliseconds.*/\1.\2 \3/p' \
| awk '{f[$1]+=$2} END {for(k in f) print f[k], k}' | sort -rn | head -20
# 3. Find top individual queries
grep -a "Query-stats" build_profile.log | grep "succeeded" \
| sed -n 's/.*Query-stats (\([^)]*\)).*succeeded in \([0-9]*\) milliseconds.*rlimit \([0-9]*\) (used rlimit \([0-9.]*\)).*/\2\t\4\t\3\t\1/p' \
| sort -rn | head -20
# 4. For the most expensive query, isolate it:
#push-options "--log_queries --z3refresh --query_stats --split_queries always"
# 5. Profile the isolated .smt2 file:
z3 smt.qi.profile=true queries-Module-NNN.smt2 2> qi_profile.txt
# 6. Parse the profile to find the dominant quantifier(s)
| Option | Effect |
|---|---|
--split_queries always | Each assertion becomes its own Z3 query |
--z3refresh | Fresh Z3 process per query (no accumulated state) |
--log_queries | Write .smt2 files for each query |
--query_stats | Print per-query timing and rlimit usage |
--z3cliopt smt.arith.nl=false | Disable non-linear arithmetic |
--z3cliopt smt.qi.eager_threshold=N | Limit quantifier instantiation eagerness |
--using_facts_from '* -Name' | Prune specific lemmas from Z3 context |
--ext context_pruning | Prune unreachable assumptions (default on) |
--fuel N / --ifuel N | Control recursive unfolding depth |
After fixing expensive queries, tighten the settings:
fuel and ifuel to the minimum that works (prefer 0-2)rlimit to 2-4x the used rlimit reported by --query_statsWhen stuck on a proof performance problem, search for commits in the same project that successfully stabilized similar proofs. github.com/FStarLang/AlgoStar is a good example of a project with many proof performance commits to mine for techniques.:
Study what techniques were used — factoring lemmas, adding patterns,
using calc proofs, adding opaque_to_smt, using --using_facts_from — and
apply the same techniques to the current problem.
The following catalog is mined from FStarLang/AlgoStar (897 commits of verified CLRS algorithm implementations in F*/Pulse). These are proven techniques organized by type of problem.
Problem: Large .fst files make Z3 slow because every function's VC
carries the context of all preceding definitions.
Solution: Extract groups of related lemmas into separate modules.
Case study (Prim MST):
Prim.Impl.fst was 2267 lines with 76 inline math lemmas — slow and fragilePrim.Greedy.fst (1010 lines, 41s) for all greedy safety
lemmas; Prim.KeyInv.fst (496 lines, 4s) for the key invariant module;
Prim.Defs.fst (68 lines, 1.5s) for basic definitionsPrim.Impl.fst shrank to 938 lines (59% reduction)d1665a6, aada386 in FStarLang/AlgoStarCase study (Heapsort):
_upto lemma proofs by having non-_upto versions delegate
to the general versions — eliminated one copy of an expensive
perm_prefix_bounded_aux proofLemmas.fst: 238s → 63s (74% reduction)29242fb in FStarLang/AlgoStar#restart-solver Between Key FunctionsProblem: Z3's accumulated state from prior functions pollutes the context for subsequent proofs, causing slowness or spurious failures.
Solution: Place #restart-solver between large functions to reset Z3.
Case study (Heapsort):
#restart-solver before build_max_heapImpl.fst: 445s → 241s (46% reduction)29242fb in FStarLang/AlgoStarCase study (Kruskal NL arithmetic):
u * n + v < n * n) worked in isolation but failed
in-context due to accumulated Z3 state#restart-solver between key functions fixed it01e2e8d in FStarLang/AlgoStar--z3refresh with --split_queries alwaysProblem: --z3refresh starts a new Z3 process per query (expensive);
proofs need high rlimit.
Solution: --split_queries always decomposes VCs into smaller queries
without the overhead of process restarts, often allowing lower rlimits.
Case study (CountingSort):
--z3rlimit 800 --z3refresh to --z3rlimit 400 --split_queries always5e35796 in FStarLang/AlgoStarProblem: Complex predicates cause Z3 to spend time unfolding definitions it doesn't need.
Solution: Make predicates [@@"opaque_to_smt"] and provide explicit
_at instantiation lemmas (for init, after_update, etc.).
Case study (Prim KeyInv):
key_inv, ims_finite_key, parent_in_mst all made opaquekey_inv_init, key_inv_after_update, key_inv_at lemmas
for controlled instantiationparent_in_mst_after_update uses introduce forall + 3-way case split
with explicit lemma calls — no Z3 quantifier matching neededaada386 in FStarLang/AlgoStarProblem: Non-linear arithmetic expressions like u * n + v cause Z3
to enter expensive NL reasoning.
Solution: Bundle NL index computation in a function with refinement types,
then use FStar.Math.Lemmas for explicit NL bounds.
Case study (Kruskal adj_weight):
adj_weight bundles u * n + v index in a refined function typeFStar.Math.Lemmas.lemma_mult_lt_right for explicit NL boundsbf00c94 → 01e2e8d in FStarLang/AlgoStarProblem: Proofs initially written with high rlimits are flaky — they may break with unrelated changes to the Z3 context.
Solution: Once a proof works, systematically reduce rlimit to the minimum needed (target ≤ 10, tolerate up to 200 for complex Pulse VCs).
Case study (ch22 BFS/DFS):
queue_bfs: 2400 → 200 (92% reduction)maybe_discover: 600 → 200 (67% reduction)dfs_visit: 800 → 200 (75% reduction)896a145 in FStarLang/AlgoStarCase study (ch26 MaxFlow):
max_flow: 600 → 50 (92% reduction!)find_bottleneck_imp, augment_imp: 200 → 80 (60% reduction)eeb0fdb in FStarLang/AlgoStarProblem: Large Pulse functions take long to verify, with every proof obligation going issuing its own Z3 query, and with the assumptions in the context accumulating through the context of the function.
Solution: Extract helpers: factor fn for sub-tasks, call them
from the enclosing function, allowing the proof to proceed more modularly.
Case study (Prim MST):
find_min_vertex: hoisted to separate fn → 3.5min build (was 4.5min)prim_step: hoisted to separate fn → cleaner invariant structureupdate_keys: hoisted to separate fn → focused VC91e8b43, fdbf5bc in FStarLang/AlgoStarProblem: Fuel 2 causes Z3 to try unfolding recursion two levels deep, which can be exponentially expensive with multiple recursive definitions.
Solution: Set --fuel 0 --ifuel 0 as the module default, then use
#push-options "--fuel N" only on lemmas that need it.
Case study (ch09 PartialSelectionSort):
remove_element_count_le/lt needed --fuel 4 explicitly4c158fa in FStarLang/AlgoStarintroduce forall Instead of SMT Quantifier MatchingProblem: Complex universal quantifiers with tricky patterns — Z3 fails to find the right instantiation.
Solution: Use F*'s introduce forall tactical sugar with explicit
case splits and lemma calls, bypassing Z3's pattern matching entirely.
Case study (Prim parent_in_mst_after_update):
introduce forall + 3-way case split (v=u, v in MST, v not in MST)aada386 in FStarLang/AlgoStarProblem: Using helper functions inside quantifier bodies prevents Z3 from matching patterns.
Solution: Use raw expressions (e.g., Seq.index) in opaque predicate
bodies rather than convenience wrappers.
Case study (Prim KeyInv):
key_inv initially used swt (safe weight accessor) in its quantifierw*n+v / Seq.index — matches what prim_safe_add_vertex needsc992174 in FStarLang/AlgoStarIf Z3 fails to instantiate long quantifier chains (proof fails even though the required facts are in scope):
smt.qi.eager_threshold controls how aggressively Z3 instantiates
quantifiers. Default is 10.--z3smtopt '(set-option :smt.qi.eager_threshold 100)'eager_threshold instantiations — these were cut offFind PoP-in-FStar at https://github.com/FStarLang/PoP-in-FStar
The section under_the_hood/uth_smt.rst contains information about F*'s SMT encoding and how to profile.
Use the F* MCP server for interactive, incremental typechecking of F* and Pulse code
Verify F* and Pulse code with fstar.exe and interpret errors
Extract verified F*/Pulse code to C via KaRaMeL (.krml intermediate representation)
Structure a new F*/Pulse verification project with Makefile and directory layout
Systematic workflows for debugging F*/Pulse verification failures
Build F*, Pulse, and KaRaMeL from source (fstar2 branch) for use in a verification project