Skip to content

feat(Boole): nat/pos as first-class types with UF+axiom SMT encoding#6

Open
kondylidou wants to merge 27 commits into
strata-org:mainfrom
kondylidou:nat-binary-datatype
Open

feat(Boole): nat/pos as first-class types with UF+axiom SMT encoding#6
kondylidou wants to merge 27 commits into
strata-org:mainfrom
kondylidou:nat-binary-datatype

Conversation

@kondylidou

@kondylidou kondylidou commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds nat and pos as built-in Boole types. No imports or declarations
required — every #strata program Boole; block has them in scope automatically.

The binary-nat algebraic datatype (pos with constructors xH/xO/xI,
nat with N0/Npos) is emitted as SMT declare-datatypes with a recursive
pos.toInt definition and bridge axioms (nat_nonneg, nat_fromInt_toInt,
nat_toInt_fromInt). The library is injected automatically when
coreProgUsesNatOrPos detects nat or pos anywhere in the program — procedure
headers, spec clauses, ite conditions, loop invariants, or quantifier bodies.
Nat operations (nat_toInt, nat_fromInt, nat_add, nat_sub, nat_mul,
nat_lt, nat_le) are available as surface-syntax operators in all Boole
programs.

Changes

  • StrataBoole/Grammar.lean — nat/pos type categories and operator surface
    syntax
  • StrataBoole/Nat.lean — natLibrary SMT preamble (algebraic datatypes +
    bridge axioms)
  • StrataBoole/Verify.leancoreProgUsesNatOrPos detection; natLibrary
    injection; port of all Core grammar pattern matches to the new
    annots : Option MetadataAnn leading field (upstream breaking change)
  • StrataBooleTest/nat_native.lean — axiom-coverage battery (round-trip,
    add, sub, literal, monotonicity, injectivity)
  • StrataBooleTest/nat_counterexample.lean,
    nat_counterexample_extended.lean — counterexample display and
    validation tests
  • StrataBooleTest/nat_detection.lean — regression tests for ite-condition,
    loop-invariant, and spec-quantifier detection paths
  • lakefile.toml + lake-manifest.json — Strata dependency updated to
    feat/nat-binary-datatype

@kondylidou
kondylidou marked this pull request as draft July 8, 2026 15:22
kondylidou and others added 26 commits July 16, 2026 14:43
Add `StrataBoole.Nat` — a reusable binary algebraic nat library built on
the new `define-fun-rec` infrastructure in Strata Core:

- `pos` / `nat` algebraic datatypes (Coq-style binary positives)
- `pos.toInt` via structural recursion (@[cases]) → define-fun-rec
- `pos.fromInt` via int-valued recursion (decreases x) → define-fun-rec;
  replaces the previous opaque UF + 3 axioms formulation
- `nat.toInt` / `nat.fromInt` as non-recursive wrappers
- Three bridge axioms kept as E-matching hints
- Full arithmetic operator surface (add/sub/mul/div/mod/lt/le/gt/ge)
- `Strata.BooleNat.prepend` helper to inject the library into any program

Add `StrataBooleTest/nat_binary_datatype.lean` with Options A (unary),
B (binary inline), and C (via natLibrary); 38 VCs all pass including 4
new termination obligations for `pos.fromInt`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds test_sat_exists_sum and test_sat_lt to Option B — both satisfiable
but not universally true. cvc5 returns unknown (expected: the three bridge
axioms are universally quantified and block concrete model search).
Locked in via #guard_msgs as regression documentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
define-fun-rec gives no observable difference through Strata's current
verify interface — both approaches return unknown for SAT queries.
The algebraic datatype is the actual fix; the Strata PR is being closed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After `import StrataBoole.Nat`, every `#strata program Boole;` block
automatically has pos, nat, nat.toInt, nat.fromInt, bridge axioms, and
arithmetic operators in scope — no explicit declarations needed.

Changes:
- StrataBoole/Nat.lean: add `import StrataDDM.Integration.Lean.HashCommands`
  and `#register_preamble Boole Strata.BooleNat.natLibrary` to register
  the binary-nat library as the Boole dialect preamble.
- StrataBooleTest/nat_binary_datatype.lean: remove `import StrataBoole.Nat`
  (Options A/B declare their own nat, which would conflict with the preamble);
  remove Option C (moved to nat_native.lean). Update obligation IDs.
- StrataBooleTest/nat_motivation.lean: remove `import StrataBoole.Nat` so
  unary/binary examples aren't shadowed by the preamble. Update obligation IDs.
- StrataBooleTest/nat_native.lean: new test demonstrating preamble injection —
  procedures use nat/pos without declarations; also verifies natLibrary directly.
- StrataBooleTest.lean: add nat_native import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the incremental SMT solver returns `unknown (some m)` for a
nat-using obligation, evaluate the obligation expression against the
candidate model with pure-Lean nat arithmetic. If evaluation yields
a concrete Boolean, promote `unknown (some m)` to `.sat m`.

