Skip to content

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

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

lukaszcz wants to merge 628 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), 16 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/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 and 16 Refute.* docfiles.

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/coalgebras and src/portableML/poly/concurrent selftests cover the changes described above.

lukaszcz and others added 30 commits July 30, 2026 02:13
ThmSetData's stored-attribute path records an ADD delta before applying
it to the global value, so a set type whose apply_to_global rejects the
theorem leaves a delta that raises again in every descendant theory.
That is an upstream bug, present independently of this work, and fixing
it belongs in its own change: restore src/1/ThmSetData.sml,
src/parse/AncestryData.{sml,sig} and the Portable.uninterruptible
primitive added for it to their upstream state.

KeyedThmSet's own export_thm keeps updating before recording -- that is
local to code introduced here and needs no shared-code change -- and the
IndDef selftest now checks only the diagnostic on the stored-attribute
path, since persistence there is governed by the upstream ordering.
path_11 exists only to supply the one_one field of the path TypeBase
entry; stopped_at_11 and pcons_11 are already exported and simp-tagged,
so the conjunction added nothing to pathTheory's public namespace.

Also drop the redundant trailing semicolons on the two Theorem
value-bindings added alongside it.
The level-2 quality gate had been un-runnable since eb9def1.  Holmake
never typechecks selftest.sml -- on Poly/ML a .uo is a load script, so
the file is compiled by quse declaration by declaration as it executes
-- and execution aborted early enough that roughly 20000 lines had never
been compiled at all, let alone run.

Kernel and harness:

  - theory_is_available asked for the current theory through
    Theory.current_theory, which raises when there is no theory segment.
    [bin/hol run] loads no prelude, so a script reaching [load "Refute"]
    hits exactly that state.  Ask the way Theory.get_parents does.
  - selftest.sml opens a scratch segment for the same reason, and
    shadows check_result so an exception from a predicate is one failed
    test rather than an aborted file: testutils.timed guards the
    function it runs but not the predicate it then applies, and the
    idiom used throughout this file does its work in the predicate.
  - the tests that enter the pipeline below refute_problem now mint the
    dynamically scoped call token themselves and release run resources
    on the way out, as refute_problem does.  Without it the smart gate
    raised "no active Refute call"; with a token but no release, a cache
    entry is stranded under a token nobody can look up again and its
    compiled_test is never closed.

Non-termination and soundness in the narrowing backend, which had never
executed because the committed module did not typecheck:

  - Refute_QC_Narrow wired retry_potential to an unbounded self-call, so
    a failed certification replay re-entered the same schedule entry for
    ever with a growing ignore list -- the list was the heap growth, and
    upd_size could not bound it because the recursion happened inside
    one entry.  Charge retries at an entry to a budget, as the random
    driver already charges its own.
  - visit_examples derived an example's genuineness from leaf values
    alone, dropping the demotion the rest of the module applies to an
    approximated domain.  Under upd_certify false this reported a
    Genuine counterexample to the true goal !x:num. ?y. y = SUC x.

Also: name the registry that actually refused when Cv rejects a type.
The custom and abstract generator registries are disjoint, so "custom or
abstract generator registered" sent the reader looking for a
register_generator call that need not exist.

Regression tests accompany each fix.  Level 1 now runs to completion in
under two minutes at ~2.4GB peak, down from a 148GB runaway; failures
are 41, all pre-existing defects this makes visible for the first time.
Eleven interactive transcripts plus a README covering the user-facing
surface: the entry points and verdicts, datatypes and functions, the
three substrates and determinism, smart generators, narrowing, custom
generators, the unused-assumption sweep, the model finder, and the
extension points.

These are session transcripts, not theory scripts -- Holmake ignores
them.  Each was run through

    bin/hol --holstate=refuteheap < examples/<file>

and the prose reconciled against what actually came back.  Files 09 and
10 need a Kodkodi component and a Java runtime; without one those calls
report Unknown rather than failing.

