SMT-LIB lexical conformance audit#
Direct-stream review findings (2026-09-10)#
Attribute-validation repair checklist#
The repair implements the generic attribute grammar from SMT-LIB 2.7
Sections 3.4 and 3.6, not new semantics for named terms, patterns or proof options.
smtlib::is_attribute_value accepts special constants, symbols and arbitrary
S-expression lists, but not bare keywords or reserved tokens.
smtlib::valid_attributes checks a nonempty span of keyword/optional-value
pairs in one forward pass, without allocation, recursion or stored borrows.
Successive keywords are successive valueless attributes. List values are opaque
S-expressions, not nested terms requiring annotation validation.
The command parser enforces exactly one attribute in set-info and set-option;
the frontend checks an annotation’s nonempty attribute sequence before building
its body. AST storage remains owned by the command/session; the validation
helpers are pure queries and cannot invalidate it. Existing unsupported
attribute semantics remain unsupported/ignored as before; this change validates
generic syntax only. Tests cover both reported failures, valueless and multiple
attributes, all value categories, quoted names, nested list metadata, deep input,
definition bodies, and live stdin.
The stdin refinement in Incremental SMT sessions removes the separate framing pass and eager parser lookahead. Guarded-buffer and live-pipe tests establish that a completed command returns without asking for another input byte. The shared lexer retains the selected symbol/numeric policy; it is not a claim of complete command/term grammar validation.
Two additional P2 validation gaps were reproduced on the prior 8e9dc59
binary and the streaming candidate e24dfcd. The attribute repair above fixes
both; they are not approved extensions:
Fixed gap |
Reproducer |
Validation owner |
|---|---|---|
Invalid metadata attribute value accepted |
|
|
Malformed term annotations ignored |
|
Frontend annotation dispatch: validate the required nonempty sequence of attributes before translating the body. |
SMT-LIB 2.7 Sections 3.4 and 3.6 specify attribute values and annotated terms.
These are invalid-input acceptance issues, not a demonstrated wrong answer on
valid input. Review fixtures are original and retained separately in
benchmarks/lexical-conformance/online-review-cases.json; the audit now requires
both to be rejected. They are not added to policy exceptions. The original
streaming qualification remains historical evidence for that earlier revision.
scripts/generate_attribute_cases.py supplies a separate deterministic gate of
200 original attribute cases (100 valid and 100 invalid, balanced between
metadata and annotations), independently syntax-checked by Dolmen. It does not
use Dolmen as a SAT/UNSAT oracle. Fresh qualification is recorded under
2026-09-10-attribute-validation-repair in docs/run-log.json.
The repaired source revision 15f4929 passes 31 CTest and 143 Python tests,
31 ASan tests, lint and the documentation build. Its SLURM correctness gate
rejects both original regressions, passes all 200 attribute cases with Dolmen,
matches the recorded Z3/cvc5 answers for 1,000 generated sessions, and passes
100 live-pipe chunking seeds (400 answers, 300 malformed-EOF rejections and
300 malformed-attribute rejections). The direct Dolmen quoted-name model smoke
also passes. No reference solvers or speed benchmarks are rerun. Configured
clang-tidy and include-cleaner complete on all 109 tracked C++ files, with no
new advisories; existing baseline warnings remain recorded in the same entry.
Other limitations remain explicit: lexical errors have absolute byte offsets, many command-shape errors still use offset zero, UTF-8 validity is not checked, stream-buffer EOF does not distinguish every device-level I/O failure, and there is no recovery after a malformed command. The solver rebuild baseline, declaration scopes and dedicated QF_UFLRA implementation are unchanged.
Online coverage inspected#
No external solver test, grammar or implementation was copied into Primo.
All 55 cases in benchmarks/lexical-conformance/cases.json are original inputs
derived from the standard. The following public collections identify useful
coverage categories:
cvc5 at
f9e5d3c312d9476763dc0afa85ac637da5907d99: quoted definitions, non-ASCII quoted metadata, and quoted datatype constructors. Datatypes are outside Primo’s supported surface; that test was inspected, not ported or presented as supported coverage.Dolmen at
5c9161a691872cee9ed0c58fe820a8b0b9e485ab: quoted reserved words and invalid characters inside quoted symbols, with expected diagnostic files. Its parsing tree also has many malformed identifier, attribute and binder cases. These are parser/type-checker tests, not an answer oracle.David Cok’s jSMTLIB tutorial, Section 6.1.5, describes a solver validation suite using scripts and expected responses, with error occurrence rather than exact diagnostic text compared. The current repository also publishes an SMTTests suite, including token acceptance/rejection, declaration, definition,
let, scope and version-specific groups. Its README distinguishes Java type-checker tests from tests forwarded to real solvers. We inspected the documentation and inventory, not executed its full JUnit suite; neither the historical tutorial nor its current golden files replace standard-derived expectations.
Inspection of Griggio’s generic parser#
The user-provided FBK parser
ships as smtlib2parser-1.4.tar.gz (MIT; copyright 2010–2013). We inspected its
Flex lexer, Bison command entry, stream wrapper, parser interface and outer
command loop. The Yices sample solving backend was not inspected or executed.
Useful structural observations from the downloaded source:
Quoted-symbol lexing returns a symbol token containing the interior name; the pipe delimiters are not part of backend symbol identity.
The scanner uses separate token categories for symbols, numerals, strings and keywords instead of treating every non-parenthesis token as a name.
The Bison entry accepts one command explicitly. The outer loop retains the scanner and calls it again; backend operations are callbacks.
The file stream reads from
FILE*. Scanner refills stop at a closing parenthesis instead of insisting on filling a large block. This is input chunking, not a second parser of complete command text.
Those mechanisms are inspection findings, not a verified live-pipe test or a proposal to copy the implementation. They illustrate that parser generators can support a command-at-a-time interface.
It is not a current lexical conformance oracle: its string state recognizes backslash-escaped double quotes rather than the current doubled-quote syntax; its quoted-symbol state admits backslash and control characters; its numeral rule permits leading zeros; comments are LF-terminated. The archive contains six sample backend inputs, not a demonstrated exhaustive conformance suite. The download hash and inspection scope are recorded in the run log.
Reproducible multi-solver test#
python3 scripts/check_smtlib_lexical_conformance.py \
--out results/lexical-conformance-new
# Enforce the selected Primo policy and compare generated standard inputs:
python3 scripts/check_smtlib_lexical_conformance.py --policy \
--mutations 1000 --seed 0 --only primo z3 cvc5 \
--out results/lexical-policy-new
The driver runs Primo, Z3, cvc5 in ordinary and strict modes, Yices, OpenSMT,
and Dolmen as a strict parser/type checker. It requires all selected binaries,
never installs tools implicitly, and supports --only for focused reruns.
Each input is executed as a file with a five-second timeout; Primo also receives
--max-theory-conflicts=1000. The output folder
must be new. Raw byte-preserving inputs/stdout/stderr, executable versions and
hashes, exact commands, all query answers and a matrix are retained. This is
not an interactive liveness, model validation or speed-measurement gate. The
optional mutation mode is a focused parser/binding fuzz gate, not a broad theory
campaign. It generates standard inputs across five logics, varying quoted names,
internal-looking user names, macro capture, parallel binding and scope replay.
--policy preserves each normative outcome but additionally recognizes the four
selected extensions for Primo. Fixed reference nonconformance remains visible
without failing the Primo policy gate. Every generated case must pass all selected
profiles; no reference majority voting is used. Original fixtures and the old
matrix/summary remain unchanged as historical evidence.
The classifier checks errors even after an answer, distinguishes crashes and unexplained nonzero exits from syntax rejection, and requires every expected query answer. Dolmen acceptance counts only as parsing/type acceptance. These small fixtures avoid arbitrary user diagnostics; the classifier is not a general SMT-LIB response parser. A nonzero audit exit is expected while the documented conformance findings remain; no failures are hidden with xfail. Normal unit tests exercise the classifier without requiring external solvers.
The historical local run has 34 valid cases, 19 invalid cases and two explicitly
diagnostic cases (solver-reserved .17 and duplicate definition parameters;
the latter is not assigned the separate let distinctness rule without a
stronger specification justification). There are 385 executions, no crashes
or timeouts, and unchanged executable hashes.
Profile |
Valid answer passes / 34 |
Valid rejected |
Wrong answers |
Invalid rejected / 19 |
Invalid accepted |
|---|---|---|---|---|---|
Primo |
21 |
11 |
2 |
3 |
15 |
Z3 5.0.0 |
32 |
2 |
0 |
13 |
6 |
cvc5 1.3.4 |
32 |
2 |
0 |
16 |
3 |
cvc5 1.3.4 strict |
33 |
1 |
0 |
16 |
3 |
Yices 2.7.0 |
32 |
2 |
0 |
16 |
3 |
OpenSMT 2.9.2 |
31 |
2 |
0 |
11 |
8 |
Primo’s remaining invalid case reports unsupported rather than a syntax error. OpenSMT’s remaining valid case produces a malformed response prefix before UNSAT, so it is a response mismatch, not a wrong Boolean answer. Dolmen 0.10 accepts all 34 valid cases and rejects 17/19 invalid cases; it does not solve the formulas. The committed matrix retains all exceptions and diagnostic rows.
The historical Primo binary returned SAT in both quoted/unquoted shadowing cases
versus UNSAT in every reference solver. All references resolved the valid alias lookup cases. However,
even cvc5 strict accepts backslashes inside quoted symbols, whereas Yices and
Dolmen reject them. All tested solvers reject the valid CR-only comment case;
Dolmen accepts it. Every profile accepts the invalid duplicate-let case.
Thus a unanimous vote does not establish language conformance.
Repair qualification#
The first repair probe (results/lexical-fix-initial) passes all 34 valid cases,
rejects 15 forbidden cases and accepts the four selected extensions. This probe
precedes the explicit internal-symbol metadata repair and is not its qualification.
The committed C++ tests cover both wrong answers, AST category copies, rejected
leading zeroes (including decimal integer parts), reserved-word declarations,
literal/name distinctions, empty symbols, parallel duplicate binding and replay.
Model tests reparse printed definitions and refute violations, including names
that resemble internal proxies; the live-pipe test fragments doubled quotes and
uses CR-only comments.
Clean-commit qualification at 8e9dc59 runs 1,000 generated sessions on SLURM
against Z3 5.1.0 and cvc5 1.3.4: all 2,000 answers per solver match, with zero
generated-case wrong answers, crashes, rejections, unknowns or timeouts. The
fixed Primo corpus has 34 valid passes, 15 policy-required rejections, four
extension passes and two explicitly diagnostic outcomes. The tricky-name model
fixture passes Dolmen directly.
ANTLR removal at 6e4ff80 changes no owned parser/solver C++ sources. The final
binary replays all 1,000 identical generated sessions and the 55 fixed cases;
the 2,000 saved reference results are reused only after verifying input hashes,
successful exits and identical answer sequences. No reference rerun is needed.
The final binary’s model also passes Dolmen directly. Its make check passes
31 CTest and 140 Python tests; make asan-test passes 31/31. Compile/link commands
contain no ANTLR dependency. The committed repair-summary.json and run-log entries
distinguish this final local replay from the SLURM comparison and static review;
none of these runs is a performance measurement or a full-corpus evaluation.
Whole-source clang-tidy and include-cleaner finish on all 109 tracked C++ files
in SLURM array 141931. There is no new blocking finding, but this is not a
warning-free baseline: the existing tactic shared_ptr advisory and include
backlog remain, with a known <algorithm> provider false positive in the lexer.
The original combined job 141930 timed out during static analysis, after its
solver/model phases passed; the continuation repeats only static checks.