Adds `LeanPos`/`LeanNat` mirror types (deriving Inhabited so `partial`
compiles), a two-pass SMT-term parser (`termToLeanPos`/`termToLeanNat`),
and a purely structural evaluator (`evalExpr`) that uses `EvalVal.VPartial`
for curried binary operators — avoiding the kernel-rejected non-positive
occurrence that `VFn : (EvalVal → Option EvalVal) → EvalVal` would create.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The proposal specifies `requires nat.toInt(b) <= nat.toInt(a)` for
nat.sub, which generates a proof obligation at every call site. The
previous implementation silently saturated to N0 when b > a (Lean's
Nat.sub semantics), which is a semantic deviation from the proposal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The batch-mode candidate-model fix (SMTUtils: parse get-value output
on unknown) lives on kondylidou/Strata until the PR is merged to
strata-org/Strata main. Switch back to the org git+rev once merged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add nat validate-candidate phase:
- EvalVal.VPartial replaces VFn (avoids non-positive occurrence)
- evalExpr handles curried binary ops via VPartial
- natCandidatePhase promotes unknown(some model) → sat when evalExpr succeeds
- Pass natBridgeAxioms to Core.verify as requeryDropAxioms so unknown
  obligations are re-queried without bridge axioms to expose counterexamples
- nat.sub changed to requires-guard precondition (nat.toInt(b) <= nat.toInt(a))

Also update lake-manifest.json to pin Strata fork branch
fix/batch-unknown-candidate-model (batch unknown model parsing + requeryDropAxioms).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two features completing the proposal's counterexample workflow:

1. Model display (Task 1): add lExprToLeanNat/lExprToLeanPos decoders for
   LExpr constructor terms, plus decodeModel post-processing in verify so
   that candidate counterexamples show e.g. `b = 2` instead of
   `b = Npos(xO(xH))`.

2. Counterexample battery (Task 2): add nat_counterexample.lean with two
   tests covering the SAT direction — a simple false claim and a constrained
   sum — both now produce `❌ fail` with concrete nat models.

3. Validation fix: evalExpr now accepts an ANF definition map built from
   the path-condition varDecl entries, so that ANF intermediates like
   `$__anf.2 = nat_toInt(a) < nat_toInt(b)` are expanded before
   evaluation. Without this, validateNatModel always returned false (the
   obligation was an opaque ANF variable absent from the solver model).

4. Core fix (in fork): dischargeObligationIncremental was querying boolean
   assumption IDs instead of actual user-typed variables; fixed to build
   SMT terms from vars + typedVarToSMTFn + estate.ufs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ttery

- coreProgUsesNatOrPos now scans .type (.data block) declarations so that
  user-defined datatypes with nat-typed constructor fields (e.g. NatList)
  correctly trigger natLibrary injection; previously only funcs/procs were
  checked, causing "Undefined type 'nat'" at TypeFactory validation

- validateNatModel now also checks that no ground path-condition assumption
  evaluates to false against the candidate; rejects broken partial assignments
  from timed-out solver sessions (e.g. candidate a=N0 contradicts requires
  toInt(a)=73) so they don't surface as spurious counterexamples

- nat_counterexample: update test 2 (constrained sum) expected output to
  ❓ unknown and document the conservative-soundness reason

- nat_counterexample_extended: new test file with 4 additional tests:
  range constraint (❌ fail), IntList head-zero (❌ fail), nonlinear mul
  (✅ pass), and NatList with nat-typed constructor fields (✅ pass, 13 obligations)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
evalUnaryOp: add pos.fromInt → VPos case; previously expressions calling
pos.fromInt in path conditions produced VPartial, silently accepting
inconsistent candidates.

evalBinaryOp nat.sub: guard precondition (toInt(b) <= toInt(a)); previously
underflowing subtraction clamped to N0 via natFromInt, promoting candidates
that violate nat.sub's requires as valid counterexamples.

evalExpr .eq: return none (not Some false) on type mismatch; the old
catch-all `_, _ => false` caused groundAssumptionsHold to reject genuine
counterexamples when one operand evaluated to VPartial.

evalExpr .fvar: erase defMap key before recursing to break circular
definitions x ↦ fvar x that would otherwise loop indefinitely.

validateNatModel obligationOk: require VBool result, not just isSome;
VPartial (partially-applied operator) is not a usable evaluation result.

decodeEntry: try lExprToLeanPos after lExprToLeanNat fails so that
pos-typed model variables are decoded to integers, not left as raw
constructor terms.

verify: cache coreProgUsesNatOrPos result to avoid traversing the program
twice (once for injection guard, once for usesGrammarNat).

Nat.lean: correct three comment blocks that falsely claimed pos.toInt and
pos.fromInt are emitted as define-fun-rec; they are UF + per-constructor
axioms in the current Strata encoder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ogUsesNatOrPos