Several transcripts currently describe defective behaviour as though it
were designed -- the certification gap in 03, the potential-only
sections and the full-copy typedef workaround in 10, the ersatz
weakening in 11.  They need re-running and rewriting as those defects
are fixed.
register_quotient documented two accepted registration theorems and had
a destructor for each, but quotient_theorem_info only ever called
dest_bare_quotient and hardcoded the partial flag, so
dest_total_equivalence was dead code and every total extensionality
theorem was rejected as an unsupported shape.  ratTheory.RAT_EQUIV is
one, so four selftests -- the public registration validation, the frac
registration merge, the quotient axiom goldens and the refute$qn
display pins -- all died in their first registration call.

The dispatch is on the conclusion's head constant rather than on a
failed parse.  A QUOTIENT conclusion cannot also be a universally
quantified equation, so the head decides the shape outright, and each
destructor keeps its own diagnostics; parsing as QUOTIENT and falling
back to the total branch on failure would report a malformed QUOTIENT
theorem as a malformed equivalence.  The abs/rep cross-check stays on
the QUOTIENT branch alone, because a total equivalence names only the
representation relation and cannot identify supplied constants.  Both
destructors now raise one message naming both accepted shapes: neither
alone can tell which one the caller was aiming at, so the old bare
"unsupported shape" was not actionable.

The second bijection law of a typedef reached the encoding as the
exported biconditional P r <=> rep (abs r) = r.  That cannot work: for
an r outside P the value abs r is unrepresented, so the equation is
unknown rather than false, the biconditional is unknown rather than
true, and no scope of a proper-subset typedef was satisfiable.  The
model finder therefore found nothing at all for such a type, while a
whole-type typedef was unaffected -- zoo_three's own ABS/REP law had no
model at any cardinality row, zoo_univ's did.

The encoding now sees P r ==> rep (abs r) = r together with the
surjectivity P r ==> ?a. rep a = r.  The guard drops exactly the half
that mentions unrepresented values, and the surjectivity restates that
half without naming abs, so the abstract carrier stays pinned to P's
extension instead of to some subset of it; without it a typedef pinned
below its true cardinality yields spurious models.  With the membership
axiom the pair is equivalent to the biconditional, so no model is
gained or lost.  A whole-type typedef degenerates to T ==> ..., and the
registration accessor still reports the theorem's own conjuncts -- only
preprocessing sees the restatement.

The level-1 failure count goes from 41 to 37, removing exactly those
four failures and adding none.
Three defects in Refute_Extract, all in code the selftest had never
reached, accounted for nine level-1 failures.

Generated names.  556d5c6 made binder names injective, escaping every
non-alphanumeric character behind a v_ prefix, and routed upper_name and
lower_name through the same function.  Those two mint names for
generated datatype constructors and generated functions, which are
already separated by a fresh serial -- C_CONS_refute_ty_3 became
C_v_CONS_urefute_uty_u3 and f_rx_odd_1 became f_v_rx_uodd_u1 for no gain
beyond unreadable output.  Worse, the same function names SML type
variables, and the escaping ate the leading quote: :'a compiled to the
value identifier v_pa, which is not a type at all.  Injectivity now
belongs to clean_name and binders alone; sanitize_name restores mere
character legality for the names Refute mints itself.

Constructor patterns in strict mode.  fa026fc pointed strict
definition clauses at the observation matcher, so that a char-list CONS
would never be rendered as an SML pattern.  The matcher only knew how to
observe generated datatypes, but strict extraction keeps lists, options
and pairs in their native SML representations and mints no datatype for
them, so every definition matching on [] , ::, NONE, SOME or a pair
rejected the whole goal with "unknown lazy pattern constructor
list$NIL".  It now emits the native patterns the strict expression
compiler already emits for those constructors; the lazy representation,
where every type is a datatype, is unchanged.

Unrequested structural equality.  f2b0f24 stopped compile_types from
requesting equalities, but equality_declarations declared one per
registered type in strict mode regardless of what was requested, so the
removal changed nothing.  A representation-only compilation still
emitted structural equality for every type it touched, and any type
mentioning an arrow failed outright, because equality on a function has
to enumerate its domain.  Both modes now declare exactly the requested
set.  Discovery cannot widen the declared type set behind the datatype
declarations that precede it: ensure_type already registers every
constructor argument, element, domain and range type.

The byte golden moves from 3359/3964 to 3461/4157 bytes.  It was last
recorded at 6156067, before four later commits that legitimately
changed emission -- variables carry a type key since 2aebf52,
definition clauses go through the observation matcher since fa026fc,
its nonlinear patterns since 42beb32, and smart generator outputs bind
before residual checks since f66f652 -- so no fix could have restored
the old digest.  The emitted source was read in full: the strict program
is the prelude, no equality declarations, and one native list match; the
lazy program adds the hole helper, the two-constructor refute_ty_3
declaration and its suspended spine.

The level-1 failure count goes from 37 to 28, removing exactly those
nine failures and adding none.
Eleven level-1 failures across the model-finder translation pipeline
came from one library defect and five stale goldens.

The defect is a8e7d31.  It wanted a partially applied BIT1 to
translate, and got there by dropping NUMERAL, BIT1 and BIT2 from
built_in_consts.  But that table is what every earlier stage consults:
def_props_for_const, unfold_defs_in_term, specialize_consts_in_term,
add_to_uncurry_table and box_type all ask is_built_in_const, so a
literal 5 stopped being a numeral to all of them.  It was unfolded and
specialized into refute$sp3$arithmetic$NUMERAL with the definitional
axioms BIT2 = ZERO + (ZERO + SUC (SUC 0)) in tow -- unary arithmetic,
which is exactly what the comment a8e7d31 deleted had warned about.
Binarization then never triggered, skolemization and case unfolding
goldens drifted, and the live bridge stopped firing smart binary
integers.  The skeleton goes back on the table and the nut layer, which
already recognizes a fully formed numeral before it looks at heads,
expands what is left: NUMERAL n as n and BIT1/BIT2 n as 2n + 1 and
2n + 2.  A bare constructor eta-expands into the same case rather than
reaching the arity-0 built-in branch, whose only outcome for it was to
raise.  That fixes the built-in count and never-unfold pins, all five
preprocessing goldens, and one live-Kodkodi failure.

Five goldens recorded behaviour their own commits had already changed.

min_univ_card_of_rep of Opt (Struct [Atom (2, 0), Atom (3, 2)]) was
pinned at 6.  An Atom (k, j0) occupies indices j0 .. j0 + k - 1, so
Atom (3, 2) reaches index 4 and demands 5; the implementation has said
k + j0 since 670b9b1, which introduced both halves, so the golden was
arithmetic that never held.

The scope facto pins predate 79c8070, which made concreteness quantify
over the constructor argument types rather than over the binder types of
each argument type -- the latter is empty for every first-order field, so
the old test only ever exercised the trivial case.  A num list whose num
is capped at an inexact 2 is now inconcrete under both facto values, and
therefore exact under neither; completeness still carries the
facto-sensitivity the test was written for.

certify's Discarded arm returned a Potential carrying "certification
refuted the model" until 8d7f8c1 made it Drop, on the ground that
kernel evaluation has refuted the assignment at every certainty level.
Its Certified arm stopped merging decoded solver values into the
evaluations at 569bbc4 and stopped attaching the reconstruction report
at 96676da.  The certified eval is now the kernel's own, which for the
underspecified HD [] is the term itself, and the test pins model = NONE
as well.

certification_copy has taken the reconstruction's per-type atom rows
since c16c124, so that custom atom names certify; the merged-type-var
fixture still passed types = [] and could no longer produce an atom
substitution.  It now carries the row MFM.reconstruct produces.

tests/mf-binary-integers.kki still encoded Num as Isabelle's clamping
nat, if i <= 0 then 0 else i.  HOL4's Num is @n. if 0 <= i then i = &n
else i = -&n, that is ABS (integerScript.sml:1888, with Num (-a) = Num a
at 1946), and fd828ee corrected the translation without regenerating
the golden.  tests/forl-expressions.kki was hand-edited by c2c8766
rather than regenerated: that commit made the bitwise operators
parenthesize their right operand, which changes 1 & (2 & 3) and
1 | (2 & 3) but not the left-nested BitXor (BitXor (1, 2), 3), whose
correct rendering stays 1 ^ 2 ^ 3.

