Skip to content

HolRefute: Quickcheck, narrowing and a Nitpick-style Kodkod model finder - #2051

Open
lukaszcz wants to merge 601 commits into
HOL-Theorem-Prover:developfrom
lukaszcz:nitpick
Open

lukaszcz wants to merge 601 commits into
HOL-Theorem-Prover:developfrom
lukaszcz:nitpick

Conversation

@lukaszcz

@lukaszcz lukaszcz commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds src/HolRefute, a counterexample generator for HOL4 goals, built on Poly/ML only. It provides four diagnostic tactics (REFUTE_TAC, QUICKCHECK_TAC, NARROWING_TAC, MODEL_REFUTE_TAC), the SML entry points behind them, a user manual chapter (Manual/Description/Refute.smd), 33 help docfiles, a selftest with level-2 acceptance tables, theory-hygiene tests, and an executable example corpus adapted from Isabelle's Nitpick manual and example suites.

The library has four backends: exhaustive and random Quickcheck, native narrowing, and a Kodkod-based finite model finder. Substrates are untrusted accelerators; every counterexample is replayed and, where replay succeeds, turned into a HOL theorem. HolRefute leaves no type, constant, theorem or binding in the user's theory on any path (success, failure, timeout, interrupt); its only ambient effect is teaching the global EVAL compset closed :rat arithmetic and :real inv.

What is ported from Isabelle/HOL, and how closely

Model finder: a port of Nitpick

The model finder is a module-for-module port of Nitpick (Isabelle2025-2, src/HOL/Tools/Nitpick). The pipeline, the intermediate languages and the trust model are Nitpick's; the adaptation is concentrated in the HOL-facing layer.

Isabelle source HolRefute module Fidelity
nitpick_preproc.ML Refute_ModelFinder_Preproc Port: binarization, constructor destruction, specialisation, boxing, skolemisation. Traversal orders that fix variable numbering are preserved deliberately (rightmost-argument descent, conjuncts_of order, axiom-side skolemisation depth).
nitpick_mono.ML Refute_ModelFinder_Mono Port of the monotonicity calculus: mtypes, constraints, frames, SAT solving. Id and set products stay unfolded (no nut builtin), the Pure meta-connective cases, bounteous_consts and tracing are not ported, and is_harmless_axiom is narrowed.
nitpick_scope.ML Refute_ModelFinder_Scope Port of scope enumeration.
nitpick_rep.ML Refute_ModelFinder_Rep Port; relations are curried (HOL4 relationTheory) rather than sets of pairs, and the application boundaries are kept.
nitpick_nut.ML Refute_ModelFinder_Nut Port. HOL4 sets are functions and binders are named, so the environment records de Bruijn levels to keep Nitpick's BoundName convention. The nut datatype and its operator enumerations are unchanged; cst gains the word and char operations. One NatToInt totality bound is deliberately corrected against upstream and marked in the code.
nitpick_kodkod.ML Refute_ModelFinder_Kodkod Port of the FORL translation. Differences are confined to HOL4 semantics (Num of a negative integer, curried relations).
nitpick_peephole.ML Refute_ModelFinder_Peephole Port with three marked divergences (s_all/s_exist on an empty declaration, rel_expr_intersects on AtomSeq, s_join's Univ clauses) plus the HOL4-side bit-width choice.
nitpick_model.ML Refute_ModelFinder_Model Port of model reconstruction and display, plus the HOL4-only certificate hook.
nitpick_hol.ML Refute_ModelFinder_HOL Port of the definition-harvesting and unfolding layer; the largest adaptation. DefnBase/TypeBase presentations replace Isabelle's code equations and simps; typedef, quotient and codatatype registries and the ersatz table are re-derived from HOL4 theory data.
nitpick.ML (pick_them_nits_in_term) Refute_ModelFinder Port of the control flow. Type-variable merging is the sortless specialisation of merged_type_var_table_for_terms, since HOL4 has no sorts.
kodkod.ML Refute_Forl Port of the Kodkodi process bridge; drives the same kodkodi-1.5.7 component, without Isabelle's launcher.
kodkod_sat.ML Refute_ForlSat Port of SAT-solver selection, honouring the same *_HOME variables.
prop_logic.ML, cdclite part of sat_solver.ML Refute_PropSat Adaptation of Tjark Weber's BSD-licensed code, used by the monotonicity calculus.
Nitpick.thy refuteScript.sml Port of the ersatz definitions (unknown, wf', wfrec', card', sum', safe_The), with fmap rows added.
nitpick_commands.ML no counterpart Options are a config record with upd_* updaters and REFUTE_TAC_WITH, not Isar syntax.

The verdict model is Nitpick's: Genuine / QuasiGenuine / Potential is decided by the encoding, max_potential is charged as in Nitpick (only a genuine model spends a max_genuine slot, where Nitpick's sound branch also charges quasi-genuine ones), and Refute trusts Kodkodi exactly as Nitpick trusts Kodkod (a Genuine verdict with no certificate is valid). Deliberate deviations, all documented in the README and manual:

  • Hilbert choice is guarded per occurrence as if ?x. P x then @P else unknown where Isabelle leaves the occurrence unguarded and constrains it only through the witness-conditional Eps_psimp; a guard vetoes NoCounterexample.
  • NoCounterexample is a total claim. Anything bounds-relative reports Unknown with a reason, and a value-position unknown in any harvested axiom vetoes it.
  • Machine words of concrete width and :char are exact native carriers (2^w and 256 atoms), which turn smart binarization off. Nitpick has no counterpart.
  • real is rational-valued, as in Nitpick. Polymorphic goals are tested at configured monomorphic instances and never earn NoCounterexample, where Nitpick varies the type variables' cardinalities like any other type.
  • sat_solver = "smart" prefers a configured JNI or external solver; Nitpick's smart order never picks a JNI solver by itself.
  • HOL4 completes non-exhaustive patterns with ARB in the raw definition; only the user's equations are harvested, so the same spurious-counterexample risk as Nitpick applies.

