mirror of
https://github.com/Z3Prover/z3
synced 2026-08-03 20:53:30 +00:00
### Problem
The order of evaluation of function arguments is unspecified in C++
(arguments are indeterminately sequenced since C++17). Compilers use
this freedom differently:
```c++
static int f(int i) { printf("%d ", i); return i; }
static void g(int, int, int) { printf("\n"); }
int main() { g(f(1), f(2), f(3)); }
```
| compiler/target | output |
|---|---|
| gcc 13, x86_64 | `3 2 1` |
| gcc 13, aarch64 | `1 2 3` |
| clang 18, x86_64 | `1 2 3` |
Z3 has many call sites where **two or more arguments each create AST
nodes**, e.g. (before this PR, `bv_rewriter.cpp:876`):
```c++
result = m.mk_ite(c, m_mk_extract(high, low, t), m_mk_extract(high, low, e));
```
The two extract nodes are hash-consed and receive their AST ids in
evaluation order, so the id assignment differs between
compilers/targets. AST ids feed heuristic tie-breaking throughout the
solver (`bool_rewriter`'s `m_order_eq` equality-operand ordering,
id-based sorts in `array_rewriter`, case-split ordering, ...), so
**byte-identical input takes different solver paths depending on the
compiler and architecture z3 was built with**.
### Evidence
Investigated while chasing cross-platform proof-time instability in
CBMC/mldsa-native CI (diffblue/cbmc#8991), on byte-identical ~12 MB SMT2
instances (bit-vectors + arrays + quantifiers), with the `string_hash`
fix from #10163 applied to isolate this effect. Z3 4.15.3, gcc 13 on
x86_64 Linux and aarch64 Linux (Graviton):
* one instance: **17 s on x86_64 vs 1633 s on aarch64** (both `unsat`; a
sibling instance shows the reverse direction). Run-to-run within one
host: ±1 %.
* Instrumenting `ast_manager::register_node_core` with an order
fingerprint (running hash over `(node hash, node id)`) shows both
architectures construct **identical AST sequences up to registration
#41,789**, where x86_64 creates `(extract[0:0] #xFFFFFFFF)` before
`(extract[0:0] #xFFFFFFFE)` and aarch64 the other way around — from
identical call stacks at the `mk_ite`-over-two-`mk_extract` site quoted
above. All divergence between the two hosts flows from such events
(pointer/ASLR effects experimentally excluded: fingerprints are
invariant under `setarch -R` and across repeated runs).
* Sequencing that one site by hand moved the first divergence to
#248,118 — the analogous `mk_ite(c, mk_select(...), mk_select(...))`
site in `array_rewriter.cpp`. Sequencing that one, too, moved it to
#248,411, inside `nnf:👿:process_iff_xor` — i.e. the next layer of
the same onion.
* With the whole `ast/rewriter` layer swept (this PR), the instrumented
builds produce **identical AST construction traces on both architectures
throughout the entire rewriter phase** of this 546k-line industrial
instance; the first divergence left is the NNF one.
### Fix
Following the precedent of
|
||
|---|---|---|
| .. | ||
| bit2int.h | ||
| bit_blaster.cpp | ||
| bit_blaster.h | ||
| blast_term_ite_simplifier.h | ||
| bound_manager.cpp | ||
| bound_manager.h | ||
| bound_propagator.cpp | ||
| bound_propagator.h | ||
| bound_simplifier.cpp | ||
| bound_simplifier.h | ||
| bv1_blaster.cpp | ||
| bv1_blaster.h | ||
| bv_bounds_simplifier.cpp | ||
| bv_bounds_simplifier.h | ||
| bv_divrem_bounds.cpp | ||
| bv_divrem_bounds.h | ||
| bv_elim.h | ||
| bv_slice.cpp | ||
| bv_slice.h | ||
| card2bv.cpp | ||
| card2bv.h | ||
| CMakeLists.txt | ||
| cnf_nnf.h | ||
| demodulator_simplifier.cpp | ||
| demodulator_simplifier.h | ||
| dependent_expr.h | ||
| dependent_expr_state.cpp | ||
| dependent_expr_state.h | ||
| der_simplifier.h | ||
| distribute_forall.cpp | ||
| distribute_forall.h | ||
| dominator_simplifier.cpp | ||
| dominator_simplifier.h | ||
| elim_bounds.h | ||
| elim_term_ite.h | ||
| elim_unconstrained.cpp | ||
| elim_unconstrained.h | ||
| eliminate_predicates.cpp | ||
| eliminate_predicates.h | ||
| euf_completion.cpp | ||
| euf_completion.h | ||
| extract_eqs.cpp | ||
| extract_eqs.h | ||
| factor_simplifier.cpp | ||
| factor_simplifier.h | ||
| flatten_clauses.h | ||
| fold_unfold.cpp | ||
| fold_unfold.h | ||
| injectivity_simplifier.h | ||
| linear_equation.cpp | ||
| linear_equation.h | ||
| max_bv_sharing.cpp | ||
| max_bv_sharing.h | ||
| model_reconstruction_trail.cpp | ||
| model_reconstruction_trail.h | ||
| propagate_values.cpp | ||
| propagate_values.h | ||
| pull_nested_quantifiers.h | ||
| push_ite.h | ||
| randomizer.h | ||
| reduce_args_simplifier.cpp | ||
| reduce_args_simplifier.h | ||
| refine_inj_axiom.h | ||
| rewriter_simplifier.h | ||
| solve_context_eqs.cpp | ||
| solve_context_eqs.h | ||
| solve_eqs.cpp | ||
| solve_eqs.h | ||
| then_simplifier.h | ||