The level-1 failure count goes from 28 to 16, removing exactly these
eleven plus "smart binary integers did not fire on the arithmetic goal"
and adding none.

@mn200 mn200 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation has multiple instances of similar problems:

  • scripting should be used for the DESCRIPTION manual
  • doc-files must conform to template
  • some doc-files seem as if they can be omitted

Source-code:

  • structures should almost always have signatures

I'd like to see the IndDefLib changes pulled out into a separate PR.

[`Refute.try_refute`](#Refute.try_refute),
[`DB.thms`](#DB.thms)

The complete library guide is \ref{Description:the-holrefute-library}.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't have text after the See-also list of idents

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't conform to template (in help/template), which calls for an hrule after the type block and a one-line summary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't conform (as above)

Comment thread help/Docfiles/Refute.config.smd Outdated
them.

``` hol4
val cfg = Refute.default_config

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use scripting (with leading >>) so that the code actually runs and produces appropriate output in the generated file.

Comment thread help/Docfiles/Refute.config.smd Outdated
[`Refute.try_refute`](#Refute.try_refute),
[`Refute.find_unused_assms`](#Refute.find_unused_assms)

The complete library guide is \ref{Description:the-holrefute-library}.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't put material after the see-also section

Comment thread Manual/Description/Refute.smd Outdated
are exactly those where that numeral is missing.

#### Types the executable backends can test

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that loading Refute adds a dependency on the theories of rationals and reals?

Comment thread Manual/Description/Refute.smd Outdated
that constant; when the model finder builds its tables, a clause with
no constant head is dropped with a warning.

Structural registrations require distinct type variables as parameters

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to have this meta-data generated and stored by default.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should have a signature

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should have a signature; and using ref for global values is extremely suspect.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should have a signature

mn200 and others added 22 commits September 29, 2026 10:52
A server gets the heap its directory's Holmakefile asks for, and up to
src/boss that heap is bin/hol.state0, which has boolLib,
proofManagerLib and DB but nothing from src/coretypes onwards.
defnbase_init named DefnBase, so a server in src/num/theories died
compiling its own startup files. The store of user definitions is
DefnBaseCore's, which that heap has; DefnBase only re-exports the
lookup.

A startup step that will not compile now costs its own feature rather
than the session, and says which one in a window/showMessage sent once
the handshake is done. Dying there is the one failure a client cannot
be told about: it happens before initialize is answered, so all it can
report is that the server exited.

What such a step writes now reaches the user too. Claiming stdout for
the JSON-RPC frames, and pointing TextIO.stdOut at stderr, is what
stdioConnection has always done on the server's behalf, but it ran
after the startup files had been compiled, so their diagnostics went
onto the wire and were eaten. LSPServer.claimStdout is that same
hijack, callable before anything else runs.

The init-file load test ran on the full heap alone. It runs on
hol.state0 as well now, and fails if the heap and the --bare flag
disagree, so dropping the flag cannot leave it quietly testing the
same heap twice. Its files are dependencies of every log in that
directory: editing one changes what all three exercise while leaving
each object file alone. A protocol test covers the same ground from
outside, by serving a directory that names hol.state0 as its heap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A proof the LSP defers is replayed under Context.with_context, which
answers that thread's ambient reads from the proof's own context.
Writes still go to the live cell, and srw_ss() initialised itself by
writing the folded state back and reading it again, so under a pin it
answered with the state unfolded: no TypeBase simpls, and none of the
updates the ancestors had parked.

A script that builds its own tactics out of srw_ss() -- what the
theories below src/boss do, the context-reading shim covering a
declaration's own reads and not a top-level function's -- then ran its
proofs against a fraction of the simpset. Three proofs of
arithmeticScript that a build proves were reported as failures, and
stepping through them showed them going through, a walk restoring the
context rather than pinning it.

Under a pin the fold happens in place and the live cell is left alone.
Context.is_pinned answers from the counter ambient reads already
consult, so a session with no proof replaying pays nothing for it.

The fold itself is now shared by a one-entry memo, keyed on the
identity of the state and the TypeBase it folds in. A read that must
not write has nowhere else to keep its answer, and the fold is 3ms: it
ran 651 times while listTheory built, and says so in the log each time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A script that defines its own simp from srw_ss() reads the ambient
simpset: the context-reading shim covers a declaration's own reads, not
the ones a top-level function makes. The expander defers the tactic
expression to when the proof runs, so such an alias reads whatever the
process is doing then, and for a proof the LSP has deferred that is
under a pin. A Proof[exclude_simps = ...] window does not reach it
either.

Eta-expanding to the goal and the context, and taking the simpset from
that context, is what srw_tac has always done and what these aliases
mean.

The val-bound aliases inside the local blocks of listScript and
rich_listScript keep their ambient read. One of those is bound once and
frozen where it sits, which is not the same thing as a proof's context,
and widening it left rich_listTheory not terminating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
one_line_ify collapses an argument column whose type has one
constructor and whose pattern variables the right-hand side ignores,
replacing the pattern with a variable in every clause.  The test for
that admitted a column as soon as one clause qualified, so a
definition whose other clauses do read those variables had equations
built for it that are not theorems, and the finisher reached GENASSUME
with a residual rather than T.

A function taking a string and an index as a pair is enough to meet
this: whichever clause raises an error ignores both components.

Clauses already holding a variable there are untouched by the
collapse, so they still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A hover on a documented identifier carries the text of its Reference
entry below the type, and a link to the file that text was read from.
The entry's own cross-references -- its "See also" list, and the "Also
exported as" banner on an alias -- are written against the single-page
anchor scheme the manual used to have, which resolves to nothing in a
hover, so each points at the entry it names.

Both come from Manual/build/Docfiles-processed, the polyscripter-
evaluated mirror of help/Docfiles that bin/build writes: markdown a
client can render, where the .txt beside each source is a pandoc
plain-text rendering and the source still holds its frontmatter and
directives. A tree built with --no-helpdocs has neither that mirror
nor the index, and a hover there shows what it always did.

LSPExtension.helpLookup has been called by the hover since it was
written and has never had an implementation: the only thing that
installed it was Help.sml, which a language server does not load.
It is lsp/help_init.ML's now, beside the other server-start files,
and Help.sml goes back to being a terminal pager for help.

The index is read once, on the first documented hover, and a tree
that has none is remembered as having none.
The processed Reference entries are not finished markdown: mdbook runs
smdpp over them. A hover renders them as they stand, so it shows the
pandoc-only constructs that neither the book nor the PDF ever
displays -- \index{}, \label{}, and ```{=latex} / ```{=html} raw
blocks, body and all. A ```{=mdbook} block keeps its body.

Whose correct rendering is nothing at all, which is why they are worth
removing before an entry uses one: an author has no reason to expect
such a construct to surface, so nothing would stop them writing it,
and the hover would be the only place it appeared.

What smdpp resolves -- \ref{}, \refentry{}, \cite{} -- is left as
written. It renders as literal text, and a cross-reference the reader
has to look up beats a deleted one. Should entries start using them,
the pass belongs in process_docfiles, where doing it once would make
the processed tree markdown in name as well.

No entry carries any of this today, so the check is a load-time
selftest rather than a case in the protocol suite: a test driven from
the built tree would pass over nothing and read as a clean result.
Generate the DESCRIPTION chapter's transcripts with polyscripter (only
the Kodkod session stays static), bring the reference entries to the
help/template shape, and drop the entries for thin wrappers, which
their parent entries now mention.
Drop the entries for extension hooks a proof author does not call
(register_backend, register_frac_type, register_ersatz,
register_generator_family, harvest_registrations) and for thin
wrappers (export_refute_*, whose theorem attributes the DESCRIPTION
now documents; try_refute, folded into refute_goal).  The hooks keep
their contracts as comments in Refute.sig.

Bring every remaining entry to help/template: a Failure section in
each, and a See-also list ending in a period.  Correct the refute_psimp
condition, abstract_generator's constructor requirement, and when the
model finder registers typedefs and quotients by itself.
Every HolRefute structure now has a signature, ascribed opaquely, and
no module keeps a global ref.  Registries, caches and the configuration
are Context.Data slots owned by the new Refute_Session: a Refute call
reads the context captured at its entry, shares that view with its
worker threads, and at exit commits its cache changes to the live
context unless something else changed them meanwhile.  Registrations
publish straight to the live context and to the calling view.  Every
change serialises on its state's lock and runs its callback outside
the view's lock, so a callback may read any state.

The configuration ref becomes set_config / current_config /
with_config, and REFUTE_TAC reads the configuration of the context it
runs in.  Cross-call caches go stale by a generation token held in
their own state instead of global counters.  EvalSML's term-table
registry is gone: the host scopes the tables around dispatch and each
reconstruction.  EvalEnum chooses a fresh definition prefix against
the theory's current names, and its unreachable failure hook is
deleted.

The selftest moves to the new configuration API and pins a
registration made from inside a Refute call.
The docfiles still described the removed the_config ref; they now
describe the stored configuration and set_config / with_config /
current_config.  REFUTE_TAC_WITH's failure clause covers updaters
validated only when applied.  Each fact now lives in one entry: the
term-level wrappers and the Model/NoModel outcomes in refute, the
generator replacement rule once per generator entry.  Rewrap two
over-long lines in the DESCRIPTION chapter.
A review of the branch against develop found helpers written more than
once, hand-rolled library functions, dead code and some wasted work.
Mechanical, behaviour-preserving fixes only.

Shared instead of copied: factor_types, premise_head, theorem_term and
close_free, rf_type, remaining, int_of_numeral, data_type_spec, chop,
an argmin (least_by), the Potential downgrade (Refute_Cert.downgrade),
the QC/QC_Narrow run-body skeleton, and the instance rebuild shared by
make_instance and transport_instance.  Library calls replace
hand-rolled code: AList, KNametab list operations, Conv.QCONV,
pred_setSyntax set literals, Portable.make_counter, Lib.enumerate
where the start is 0.

Removed: Model's reconstruct/term_for_rep, define_clique,
compile_plan, serial_commas, PropSat.simplify, ParList.get_some, the
always-true should_tabulate_suc_for_type and its branches, parameters
with a single value (scope cursor iterative flag, extraction mode,
termlists_of selector), ~60 unreachable catch-all arms in
Refute_Extract, and unused signature entries.  Backends now register
only through Refute.sml.

Less work: record_primitive uses a per-extraction field map instead of
scanning TypeBase per application; the builtin codatatype list is
checked before the ancestry test; types are deduplicated before
classification; all_vars and free_vars_lr are hoisted; quadratic
appends are linear; narrowing normalises and converts to PNF once per
instance, not per depth.

Investigation-log comments are cut to current facts.
Backends declare a family (Quickcheck, ModelFinder or Other), and
QuickcheckBackends takes the registered Quickcheck family instead of
Refute_QC's hard-coded name list, which also named the narrowing
backend Refute_QC does not own.
Refute_Core.sig and the Refute facade each listed the config types and
all ~70 updaters; both now include Refute_Config, the facade realising
its datatypes as Refute_Core's so the two stay one type.
Extract spelled the literal test out three times.  The enumerator's
clause-pattern copy lacked word literals, so a word-literal clause
input compiled to an unguarded wildcard.  No outcome is known to
change: a downstream check already filtered the extra candidates.
The three candidate counters travelled as string-keyed stats and were
re-parsed, summed and re-rendered in QC, EvalCompute and EvalSML.
Refute_Core now owns the qc_counters record, its one rendering into
stats and its one summary text, shared by QC's vacuity reason and the
witness line.
The four num/int orders were listed in MF_HOL, twice in Nut and in
Mono; MF_HOL's order_consts now feeds all of them.  Nut's arithmetic
if-chain is a key table, and Nut checks at load that its word and
char operator tables name exactly MF_HOL's built-in constants.
A backend record now carries an optional render hook returning the
scope, bindings and model text of a counterexample; kodkod supplies
it, and the scope/model/boxed-type display code moves from Refute_Core
into Refute_ModelFinder.  Backends without a hook show their bindings
with format_term.

format_term prints Quot terms through a user printer on a copy of the
grammar instead of a placeholder substitution, so long terms containing
Quot now wrap at their real width.
The model finder's codatatype, quotient, typedef and frac registries
become one operator-keyed table of classifications, so a type
operator can have only one.  A registration replaces only its own
kind; the one exception is Frac, which replaces a quotient or typedef.
Built-in codatatypes and fmap's synthetic encoding count as
classifications too, so a typedef registration over fmap is now
refused.

harvest_typedef now answers only for genuine typedefs, which removes
the need for Refute_QC.transportable_typedef's raw_typedef_data guard
against the synthetic frac/fmap entries.
Refute_Core.call_memo memoises a builder for the running call,
backends included: concurrent callers wait for the first build.  The
model finder's definition/nondefinition tables and the certainty
ceiling's nondefinition scan use it, so QC's smart context, every
model-finder instance and harvest restarts share one build.

Measured on find_unused_assms over sumTheory (2s probes): 16 table
builds at ~15ms in 22-23s, about 1%, so no cross-call cache.
Fourteen structures kept their signature inline, three of them as an
anonymous `:> sig ... end`.  Each now has X.sig declaring signature X,
ascribed `structure X :> X`, and the two uppercase signature names
(REFUTE_PROP_SAT, REFUTE_MODEL_FINDER_MONO) follow the structure name.
Interfaces are unchanged.

Refute_Config, which holds only a signature, stays a .sml: holdep makes
every reference depend on Refute_Config.uo, which a lone .sig cannot
provide.
@lukaszcz

lukaszcz commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Documentation has multiple instances of similar problems:

  • scripting should be used for the DESCRIPTION manual
  • doc-files must conform to template
  • some doc-files seem as if they can be omitted

Source-code:

  • structures should almost always have signatures

I'd like to see the IndDefLib changes pulled out into a separate PR.

Documentation:

  • scripting is now used unless there's a kodkod dependency
  • doc-files made to conform to template, rewritten and made more concise
  • duplication and unnecessary doc-files removed

Source-code:

upd_search QuickcheckBackends resolved the family's backend names when
the update was built, and config_in applied updates outside any session,
so the registry read went to the live context.  Inside a proof that is
the "ambient context read while a proof was running" warning, and a
tactic given an older context ran the live registry's backends.

The QuickcheckBackends arm now resolves on application, and config_in
applies updates in a session over the tactic's context.  Either change
alone leaves the ambient read; the new selftest fails under each.
TypeBase's accessors read the live context, so a Refute call given an
older context saw datatypes defined after it, and a tactic's reads were
reported as ambient reads inside a proof.

Refute_TypeBase mirrors the twelve TypeBase functions Refute uses over
Refute_Session.context, the running call's view, and every call site
goes through it.  Outside a call the view is the live context, so
top-level entry points are unchanged.  Refute writes no TypeBase entry
during a call, so a frozen view misses nothing the call made itself.
load compiles a module's sources in the caller's top level, so after
`open realTheory` the pervasive abs is the theorem of that name and
load "Refute" failed with type errors.  process_docfiles runs every
docfile in one session, and Conv.MP_CONV opens realTheory before the
Refute docfiles load the library.
Feedback.emit_MESG, emit_WARNING and emit_ERR switched their flag off
and left it off, so every later docfile in the shared process_docfiles
session rendered without HOL messages, warnings or error details,
among them the Refute tactics' counterexample reports.
With warnings no longer suppressed across docfiles, the
Parse.set_known_constants example prints the PROD_IMAGE overload name
in its output, and reference.pdf failed with "Unicode character Π
(U+03A0) not set up for use with LaTeX".
@lukaszcz
lukaszcz requested a review from mn200 September 30, 2026 18:34
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