Quickcheck: design ported, execution re-engineered

  • Exhaustive generators. Plan compilation is ported from exhaustive_generators.ML (the Refute_QC comment cites the upstream lines). Smart generators follow Isabelle's predicate-compiler design: Refute_SmartGen mode-checks Horn SCCs for positive and negated first-order modes and flattens function equations into graph clauses; upd_allow_function_inversion defaults off like Isabelle's flag, and upd_use_subtype matches use_subtype.
  • Random generators. Function values are drawn on demand as random_fun_lift does, but eagerly rather than lazily.
  • Execution differs. Isabelle compiles generators through the code generator to ML. HolRefute compiles a goal to a test-plan IR (Refute_Eval.plan) that two substrates run: in-process SML extraction (Refute_Extract) and computeLib. Both consume one PRNG in the same order, so a seed reproduces the candidate stream on either.
  • abstract_generators.ML becomes abstract_generator; find_unused_assms.ML becomes Refute_Unused (check_/find_/print_unused_assms), with the same maximal-droppable-set semantics.
  • The driver in quickcheck_common.ML is not ported: budgets, iterative size deepening, the backend pool and certainty ceilings are Refute_Core's own.

Narrowing: generators ported, engine native

The type representation (Narrowing_sum_of_products), finitize_functions and the quantifier-pulling pass are ported from narrowing_generators.ML. The Haskell engines (Narrowing_Engine.hs, PNF_Narrowing_Engine.hs) are replaced by a native SML narrowing engine (Refute_Narrow, Refute_QC_Narrow), so no Haskell toolchain is needed. Finite function inputs are ffun update chains defined in refuteScript.sml instead of Isabelle's type-scoped Constant names.

HOL4-only additions with no Isabelle counterpart

  • Certification (Refute_Cert, Refute_Cert_Narrow, Refute_Cert_Model): QC hits are replayed with computeLib, narrowing replays its case tree, and model-finder values are replayed through Skolem provenance, bounded synthesis, one-layer cases, single-property induction, Presburger and REAL_ARITH. Replay is fail-closed and never weakens the encoding's certainty.
  • Concurrent backend pool with one absolute deadline per call (ParList, below).
  • Theory hygiene: all runtime definitions are bracketed in a theory snapshot/revert; theory_tests/ checks that a descendant theory inherits nothing.
  • Custom generator families (register_generator_family, fmap), Refute_EvalFmap for ground finite-map equality, and the rat/real compsets.

Changes outside src/HolRefute

src/portableML/poly/concurrent/ParList.{sig,sml}, selftest.sml, Holmakefile (new)

A small racing/mapping combinator library over Poly/ML threads: map_with_workers, get_some_with_workers, get_some, get_first, and uninterruptible_wait.

  • Why it is needed. Refute_Core runs backend admission and the backend race through it (map_with_workers for admission, get_some_with_workers for the concurrent pool, get_first for upd_sequential). Refute_QC uses uninterruptible_wait so the theory-revert cleanup after an interrupted run still completes.
  • Why it lives here. Multithreading, Future, Timeout and now ParList are built by the kernel band with --poly_not_hol and linked into sigobj. Naming this directory in HolRefute's INCLUDES would rebuild it with the overlay in scope and create an Sref -> Overlay dependency that breaks the next kernel bootstrap in src/bool (documented in HolRefute's Holmakefile).
  • What it fixes that nothing in the tree offered. Thread_Attributes.uninterruptible clears the broadcast flag, and HOL's REPL delivers Ctrl-C as Thread.broadcastInterrupt, which Poly/ML drops rather than defers for a thread not accepting broadcasts. A masked join therefore swallowed Ctrl-C. ParList masks with InterruptDefer instead and keeps joins observant. Workers are interrupted, never Thread.killed, since a killed worker can leak the process-global theory mutex. Directed-interrupt tests cannot see any of this, so selftest.sml drives the masked windows through ParList_Test hooks. The Holmakefile change only wires that selftest in under HOLSELFTESTLEVEL.

src/IndDef: KeyedThmSet and the [coinduction] attribute

  • KeyedThmSet.{sig,sml} (new). The ThmSetData-backed "theorems keyed by the constants their clauses talk about" machinery that IndDefLib implemented inline for rule_induction is extracted into a factory parameterised by which clause part carries the key. IndDefLib now instantiates it with Hypothesis; the exported names, set type and map type are unchanged, so nothing downstream changes. What does change: malformed theorems get a diagnostic naming the set type and the part inspected, and non-implicational theorems are diagnosed instead of escaping as a bare dest_imp failure.
  • CoIndDefLib. Instantiates the same factory with Conclusion, exporting coinduction_map, thy_coinductions, add_coinduction, export_coinduction, and registers the coinduction theorem attribute. Hol_coreln now exports its _coind theorem automatically, as Hol_reln already did for _strongind.
  • Why HolRefute needs it. Nitpick classifies each inductive constant as an lfp or gfp to decide how to unfold and iterate it. Refute_ModelFinder_HOL answers that from IndDefLib.rule_induction_map and CoIndDefLib.coinduction_map (is_registered_gfp, registered_stem). Without a persistent coinduction registry, coinductive relations could only be recognised in the session that defined them.
  • selftest.sml, Holmakefile. Tests for both key parts' diagnostics, the non-implicational case, the stored-attribute path, and a persisted well-formed export. The Holmakefile adds KeyedThmSet.uo to the selftest link line.
  • Manual/Description/modern-syntax.smd. Documents coinduction in the theorem-attribute table.

src/coalgebras/pathScript.sml, selftest.sml, Holmakefile

pathTheory had constructors, path_cases, injectivity and distinctness theorems but no case constant and no TypeBase entry. This adds path_case with its compute/simp equations, case_cong, case_eq, case_elim, a case overload so case p of stopped_at x => ... | pcons x r q => ... parses, and a TypeBase registration using path_bisimulation as the induction principle.

  • Why HolRefute needs it. HolRefute's built-in codatatype registry (llist, ltree, itree, itreeTau, lbtree, path) validates each entry through its case constant and TypeBase.constructors_of; path could not be registered without both. The selftest's model-finder table exercises path injectivity, distinctness and bisimulation.
  • Side effect for other users. path now behaves like the other coalgebraic types under case syntax, EVAL and the simplifier. The coalgebras selftest gains simp and case-syntax checks and a TypeBase registration check. path_11 stays local since its two conjuncts are already exported.

src/parallel_builds/core/Holmakefile

Adds ../../HolRefute to INCLUDES under POLY, for every kernel. This is what pulls HolRefute into bin/build; no sequence file changes. HolRefute builds under --otknl as well as the standard kernel.

AGENTS.md (new symlink to CLAUDE.md)

Lets agent tooling that reads AGENTS.md pick up the existing project notes. No content.

Manual/Description, help/Docfiles

A new Refute chapter (wired into chapters.txt, libraries.smd, and the generated-file .gitignore), and 33 Refute.* docfiles for the tactics, entry points, registries, config, and the unused-assumption probes.

Testing

  • HOLSELFTESTLEVEL=2 Holmake in src/HolRefute is the quality gate: the selftest (level 2 adds cross-substrate conformance, the narrowing table, the corpus and the model-finder acceptance tables), then theory_tests/. Model-finder rows need HOL4_KODKODI pointing at an unpacked kodkodi-1.5.7 and a Java runtime; without it they report inconclusive and the theory scripts still build.
  • Holmake examples in src/HolRefute builds the example corpus.
  • src/IndDef, src/coalgebras and src/portableML/poly/concurrent selftests cover the changes described above.

lukaszcz added 30 commits July 30, 2026 00:27
lukaszcz and others added 30 commits September 2, 2026 11:13
RAT_EQ_DECIDE_CONV is wired into the compset under the polymorphic [=]
key, which computeLib offers for equality at every type, and it decided
whether the redex was a rat only by failing inside RAT_CMP_STEP -- after
ratLib.RAT_CALC_CONV had already parsed, announcing an invented type
variable each time.