Previously, a procedure with `var z : nat := ...` in the body but no
nat/pos in its header signature would silently skip natLibrary injection,
causing a compilation error. Now stmtListHasNatOrPos recursively checks
Cmd.init type annotations across structured statement lists (including
nested ite/block/loop bodies), covering deviation 4 from the proposal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- evalBinaryOp: add nat.div / nat.mod arms (silent accept on zero divisor)
- validateNatModel obligationOk: tighten VBool _ → VBool false so
  P=true does not count as a counterexample
- lExprUsesNatOrPos: new helper scans operator ids and sub-expressions
- coreProgUsesNatOrPos: scan function/recFuncBlock bodies and proc
  spec preconditions/postconditions via lExprUsesNatOrPos
- usesGrammarNat: remove !hasUserNatDecl guard so prepend-based
  programs still get natCandidatePhase and natBridgeAxioms
- decodeModel: guard with usesGrammarNat (maybeDecodeModel) to skip
  the nat model decode pass when no nat types are in use

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Points to feat/def-fun-rec commit that fixes the re-query axiom existence
check: guard now reads axiom names from program.decls (the original input)
instead of oblProgram.decls (which never has .ax declarations after
toCoreProofObligationProgram strips them). Resolves the false warning
"none of [nat_nonneg, ...] matched any axiom declaration" that was
preventing the re-query from running for nat_sum_prog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three correctness fixes to the nat/pos detection pass and one manifest bump:

1. lExprUsesNatOrPos: add .abs and .quant cases so nat ops inside
   quantifier bodies (forall n : nat ::) and lambda expressions are
   not invisible to natLibrary injection.

2. stmtListHasNatOrPos: scan assert/assume/cover/set expression content,
   not just local variable type annotations. Procedure bodies that assert
   nat arithmetic were silently skipping natLibrary injection.

3. lake-manifest.json / lakefile.toml: bump Strata dependency to
   feat/def-fun-rec @ c4833a31 which fixes the re-query pass to actually
   strip bridge axiom assume statements from obligation procedure bodies
   before the axiom-free re-query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
stmtListHasNatOrPos was silently dropping nat ops in:
- .ite condition (ExprOrNondet .det e)
- .loop guard, termination measure, and invariant expressions
- .funcDecl body, axioms, and measure

lExprUsesNatOrPos was already complete after the previous commit
(.abs and .quant added); the nine LExpr constructors are now all handled
with no unintended wildcard falls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests

Add `nat_axiom_battery` to `nat_native.lean` (6 procedures covering round-trip
2, add/sub correctness, ground literal, monotonicity, and toInt-injectivity) and
a new `nat_detection.lean` with 3 regression tests for the detection code paths
fixed on this branch:

* `test_ite_cond` — nat ops appear only in an ite condition, exercising the
  `.ite (.det c)` case added to `stmtListHasNatOrPos`
* `test_loop_inv` — nat ops appear only in a while-loop invariant, exercising
  the `.loop _ _ invs body` case
* `test_spec_quant` — nat ops appear only in a spec-ensures quantifier body,
  exercising the `.quant _ _ _ _ tr b` case added to `lExprUsesNatOrPos`

If any of these detection fixes is reverted, the corresponding test fails
because natLibrary is not injected and the `nat` sort is undefined in the SMT
encoding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop unused `dummy : int` from `test_literal` and `test_ite_cond`, and
remove the tautological `assert 0 <= dummy || 0 > dummy` body (with its
accompanying `dummy` parameter) from `test_spec_quant`, replacing it with
an empty body `{}`.  Update all downstream obligation IDs that shifted as
a result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kondylidou
kondylidou force-pushed the nat-binary-datatype branch from 7a5b9b1 to 74c4fc5 Compare July 16, 2026 12:46
Two upstream breaking changes required porting:

1. All Core grammar ops (`command_*`, `varStatement`, `initStatement`,
   `assign`, `assume`, `assert`, `cover`, `if_statement`, `havoc_statement`,
   `while_statement`, `call_statement`, `block_statement`, `exit_statement`,
   `typeDecl_statement`, `funcDecl_statement`, `command_cfg_procedure`,
   `command_typedecl`, `command_typesynonym`, `command_constdecl`,
   `command_fndecl`, `command_fndef`, `command_recfndefs`, `command_axiom`,
   `command_distinct`, `command_datatypes`) gained a leading
   `annots : Option MetadataAnn` parameter. All pattern matches in
   Verify.lean extended with a leading `_` wildcard. The `assert`/`cover`
   reachCheck field was replaced by annots; drop the now-invalid
   reachCheck metadata injection.

2. StrataDDM `@[noExponent]` arg-level format marker (needed by
   Strata/DL/SMT/DDMTransform/Parse.lean) and parenthesization of arrow
   types in type applications require StrataDDM >= 4ea13cd. Bump
   lake-manifest accordingly; update one guard_msgs docstring in
   stack_array_based.lean where ite-in-implication now prints with
   surrounding parens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kondylidou kondylidou changed the title feat(Boole): binary nat datatype with recursive int bridge feat(Boole): nat/pos as first-class types with UF+axiom SMT encoding Jul 16, 2026
@kondylidou
kondylidou marked this pull request as ready for review July 16, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant