Knuckledragger Interactive Theorem Prover

repository·main·Indexed 41 days ago

https://github.com/philzook58/knuckledragger

An interactive theorem prover (version 0.3.0) that leverages Z3 for formal verification. It supports inductive datatypes via kd.Inductive, recursive functions via kd.define, and both automated (kd.prove) and interactive (kd.Lemma) proof environments. Includes ASMC (Assembly Checker), a tool for statically checking verification conditions in assembly code using Ghidra PCode and macros like kd_entry and kd_exit.

Tokens
30.9K
Snippets
142
Records
154
Agent score
52%

What's inside Knuckledragger

  1. Extend Imp with Break Statements

    main

    To support break, the evaluation relation is extended with a result signal (SContinue or SBreak).

    The relation st =[ c ]=> st' / s means command c starts in st, ends in st', and returns signal s.

    Behavior of break:

    • CBreak: Returns SBreak and leaves the state unchanged.
    • Sequencing (c1 ; c2): If c1 returns SBreak, c2 is skipped, and the SBreak signal is propagated.
    • Loops (while b do c end): If c returns SBreak, the loop terminates immediately, and the loop itself returns SContinue (to signal that the break was handled by the loop, not an outer loop).
    Inductive result : Type :=
      | SContinue
      | SBreak.
    
    Inductive ceval : com -> state -> result -> state -> Prop :=
      | E_Skip : forall st, st =[ CSkip ]=> st / SContinue
      ... (other rules) ...
    where "st =[ c ]=> st' / s" := (ceval c st s st').
  2. Syntax of Imp: Arithmetic, Boolean, and Commands

    main

    The Imp language consists of three main syntactic categories:

    1. Arithmetic Expressions (aexp): Includes natural numbers (ANum), variable identifiers (AId), addition (APlus), subtraction (AMinus), and multiplication (AMult).
    2. Boolean Expressions (bexp): Includes boolean constants (BTrue, BFalse), equality (BEq), inequality (BNeq), less-than-or-equal (BLe), greater-than (BGt), negation (BNot), and logical AND (BAnd).
    3. Commands (com): The executable statements, including CSkip (no-op), CAsgn (assignment x := a), CSeq (sequence c1 ; c2), CIf (conditional if b then c1 else c2 end), and CWhile (loop while b do c end).

    Custom notation allows writing these expressions using standard mathematical symbols (e.g., x + y, x <= y, x := y) within the <{ ... }> delimiter.

    Inductive aexp : Type :=
      | ANum (n : nat)
      | AId (x : string)
      | APlus (a1 a2 : aexp)
      | AMinus (a1 a2 : aexp)
      | AMult (a1 a2 : aexp).
    
    Inductive bexp : Type :=
      | BTrue
      | BFalse
      | BEq (a1 a2 : aexp)
      | BNeq (a1 a2 : aexp)
      | BLe (a1 a2 : aexp)
      | BGt (a1 a2 : aexp)
      | BNot (b : bexp)
      | BAnd (b1 b2 : bexp).
    
    Inductive com : Type :=
      | CSkip
      | CAsgn (x : string) (a : aexp)
      | CSeq (c1 c2 : com)
      | CIf (b : bexp) (c1 c2 : com)
      | CWhile (b : bexp) (c : com).
  3. Implement Type Classes

    main

    A type class is a structure combining abstract constants and properties. You can declare a type as an instance of a type class by providing concrete definitions for its constants and proving its properties. Lean then automatically retrieves the correct instance based on the type.

    instance Nat.Inhabited : Inhabited ℕ :=
      { default := 0 }
    
    #eval (Inhabited.default : ℕ)
  4. Use the Lemma object for backwards tactics

    main

    The kd.Lemma object implements a backwards tactic system. It maintains a list of goals and lemmas. You can push new goals, record proofs into lemmas, and finally assemble the proof using l.qed().

    l = kd.Lemma(smt.ForAll([n], n + Nat.Zero == n))
    # l.induct(_n)              
    # l.auto(by=[add.defn])
    # add_x_zero = l.qed()
  5. Use the EGraph class for term equivalence management

    main

    The EGraph class is a solver implementation used to manage sets of equivalent terms (equivalence classes) using an E-graph structure. It integrates with an SMT solver to handle semantic equality.

    Key capabilities include:

    • Adding terms and asserting equalities between them.
    • Performing bottom-up pattern matching (ematch) and rewriting (rw).
    • Extracting optimal terms based on a cost function.
    • Generating Graphviz visualizations of the e-classes.
    • Retrieving proofs for equalities when proof=True is passed to the constructor.
    from kdrag.solvers.egraph import EGraph
    import kdrag.smt as smt
    
    E = EGraph(proof=True)
    f = smt.Function('f', smt.IntSort(), smt.IntSort())
    x, y = smt.Ints('x y')
    E.add_term(f(x))
    E.union(f(x), y, reason="example reason")
  6. Use `cases` for case distinction

    main

    The cases tactic performs a case distinction on a term, creating subgoals for each constructor of that term's type. Unlike induction, cases does not produce induction hypotheses.

    It can also be used on hypotheses of the form l = r to perform substitution and pattern matching on the equality.

    theorem injection_example {α : Type} (x y : α) (xs ys : List α)
        (h : x :: xs = y :: ys) :
      x = y ∧ xs = ys :=
      by
        cases h
        simp
  7. Define dependent inductive types (Vectors)

    main

    Dependent inductive types allow types to depend on values. A common example is Vec α n, a vector of type α with a length n encoded in its type. This allows for compile-time guarantees about list lengths.

    inductive Vec (α : Type) : ℕ → Type where
      | nil                                : Vec α 0
      | cons (a : α) {n : ℕ} (v : Vec α n) : Vec α (n + 1)
  8. Interactive proof construction with ProofState

    main

    The ProofState class is used for interactive, imperative proof construction. It acts as a builder for kd.Proof objects. You start a proof using Lemma(goal).proof() and mutate the state using tactics. Once satisfied, call .qed() to build the final proof.

    Key features:

    • push() and pop(): Manage a stack of proof states, allowing you to try a tactic and backtrack if it fails.
    • intros(): Opens implications or negations, moving the antecedent into the context.
    • fixes() / fix(): Opens forall quantifiers.
    • simp(): Applies Z3 simplification to the goal or context.
    • cases(t): Performs case analysis on a boolean or a datatype constructor.
    • auto(): Attempts to discharge the current goal using Z3.
    p, q = smt.Bools("p q")
    
    # Interactive workflow
    proof = (Lemma(smt.Implies(p, q))
             .push()           # Save state
             .intros()         # Context: [p], Goal: q
             .auto()           # Discharge goal
             .pop()            # Backtrack if needed
             .qed())
  9. Model regular expressions using inductive definitions

    main

    Regular expressions can be modeled as an inductive type reg_exp T, where T is the type of characters. This allows you to define a matching relation s =~ re (where s is a list T) using an inductive proposition exp_match.

    Key constructors for exp_match include:

    • MEmpty: Matches the empty string [] to EmptyStr.
    • MChar: Matches a single-element list [x] to Char x.
    • MApp: Matches the concatenation of two strings s1 ++ s2 to the application of two regexes App re1 re2.
    • MUnionL/MUnionR: Matches a string to either side of a Union.
    • MStar0: Matches the empty string to a Star re.
    • MStarApp: Matches the concatenation of a string s1 (matching re) and s2 (matching Star re) to Star re.
    Inductive reg_exp (T : Type) : Type :=
      | EmptySet
      | EmptyStr
      | Char (t : T)
      | App (r1 r2 : reg_exp T)
      | Union (r1 r2 : reg_exp T)
      | Star (r : reg_exp T).
    
    Inductive exp_match {T} : list T -> reg_exp T -> Prop :=
      | MEmpty : [] =~ EmptyStr
      | MChar x : [x] =~ (Char x)
      | MApp s1 re1 s2 re2 (H1 : s1 =~ re1) (H2 : s2 =~ re2) : (s1 ++ s2) =~ (App re1 re2)
      | MUnionL s1 re1 re2 (H1 : s1 =~ re1) : s1 =~ (Union re1 re2)
      | MUnionR re1 s2 re2 (H2 : s2 =~ re2) : s2 =~ (Union re1 re2)
      | MStar0 re : [] =~ (Star re)
      | MStarApp s1 s2 re (H1 : s1 =~ re) (H2 : s2 =~ (Star re)) : (s1 ++ s2) =~ (Star re)
    where "s =~ re" := (exp_match s re).
  10. Handle potential errors using the natoption type

    main

    When writing functions that might fail (e.g., accessing an index in a list that is too short), avoid returning a 'magic' default value like 42. Instead, use an inductive type like natoption to explicitly represent success or failure.

    natoption has two constructors:

    • Some n: Represents a successful result containing the value n.
    • None: Represents a failure or an empty result.

    To extract a value from a natoption while providing a fallback, use an elimination pattern (like option_elim).

    Inductive natoption : Type :=
      | Some (n : nat)
      | None.
    
    (* Example: nth_error returns Some value if index exists, otherwise None *) 
    Fixpoint nth_error (l:natlist) (n:nat) : natoption :=
      match l with
      | nil => None
      | a :: l' => match n with
                   | 0 => Some a
                   | S n' => nth_error l' n'
                   end
      end.
    
    (* Example: option_elim extracts the value or returns a default *) 
    Definition option_elim (d : nat) (o : natoption) : nat :=
      match o with
      | Some n' => n'
      | None => d
      end.