add_convs appends the conv after the rules the key holds when Refute
loads, but not after those a later Datatype adds, so in an interactive
session stepping through a script the conv really is reached first for
every datatype defined from there on.  Under the grammar ancestry a
script header installs, that parse guesses: e REFUTE_TAC on the hotel
example emitted 14454 "inventing new type variable names" messages, one
pair per candidate equality on the compute substrate.  Checking the
redex type before any work removes them, with the same counterexample
and the same 29630 candidates.

The pin sets Globals.interactive itself: bin/hol run selftest leaves it
false, and the parser announces a guess only when it is true, so without
that the check passes against the unguarded conversion as well.

Claude-Session: https://claude.ai/code/session_01XQTAPaX2Wnt2Bx5G7wVquU
A goal refuted by NARROWING_TAC, QUICKCHECK_TAC or MODEL_REFUTE_TAC was
not always refuted by REFUTE_TAC.  On the AVL rotation goal in
examples/refuteAVLScript.sml, Only [Narrowing] found the counterexample
in 0.7s while AllBackends returned Unknown after the whole 10s budget.
Two independent causes, load-bearing in disjoint modes.

No per-backend budget.  One search_context was built from the config's
timeout and every backend ran with its remaining time.  Each QC backend
deepens until search_expired, so whichever backend ran spent the whole
budget: in sequential mode exhaustive took all 10s and narrowing never
started.  run_backend now derives a context per backend.  Backends that
run at once each get the whole timeout; backends that run in turn split
what is left of it, a backend that exhausts or declines early donating
its remainder to those behind it, so a sequential call stays inside the
timeout.  That matters because the sequential users are the probe
sweeps in try_refute and Refute_Unused, which run one call per premise.

A globally serialized substrate.  The QC backends only looked parallel:
with_term_tables held table_mutex and with_native_hooks held
native_mutex for an entire test window -- and generated code wraps a
whole run in with_term_tables -- while a window was bounded only by the
deadline.  Acquisition was a trylock/sleep spin with no queue, so a
backend re-acquiring in a tight loop barged past a waiter polling every
10ms, which is why the loss was race-dependent.  Those locks existed
only because the state generated code reads was process-global under a
save-set-run-restore discipline, which is dynamic scoping.  The
dispatch slot, deadline, ignored-hit filter and term tables are now per
thread; native_mutex is gone, table_mutex covers only the registry
lookup, and compiler_mutex remains but spans only the compile.
reconstruction_forces stays process-wide: it is telemetry, and a
reconstruction may be forced on a thread other than the one that ran
the test.

Narrowing also reported "search exhausted" when it had finished no
depth at all, which reads as a completed search and helped hide this.

Ablation, each mechanism removed separately and re-measured: without
the per-backend budget the sequential rows return Unknown; without the
re-entrant substrate the parallel rows do.  Re-entrancy is what carries
the parallel case that every tactic uses; the budget carries the
sequential case, where backends never contend for the lock.

all_phases_share_one_deadline pinned the contract this replaces.  It is
split into three: admission still shares the call's deadline, a
parallel backend's deadline outlives it, and a lone sequential backend
inherits what remains.  Two pins are added for the mechanisms above.

Claude-Session: https://claude.ai/code/session_01XQTAPaX2Wnt2Bx5G7wVquU
Every goal mentioning MEM or IN was rejected by extraction, so narrowing
bailed out before depth 1 on all of them.  MEM x l is an overload for
x IN LIST_TO_SET l, and DefnBase.lookup_userdef is not obliged to hand
back a definition: for bool$IN it answers with GSPECIFICATION,
|- !f v. v IN GSPEC f <=> ?x. (v,T) = f x, whose second argument pattern
is GSPEC f.  Extraction took that for IN's defining equations, found a
non-constructor pattern, and refused -- with the constant's own name
nowhere in the reason, so the refusal read as a set-comprehension
limitation rather than as membership being unavailable.  The
("bool", "IN") intrinsic in the strict and lazy primitive tables
compiles membership as the application IN_DEF states, and the question
never reaches lookup_userdef.

The failure was silent in the worst way: NARROWING_TAC still prints a
report, just an Unknown one, in a few milliseconds.  All three
NARROWING_TAC calls in examples/refuteHotelScript.sml were no-ops for
this reason, and the AA-tree example's the_fixed_insert_inserts row was
reporting the bail-out where it now runs a search.

Two selftest rows had written the defect down as expected behaviour,
which is why nothing caught it.  "Refute native set-comprehension
refusal" asserted the exact string "non-constructor pattern: GSPEC f"
on a nub goal, and the full conformance matrix carried a NativeSML
inapplicability on the same goal.  Both are repaired against measured
behaviour, not relaxed: the conformance row now demands that all three
substrates agree, and the former row keeps a comprehension case, which
is still not executable but is refused in preprocessing ("not
executable: GSPEC") before extraction sees it -- so the GSPEC f pattern
refusal is now unreachable rather than merely unexercised.  Auto
fall-through keeps its own rows and no longer has this goal to measure
it, since native no longer falls through here.

Three pins are added: the intrinsic in each extraction mode, and the
end-to-end consequence, a MEM goal that Only [Narrowing] refutes with a
Genuine counterexample.  Ablation: all three redden with the intrinsic
removed.

The hotel example's feels_safe conjecture was carried as prose with its
search omitted, on the stated grounds that the adaptive defaults did not
produce a genuine counterexample.  Two separate things were wrong.  The
extraction gap above, and the model: hkey had three constructors, the
attack needs three check-ins, and each check-in issues a key not issued
before, so with K0 initially issued the conjecture was true there.  An
exhaustive forward search over hotel-valid traces found nothing at three
keys up to length 8 and a witness at length 5 with four.  Isabelle's
original uses four keys.  With K3 added and splits, no_checkin and
feels_safe defined, NARROWING_TAC refutes the conjecture under the plain
adaptive defaults, certified, at size 7 -- the size Isabelle's
quickcheck[narrowing] needs -- in 0.012s.

Claude-Session: https://claude.ai/code/session_01E1cvmgVowvtZmP3Q3dvEDu
Rewrite tests/selftest.sml (37.6k lines to 4.1k) so it exercises only
the Refute signature: outcomes, certificates, messages, configuration
and registrations.  Mechanism-level checks (internal modules, counters,
serializer and kodkodi transcript goldens, timing) are gone, and
tests/goldens/ with them.  Level 2 keeps the conformance matrix,
narrowing table, corpus, MF acceptance tables and the differential and
soundness suites as per-row tests.

Claude-Session: https://claude.ai/code/session_01SSJxCK2hPm7jmv2Jy9aqDQ
Drop every library binding nothing references (test-only accessors,
write-only telemetry counters, debug dumps, the Forl/Mono test
structures, unreachable PNF and policy-update paths), fold duplicated
helpers onto shared definitions, and simplify trivially equivalent code.
No user-visible behaviour changes: the Refute signature, defaults,
messages, candidate order and verdicts are untouched, and the level-1
and level-2 selftests plus theory_tests pass unchanged.

README shrinks to a developer orientation; the user documentation lives
in Manual/Description/Refute.smd.

Claude-Session: https://claude.ai/code/session_01SSJxCK2hPm7jmv2Jy9aqDQ
Seven entry points on the Refute facade had no consumer anywhere in the
tree: register_substrate, the four idempotent "restore the built-in"
registrations (register_frac_type_rat, register_frac_type_real,
register_function_display, register_fmap_display),
register_backend_with_ceiling, and lookup_term_postprocessor.  The
restore group only ever meant "I overrode a built-in and want it back",
a case Isabelle covers with unregister_* rather than with a restore
function per built-in.

Every registry stays.  The substrates register themselves from
Refute_EvalCompute, Refute_EvalCv and Refute_EvalSML; narrowing and
kodkod declare their own certainty ceilings; and Refute.sml installs the
Frac and display defaults directly through Refute_ModelFinder_Model.
Only the facade re-exports go, so no search behaviour changes.

The selftest keeps its backend-race ceiling test and its six
postprocessor assertions by aliasing those two entry points from their
own modules, and the CLAUDE.md rule confining the selftest to the Refute
signature now names that as its one exception.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
Rephrase every Refute.*.smd entry and Manual/Description/Refute.smd for
brevity, keeping the transcripts and every distinct rule.  Detail that
was duplicated verbatim between the reference entries and the manual
(upd_instantiate, upd_use_subtype, the codatatype witness shape) now
lives in full in the reference entry and in summary in the manual.

Corrections against the source: the depth field is smart-enumerator
fuel and is not read by narrowing; the Kodkodi launcher also needs
/bin/sleep; quickcheck and model_refute are refute_with over the typed
search selection; abstract_generator's pred is a guard on generated
values.

Claude-Session: https://claude.ai/code/session_01N8Z5QawKrRYRbMA82shvG1
Backends trace from their own workers, so two of them can be inside
HOL_MESG at once.  Feedback.MESG_outstream is a plain callback, and a
consumer that accumulates into a ref -- which is what the hook invites,
and what the selftest's capture helper does -- silently loses one of two
concurrent updates.  The symptom was a level-2 trace missing a backend's
"started" line while that same backend's Unknown reason still appeared in
the outcome: it ran, its line was dropped.  "trace 2 reports selection
and the race" failed non-deterministically on that.

Refute owns the worker pool, so it owns delivering one whole message at
a time.  Private.emit serializes every emission reachable from a backend;
say and the new warn go through it, as do the model finder's four direct
HOL_MESG/HOL_WARNING calls.  Refute_Forl keeps its own HOL_WARNING: it
depends on no other Refute module, and routing it here would invert that
layering for a Kodkodi error path.

Measured: an unsynchronized ref-accumulating consumer lost a line in
3 of 15 trials, a mutex-protected one in 0 of 15; the pin then passed
12 of 12 unchanged, so it needed no adjustment.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
Refute_Gen.narrowing_terms indexes a numeric kind's values by position
and flat_shape makes that position the alternative id, so an id denotes
the same value at every depth -- the invariant primitive_value already
relies on for the conversion side.  The reconstruction side tabulated it
instead, one arm per (depth, id).  For a char carrier that is 256 arms
per depth, 2816 across the default 0-10 window, twice over for the replay
reconstruction; flat shapes then spelled out another 256 nullary
alternatives per depth.  The generated program reached ~410KB and took
17s to compile inside a 10s budget, holding the global compile lock while
exhaustive and random -- 0.07s each on the same goal -- never ran.

Key those arms on the id alone.  Only a custom enumerator may hand back a
different list per depth, so only it keeps the depth in the key.  Flat
all-nullary shapes emit a List.tabulate of the identical list rather than
one line per value, keeping the program proportional to the window rather
than to the carrier.

Measured on examples/refuteExample07's string goal: 17.01s to 0.81s, and
repeated calls no longer degrade -- a second call previously exceeded 60s.
!s:string. s <> "x" now refutes in ~1s where it never finished in 60s.
The example's counterexample went from 20-30% missed to 20 of 20 inside
the default budget.

Regression pin: "narrowing refutes char and string goals".  Ablated by
reverting this file, both goals time out at the 10s default, 4 of 4.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
Quality-only pass, no intended behaviour change.

Four structural changes:

  - [certainty_ceiling] becomes a field on the [backend] record.
    [backend_registration], [register_backend_with_ceiling] and the
    dual path through [resolve_backend_registrations] are gone; there
    is one registration entry point again.
  - Everything a backend worker emits now goes through
    [Refute_Core.Private.emit].  That invariant was stated in a
    comment but three sites in Refute_ModelFinder_Model and the mono
    tracer called [HOL_MESG] directly.  [MFMono.trace_msg] holds the
    gate and the emission together and is exported so that
    Refute_ModelFinder reports through it too.
  - Depth stability is carried in the shape data, as [depth_stable]
    on [Narrowing_sum_of_products], instead of being re-derived by
    each consumer; every generator arm states its own answer.
  - Refute_Cert_Model's seven [enable_*] policy fields were true at
    every construction site, so they are deleted and their thirteen
    read sites folded.  [cases_allowed] and [induction_allowed] leave
    [failed_state] with them: as constants they contributed nothing
    to [same_state].

Smaller ones: [cleanup_timeout] is no longer a ref that nothing
assigns; the three copies of [add_reason] are one; the local
[list_compare] in Refute_ModelFinder_Scope no longer shadows
[Portable.list_compare]; Refute_SmartGen's write-only [degradation]
inference is deleted; Refute_Extract memoizes [spec_of] and
[exact_entries_of] per type and dedups entries through a Redblackset
rather than by comparing rendered SML source; [print_wf_cache] and
the scope-frontier trace are behind their trace level; and the
Holmakefile records the Refute_Core edge that holdep cannot see
through the structure alias.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
[Refute_ModelFinder_Mono.trace] is a bool ref that nothing in the tree
ever assigns, so the calculus's own tracing -- a per-term dump and the
final mtype assignment table -- could not be turned on.  Delete it,
along with what only fed it: [print_mcontext], [resolve_mtype],
[resolve_atom], [extract_assigns], [annotation_from_bools] and
[association_defined].

With the assignment table unread, [solve] answers satisfiability
directly instead of extracting one annotation per variable up to
[max_fresh] on every successful monotonicity check.

The driver's abandonment diagnostic is not debris and stays: it moves
to [Refute_Core.Private.say 2], which is where the rest of the model
finder reports and what the deviation comment in
[consider_definitional_axiom] already claims happens.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
[Refute_EvalSML.update_term], [Refute_Narrow.update_function] and
[Refute_ModelFinder_Model.make_update] were three spellings of
[Term.mk_comb (combinSyntax.mk_update (point, value), base)], and
[fun_term] inlined a fourth.  The definition moves to [Refute_Util],
which is the one module all three layers sit above;
[Refute_ModelFinder_Util] re-exports it like the other shared term
helpers, and [Refute_EvalSML] keeps the name because generated
extraction code calls it by that path.

Refute_Util gains combinSyntax, noted in its header: still no
Refute-layer dependency, which is what that charter is about.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
The global compset has no rule for = at :'a |-> 'b (finite_mapLib's
fragment covers FLOOKUP/FDOM/FUNION/DOMSUB only), so computeLib reduced
fm = fm' just for literally identical chains and left every other pair
stuck.  A Test whose conclusion is such an equality could therefore
never yield a genuine counterexample on Compute: each premise-satisfying
draw surfaced as "evaluation stuck during testing", including pairs of
permuted chains denoting the same map, which display identically.

Refute_EvalFmap hangs FMAP_EQ_DECIDE_CONV on the shared ("=", "min")
key, guarded by an fmap type test, like Refute_EvalRat's rat equality.
F comes from a key where computeLib decides the two FLOOKUPs unequal and
congruence under f = g; T from proving both chains equal to a
sorted-dedup canonical chain via FUPDATE_EQ / FUPDATE_COMMUTES.  Every
step is a proof, so undecidable cases still fail through and stay stuck.
Certification uses the same compset, so such hits now certify too.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
Thread_Attributes.uninterruptible clears the broadcast flag, so a Ctrl-C
arriving inside a masked region is dropped outright rather than held.
Three windows in ParList were affected: the fork/record pair, the mutex
acquisition inside Multithreading.synchronized, and the mandatory join.

uninterruptible_defer masks with Thread.InterruptDefer instead, leaving
the broadcast flag as the caller had it, so an interrupt landing in the
window is delivered once the caller's attributes come back.  with_lock is
synchronized without the tracing, built on the same primitive.

stop_threads now closes the job queue before interrupting the workers: a
job that answers its interrupt with `handle _` then costs the caller one
job rather than all the work that remains.

Directed-interrupt tests cannot see any of this -- directed interrupts
are retained across a plain mask, only broadcasts are lost -- so the new
selftest.sml drives the windows through the ParList_Test hooks.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
Fixes for the confirmed and verified-plausible findings of the final
review.  Each is ablation-validated: the mechanism is removed and the
named pin goes red, and red only there.

  * Refute_QC: an expired search no longer reports completeness.
    search_expired doubles as the sampling loop's termination condition,
    so the veto is taken at the point the loop already tests.
  * Refute_Cert_Narrow: a witness hole no longer diverts the existential
    case split; cover_variable spends from cover_budget instead.
  * Refute_EvalEnum: quiet_theory_work restores the message flags on the
    raising path as well as the normal one.
  * Refute_ModelFinder_HOL: a refute_simp clause is keyed on the head
    constant under a negation.  strip_imp_only reaches the head that a
    conclusion read as [~c = T] hides behind bool$~.
  * Refute_ModelFinder_Preproc: a binder shadowing an outer variable is
    not a static argument.
  * Refute_Extract: definition_theorem recovers a constant's equations
    from its home theory when a descendant's specification has shadowed
    the DefnBase entry -- pred_set's GSPECIFICATION hides bool$IN.

Three pins that this pass rewrote were unable to discriminate as
written: two asserted results unreachable for their goals, and one read
its baseline from the ambient global state the defect corrupts upstream.
They now set their own baseline and assert where the worlds diverge.

The Cv substrate is deleted.  NativeSML and Compute between them cover
every plan it compiled, so it earned no selection of its own; with it
gone the cv_compute automation changes it required revert to develop.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
Nothing in the library called Refute from inside a running search:
Refute_Unused drives its probes from the top level, and the only
exerciser was a selftest stub.  The capability nevertheless cost three
mechanisms, because a backend that re-entered would compile a second
substrate test inside the enclosing call's open theory bracket and then
wait on a theory lock its own caller holds.

refute_problem now refuses such a call where the caller still has a
stack to report it on, rather than supporting it.  The context token
already reaches backend workers, so the existing test catches them.

With that settled the theory bracket has at most one holder, and
bracket_depth, bracket_owner and holds_theory_bracket go: a second open
waits on theory_lock exactly as another thread's would.  The comment
that justified re-entrancy described Cv opening its bracket while
compiling, which stopped being true when Cv was deleted.

The selftest pin is inverted rather than dropped, and is validated by
ablation: restoring the old `run ()` branch reddens it and nothing else.
register_backend's reference entry and the Description manual gain the
restriction, since backend authors are the ones it binds.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
push_quantifiers_inward orders a cluster of like binders by a cost
model: quantifying a variable multiplies the combined size of the
components mentioning it by that variable's domain cardinality.  The
cost table was reversed relative to the list it was indexed by, so
each binder was charged its mirror's cardinality.

This is faithful to nitpick_preproc.ML, where T_costs is built
innermost-first (de Bruijn order) but indexed by the outermost-first
bound number that `flip` produces.  It is a deliberate divergence
from upstream, not an accidental one: the ordering is a heuristic
over logically equivalent formulas, so no answer changes either way,
but a search running under a deadline turns a bigger encoding into
Unknown where a smaller one would have found a model.  On a
size-balanced cluster the mirrored index picks the expensive binder
for the outermost position -- the worst available choice, and
symmetrically so, whichever binder is the expensive one.

The table now holds cardinalities rather than types, so
typical_card_of_type runs once per binder instead of once per query
inside the n! * n permutation search.  Upstream precomputes this
too; the port lost it when it moved from de Bruijn indices to terms.

Also split gather into a linear gather_rev plus one reverse, rather
than appending a singleton at every binder.

Claude-Session: https://claude.ai/code/session_01Lj7exoafZqUZjGQnskC47w
Correct the Description chapter, the Docfiles entries and the README
against the source, and trim prose that carried no information.

Corrections: sequential runs split the remaining deadline among the
backends not yet started; the certainty ceiling ends a sequential
search too; smart generators serve exhaustive testing only; an
upd_instantiate pin with type variables is rejected by the updater,
not reported as inconclusive; merge_type_vars merges into the
alphabetically first variable; every word constant outside the
encoded tier is refused, not a named list; sat_solver "smart" prefers
a configured native or external solver over SAT4J; four term
postprocessors are built in, not two; harvest_registrations sweeps the
ancestry plus the current theory; export_refute_psimp does not require
conditional equations, export_refute_unfold applies the same
constant-head rule, and the check runs when the tables are built; the
Hilbert-choice veto fires on guard insertion; a polymorphic goal only
forfeits NoCounterexample; register_generator drops the type's
abstract generator; unfolding lives in _HOL, not _Preproc.

Claude-Session: https://claude.ai/code/session_017ygKa1f65dGr4Rxpb8x2FA
# Conflicts:
#	src/IndDef/IndDefLib.sml
#	src/IndDef/selftest.sml
…docs

Every claim about how closely a module follows Isabelle's Nitpick or
Quickcheck was checked against the Isabelle2025-2 sources and the
kodkodi-1.5.7 component; the false ones are corrected, prose only.

Peephole is not a faithful port: three divergences are now named in
its header.  Mono's "complete M3 calculus" is upstream's monotonicity
calculus minus the Id/set-product rules, Pure cases, bounteous_consts
and tracing.  Nut's "bug-for-bug" FIXME annotated correct code; the
deliberate NatToInt bound correction is now marked instead.  Two
Kodkod "deviation" markers and one in Mono annotated code identical
to upstream.  The genuine-slot accounting, the two-round harvest cap
and the certification promotion are recorded as divergences rather
than parity.  Core's function-inversion comment denied a recogniser
that exists; QC's plan pseudo-code was mis-scoped and dropped a bound
subtraction; Preproc's branch stands in for upstream's Free and Var
cases; refuteScript's wfrec' port adds a WF guard and a different
fallthrough.  Narrowing, Forl and ForlSat comments, the README, the
manual and the examples README are corrected on solver selection,
CNF-file cleanup, JNI platforms, Eps_psimp, the polymorphic-goal
limitation, random-function laziness and manual section numbers.

Nine citations of selftest pins removed in c8bc036 are dropped, and
"Part 7" references now point at Part 6.

Claude-Session: https://claude.ai/code/session_01XTaiRGwPRK34zdimMPeEy9
parallel_builds/core kept HolRefute out of the otknl build because it
once INCLUDEd num/theories/cv_compute/automation, which that build
omits.  The Cv substrate and the dependency went in f6ae9ca, so the
guard and its comment were stale.  Verified with an --otknl build of
the tree up to parallel_builds/core plus HolRefute's examples and
theory_tests, with only the OpenTheory article export stubbed (the
opentheory tool is not installed here).

Claude-Session: https://claude.ai/code/session_01XTaiRGwPRK34zdimMPeEy9
# Conflicts:
#	src/IndDef/IndDefLib.sml
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.

2 participants