Finalized model interpretations#
Status and acceptance contract#
The lexical repair in SMT-LIB lexical conformance audit changes name representation,
not interpretation completion: the printer encodes canonical names using the
shared lexical policy and checks parameter freshness by canonical identity.
Symbol::internal explicitly marks generated auxiliaries; model completeness
and output filtering must never use a user’s name prefix as ownership evidence.
Names containing the selected backslash/control-character extensions round-trip
in Primo but are not promised portable to strict external parsers.
This is Primo’s original design and implementation contract for the general
model redesign, implemented through 8ff9991. The
legacy QF_UFLIA builder completed by EUF representative, while its printer
deduplicated rows by evaluated arguments and could replace their results after
constants had been printed. The fixture
benchmarks/regressions/qf_uflia_model_evaluated_tuple.smt2 exposes the mismatch.
Delivery followed: baseline evidence; typed core model/evaluator and SMT-LIB printer; QF_UFLIA; Boolean/QF_UF; Int/Real and mixed-Real paths; removal of legacy builders; cross-logic external validation.
The typed foundation (core::Model, core::ModelEvaluator, and
smtlib::print_model) is implemented and independently unit-tested. The current
QF_UFLIA collection driver validates owned ComponentModelFragment exports and
calls assemble_component_model in smt/component_model_assembler.cpp; its
private FragmentModelBuilder preserves the conversion-replay semantics below.
Boolean, QF_UF, QF_LIA, QF_LRA, QF_UFLRA and preprocessing-only paths retain
the private ComponentModelBuilder in typed_model_builder.cpp, entered through
smt::build_typed_model. Both adapters produce
the same immutable core representation and share its evaluator and printer.
Legacy string builders and their separate arithmetic evaluators remain removed.
See N-component combination migration for the new fragment contract and its
qualification; the following evidence closes the earlier general-model redesign,
not the subsequent N-component migration.
That earlier shared implementation completed its cross-logic correctness gates:
31,000 generated case/profile runs at 6f89774, followed by 7,000 mixed-logic
generated runs and 4,140 retained seed mutations at 3a2dd40. These verification
commits have identical solver sources to 8ff9991. All produced SAT models
submitted for acceptance passed independent Dolmen validation; no wrong answer,
invalid model or crash was found in the accepted gates. Resource limits are
reported separately, not counted as passes: 80 timeouts and two unknowns in the
main matrix, 108 timeouts in saved-Real semantic validation, and five mutation
timeouts. The Real replay gate additionally covers explicit monotone elimination.
The mutation campaign checks all four preprocessing modes and the QF_UFLIA
combination-off path; the generated matrix also checks mixed-Real reconciliation
off. Strict input-grammar diagnostics and rejected unrestricted mutations are
not accepted evidence. See the cross-logic and final-mutation entries in
docs/run-log.json for commands, seeds, per-family counts and artifacts.
The closing include-only revision 315b792 also passes a fresh 7,000-case
mixed-logic SLURM rerun, 3,487 independent SAT-model checks and all 72
witness/profile checks, with no errors or resource limits in that rerun.
Implementation, external correctness validation and review are complete;
the separately documented baseline advisories are not part of this bug fix.
The baseline eight-file external check reports seven historical witnesses
passing and the evaluated-tuple fixture failing; see the model-redesign entry
in docs/run-log.json.
Data and ownership#
core::ModelValue contains a sort ID and a Boolean, exact rational, or
uninterpreted-domain index. Sort IDs distinguish Int from Real and distinct
uninterpreted universes. core::Model owns a snapshot of the signature and
constant/function interpretations. Each function has a total default and a
finite map from typed argument tuples to typed results. Construction rejects
ill-sorted values and contradictory duplicate rows. Its public surface is
read-only after construction; neither evaluation nor serialization completes it.
The model owns no AST, SAT objects, term references, or component caches. The signature snapshot allows printing after the original term manager is destroyed. Assumptions sessions pass explicit visible-symbol IDs to the model builder. For QF_UF, the builder sees the complete retained theory assignment and active lowered assertion roots, while completion and printing expose only the active signature. Original ITE arguments are evaluated through that final model, rather than completed as separate unlowered UF classes. The session checks the model against original assertions. Popped declarations can reuse names with fresh IDs; prior snapshots remain independent across pop and reset. See Assumption-based incremental QF_UF. An evaluator borrows a live const term manager while evaluating its IDs; IDs are meaningful only in that manager. An evaluator’s memo belongs to one frozen model and cannot survive replacing it with a different interpretation.
The primo_smtlib library links primo_terms explicitly: its model printer
reads core interpretations and therefore needs the core exact-arithmetic
headers and libraries even on hosts without system-wide GMP development files.
smt/ alone owns postsolve reconstruction and component adapters. smtlib/
serializes the frozen interpretation, quotes identifiers, and chooses parameter
names without capture. Public CheckSatOutcome::model remains serialized text;
internal solver results carry the typed model to the session boundary.
Evaluation and proof obligations#
Basis: the original term and conversion contracts in Architecture and Tactic architecture, with ordinary structural evaluation of Primo’s typed core DAG. No optimized model-construction algorithm from another solver is claimed.
Equality compares same-sort semantic values, not printed strings.
Arithmetic computes exactly from children; applications consult only the function’s finalized tuple table and default.
ITE evaluates its condition and selected branch only. Boolean connectives, comparisons and every currently supported arithmetic term kind are covered.
An explicit stack and memo evaluate each demanded DAG node once. Cost is linear in visited nodes and edges apart from exact arithmetic and tuple hashing; stack/memo space is linear. No recursive descent over model terms.
Int division/modulo use the existing normalized positive-divisor term contract; unsupported zero divisors are not silently assigned a value.
Reconstruction must preserve authoritative component values and accepted
equalities/disequalities, and replay typed conversions in reverse execution
order with intermediate definition versions intact. Required definitions are
dependencies, not independently completable leaves. Provisional choices must
remain separate from authoritative and derived values. Replacing a provisional
choice invalidates its derived dependants and application keys before freezing.
Application result slots are distinct from argument-key dependencies: valid
equations such as x = f(x) must not be mistaken for cyclic substitution logs.
The worklist must either finish or identify a stalled dependency/conflicting
authoritative assignment, never stop at an arbitrary number of rounds.
In particular, one (SymbolId, evaluated arguments) has one result. This is a
construction invariant, not a row-selection preference. No output is emitted
until constants and tables agree. Relevant roots come from assertions,
component assignments and conversions, plus visible declarations for defaults;
unrelated arena terms cannot dictate the interpretation.
Checking, errors and verification#
Component adapter and replay checklist#
The collection adapter first validates the exact accepted model token, shared
scope coverage and partition agreement, authoritative values and symbol
ownership. It identifies component-local domain labels through shared bindings
before constructing evaluated function tuples. These additional obligations
belong to validate_component_fragment and assemble_component_model, not to
the frozen legacy builder. The base-construction and replay principles below
are shared by both adapters.
The staged adapter builds its base interpretation only from the final active assertions, accepted component facts, numeric component values and purification aliases. It saturates their EUF classes, attaches exact numeric/Boolean anchors, and uses distinct values for otherwise unanchored classes. Function rows are inserted by the shared evaluator’s evaluated argument tuple; conflicting rows are errors, not printer preferences. Historical applications appearing only in an eliminated definition are evaluated against that table, never added to it.
Solved-constant steps are simultaneous updates evaluated in the interpretation of the following stage. Replay runs backwards, with a fresh evaluator memo at each changed stage. This retains intermediate versions without flattening a definition map. Ground-unit rewriting and Boolean-unit propagation retain their facts in the reduced assertions; their logs are consistency checks at the corresponding replay stage, not constant assignments. Purification aliases are base-model constraints and require no change to public interpretations.
Monotone Real elimination reuses the one-sided reconstruction rule described in Monotone (one-sided) variable elimination: what to cite: evaluate the remaining affine terms and choose a value one unit beyond the tightest bound. Each restored variable starts a fresh evaluation epoch. Shared discharged constraints are checked after the batch, since a later variable can repair a constraint shared with an earlier one. This is the existing baseline rule, not a new elimination algorithm.
The shared explicit-stack evaluator accepts a symbol resolver for construction; its interpreted operators and lazy ITE scheduling are identical to frozen-model evaluation. A resolver must remain stable during one evaluation epoch. Base construction may insert a previously absent row but must never revise a returned value. Replay changes only eliminated constants, between epochs. Cost is linear in the evaluated DAG per epoch, plus congruence closure and exact arithmetic; overlapping definitions in separate epochs can be revisited. This is an original baseline, not an incremental dependency-invalidation optimization.
The session retains original typed assertion roots before preprocessing.
Debug builds and explicit production --check-model check those assertions
against the frozen interpretation. Structural consistency is always checked.
The flag does not request a model by itself: use SMT-LIB
:produce-models true and get-model as usual. All solver model routes use the
same evaluator. Resource limits in solving or model-separation searches can
still return unknown; contradictory component values, malformed interpretations
and failed reconstruction checks are internal errors.
Failures are internal errors with no model and nonzero CLI exit, not unsupported
input or unknown. Model-free solving avoids this work.
Tests must cover typed equality, duplicate tuples, defaults, selected ITE branches, exact arithmetic, deep DAGs, signature lifetime, quoting, definition chains, purification aliases, cyclic UF equations, input-order changes, and forced arena growth. The latest and historical witnesses must receive external printed-model validation; internal checking does not independently verify the evaluator or serializer. Four preprocessing modes and nondefault solver paths are part of the profile matrix.
Each migrated logic receives a fresh merge gate. Closure requires at least
3000 generated cases per affected logic, applicable real-seed mutation gates,
independent validation of produced SAT models, no unresolved correctness
failures, and resource limits separately reported. Heavy verification runs on
the requested SLURM route. Exact commands, commits, profiles, job IDs and
artifacts belong in docs/run-log.json. Local checks include make check,
make asan-test, make lint, make docs, and static review. Reconstruction and
validation timing must be distinguished from solve timing. No performance
promotion is implied by this redesign. :time.model.reconstruction-us measures
the shared adapter/replay builder; :time.model.check-us and
:time.model.serialization-us measure checking and printing separately. Mixed
component preparation retains its existing theory/model-separation counters.
For a host without LLVM review tools, make contrib-clang-tools installs pinned
clang-tidy/clang-format 18.1.8 in contrib/local/clang-tools-env. The remote
preparation helper can provision this optional target with
--contrib-target contrib-clang-tools --reuse-dependencies. The static-review
batch script checks every non-generated C++ source in the prepared tracked
snapshot and records its manifest, rather than relying on remote Git metadata.
The optional installer also snapshots the build compiler’s C and C++ header
search roots for compute nodes without development headers. Review adds those
roots in compiler order to the existing compile database; it does not rebuild
the solver with the compute node’s potentially different compiler. The first
static-review attempt exposed that environmental mismatch before any analysis
ran. Local CTest/ASan and independent SLURM solver gates remain separate checks.
The final whole-tree scan (99 non-generated C++ files, SLURM 134747) completed.
Review removed four unused non-paired headers and added the direct model-error
header. One analyzer leak warning was adjudicated as retained shared_ptr
ownership in Goal, not a discarded allocation. Paired-header checks and
180 unrelated missing-direct-include suggestions remain advisory baseline debt;
the scan is not described as warning-free. Details and raw logs are indexed in
docs/run-log.json.
The mutation gate accepts PRIMO_MUTATION_TOOL=opfuzz with Primo’s
scripts/fuzz_configs/model_linear_ops.txt. This profile changes Boolean and
order operators and affine arithmetic without introducing new terms, binders,
variable divisors, or nonlinear products. Unrestricted typefuzz remains useful
for boundary testing, but its out-of-fragment and ill-scoped mutants must be
classified separately; they are not model-validation passes. Both profiles
check every original seed first and send identical solver options to answer
comparison and independent printed-model validation.
For the supported fork, scripts/seeded_opfuzz.py seeds Python’s random generator
before executing the installed OpFuzz launcher, since only TypeFuzz exposes a
native --seed flag. Every retained mutant also receives a fresh answer check
through check_solver_seed_corpus.py: YinYang’s handling of generic nonzero
solver exits is not sufficient as the gate’s acceptance oracle.
FuzzSMT’s Real generator can emit products of constant expressions that Dolmen
rejects under its strict linear grammar. The semantic revalidation gate retains
the exact assertions and models but uses Dolmen’s
--force-smtlib2-logic=QF_NRA, a semantic superset, to check their truth. This
does not establish strict QF_LRA syntax conformance. The separate
slurm_typed_lra_reconstruction_gate.sbatch uses the conforming Boolean/affine
generator across all preprocessing modes and explicitly tests monotone replay.
Visible signatures in retained sessions#
core::Model optionally accepts explicit visible SymbolIds and owns that list
alongside its signature metadata. The original constructor behavior is preserved
when the list is omitted. Explicit lists reject duplicate, invalid or internal
IDs and require every listed symbol to have an interpretation. The printer emits
only listed symbols. Historical signature entries remain addressable by ID, so
retained snapshots survive pop, redeclaration and reset. The assumptions engine
projects completed interpretations onto active symbols and internal auxiliaries;
auxiliaries support original-assertion checking and are never printed.
For an explicit visible signature, ComponentModelBuilder completes visible
symbols and symbols required by retained atom interpretations. It omits unused
popped declarations. Dense construction slots preserve SymbolId indexing, and
unpopulated slots are removed before constructing the immutable model.
The shared builder reports :model.congruence.disequality-checks. Inserting a
disequality checks only the new pair after endpoint registration finishes;
any merges during registration still scan all older pairs. This avoids quadratic work
for pure disequality batches while preserving conflict reasons and rollback.
The proof checklist and native model replay evidence are in
Assumption-based incremental QF_UF.