diff --git a/lisa-kernel/src/main/scala/lisa/kernel/fol/CommonDefinitions.scala b/lisa-kernel/src/main/scala/lisa/kernel/fol/CommonDefinitions.scala
index 8a9d040f59dc4e4095cf254cf9b46a451d5148e2..6f65ffdfa92e73e527700e507f064dbf0b87cf62 100644
--- a/lisa-kernel/src/main/scala/lisa/kernel/fol/CommonDefinitions.scala
+++ b/lisa-kernel/src/main/scala/lisa/kernel/fol/CommonDefinitions.scala
@@ -26,7 +26,7 @@ private[fol] trait CommonDefinitions {
 
     val counterSeparator: Char = '_'
     val delimiter: Char = '`'
-    val forbiddenChars: Set[Char] = ("()[]{}?" + delimiter + counterSeparator).toSet
+    val forbiddenChars: Set[Char] = ("()[]{}?,;" + delimiter + counterSeparator).toSet
     def isValidIdentifier(s: String): Boolean = s.forall(c => !forbiddenChars.contains(c) && !c.isWhitespace)
   }
 
diff --git a/lisa-kernel/src/main/scala/lisa/kernel/fol/TermDefinitions.scala b/lisa-kernel/src/main/scala/lisa/kernel/fol/TermDefinitions.scala
index 58d7ad100d9366f34dd9391e59f6b24ecaf9e8fb..6e1b9b5cd8405f59ba6d49c682aef4ecd0f7ecab 100644
--- a/lisa-kernel/src/main/scala/lisa/kernel/fol/TermDefinitions.scala
+++ b/lisa-kernel/src/main/scala/lisa/kernel/fol/TermDefinitions.scala
@@ -38,6 +38,7 @@ private[fol] trait TermDefinitions extends TermLabelDefinitions {
    */
   sealed case class Term(label: TermLabel, args: Seq[Term]) extends TreeWithLabel[TermLabel] {
     require(label.arity == args.size)
+    val uniqueNumber: Long = TermCounters.getNewId
     val arity: Int = label.arity
 
     override def freeVariables: Set[VariableLabel] = label match {
@@ -55,6 +56,13 @@ private[fol] trait TermDefinitions extends TermLabelDefinitions {
     }
     override def freeSchematicTermLabels: Set[SchematicTermLabel] = schematicTermLabels
   }
+  private object TermCounters {
+    var totalNumberOfTerms: Long = 0
+    def getNewId: Long = {
+      totalNumberOfTerms += 1
+      totalNumberOfTerms
+    }
+  }
 
   /**
    * A VariableTerm is exactly an arity-0 term whose label is a variable label, but we provide specific constructors and destructors.
diff --git a/lisa-utils/src/main/scala/lisa/prooflib/BasicMain.scala b/lisa-utils/src/main/scala/lisa/prooflib/BasicMain.scala
index e22a111330a7f7a49858a50e68350e034881a890..f7bcd5f8fc8cb4892ca7cac0c3b91a2ed38203b0 100644
--- a/lisa-utils/src/main/scala/lisa/prooflib/BasicMain.scala
+++ b/lisa-utils/src/main/scala/lisa/prooflib/BasicMain.scala
@@ -1,6 +1,10 @@
 package lisa.prooflib
 
+import lisa.utils.Serialization.*
+
 trait BasicMain {
+  val library: Library
+
   private val realOutput: String => Unit = println
 
   given om: OutputManager = new OutputManager {
diff --git a/lisa-utils/src/main/scala/lisa/prooflib/Library.scala b/lisa-utils/src/main/scala/lisa/prooflib/Library.scala
index 1b57fa94f8b94b1b36d4194b62417a2ab448e994..73cc6096cc8359a3ea44e9fa086cd27cee31b556 100644
--- a/lisa-utils/src/main/scala/lisa/prooflib/Library.scala
+++ b/lisa-utils/src/main/scala/lisa/prooflib/Library.scala
@@ -30,6 +30,8 @@ abstract class Library extends lisa.prooflib.WithTheorems with lisa.prooflib.Pro
 
   var last: Option[JUSTIFICATION] = None
 
+  var _withCache: Boolean = false
+
   val knownDefs: scala.collection.mutable.Map[F.ConstantLabel[?], Option[JUSTIFICATION]] = scala.collection.mutable.Map.empty
 
   def addSymbol(s: F.ConstantFunctionLabel[?] | F.ConstantPredicateLabel[?] | F.Constant): Unit = {
diff --git a/lisa-utils/src/main/scala/lisa/prooflib/ProofsHelpers.scala b/lisa-utils/src/main/scala/lisa/prooflib/ProofsHelpers.scala
index 5fe1901e905d8b75b42e3645aac0a36519aea82f..23ba931d5186fab1f474e6c1c9833bfadc14f84f 100644
--- a/lisa-utils/src/main/scala/lisa/prooflib/ProofsHelpers.scala
+++ b/lisa-utils/src/main/scala/lisa/prooflib/ProofsHelpers.scala
@@ -148,17 +148,17 @@ trait ProofsHelpers {
   )
   class definitionWithVars[N <: Arity](val args: Variable *** N) {
 
-    // inline infix def -->(using om: OutputManager, name: sourcecode.Name, line: sourcecode.Line, file: sourcecode.File)(t: Term) = simpleDefinition(lambda(args, t, args.length))
-    // inline infix def -->(using om: OutputManager, name: sourcecode.Name, line: sourcecode.Line, file: sourcecode.File)(f: Formula) = predicateDefinition(lambda(args, f, args.length))
+    // inline infix def -->(using om: OutputManager, name: sourcecode.FullName, line: sourcecode.Line, file: sourcecode.File)(t: Term) = simpleDefinition(lambda(args, t, args.length))
+    // inline infix def -->(using om: OutputManager, name: sourcecode.FullName, line: sourcecode.Line, file: sourcecode.File)(f: Formula) = predicateDefinition(lambda(args, f, args.length))
 
-    inline infix def -->(using om: OutputManager, name: sourcecode.Name, line: sourcecode.Line, file: sourcecode.File)(t: The): ConstantFunctionLabelOfArity[N] =
-      FunctionDefinition[N](name.value, name.value, line.value, file.value)(args.toSeq, t.out, t.f, t.just).label
+    inline infix def -->(using om: OutputManager, name: sourcecode.FullName, line: sourcecode.Line, file: sourcecode.File)(t: The): ConstantFunctionLabelOfArity[N] =
+      FunctionDefinition[N](name.value, line.value, file.value)(args.toSeq, t.out, t.f, t.just).label
 
-    inline infix def -->(using om: OutputManager, name: sourcecode.Name, line: sourcecode.Line, file: sourcecode.File)(term: Term): ConstantFunctionLabelOfArity[N] =
-      SimpleFunctionDefinition[N](name.value, name.value, line.value, file.value)(lambda(args.toSeq, term).asInstanceOf).label
+    inline infix def -->(using om: OutputManager, name: sourcecode.FullName, line: sourcecode.Line, file: sourcecode.File)(term: Term): ConstantFunctionLabelOfArity[N] =
+      SimpleFunctionDefinition[N](name.value, line.value, file.value)(lambda(args.toSeq, term).asInstanceOf).label
 
-    inline infix def -->(using om: OutputManager, name: sourcecode.Name, line: sourcecode.Line, file: sourcecode.File)(formula: Formula): ConstantPredicateLabelOfArity[N] =
-      PredicateDefinition[N](name.value, name.value, line.value, file.value)(lambda(args.toSeq, formula).asInstanceOf).label
+    inline infix def -->(using om: OutputManager, name: sourcecode.FullName, line: sourcecode.Line, file: sourcecode.File)(formula: Formula): ConstantPredicateLabelOfArity[N] =
+      PredicateDefinition[N](name.value, line.value, file.value)(lambda(args.toSeq, formula).asInstanceOf).label
 
   }
 
@@ -172,13 +172,13 @@ trait ProofsHelpers {
   /**
    * Allows to make definitions "by unique existance" of a function symbol
    */
-  class FunctionDefinition[N <: F.Arity](using om: OutputManager)(val name: String, val fullName: String, line: Int, file: String)(
+  class FunctionDefinition[N <: F.Arity](using om: OutputManager)(val fullName: String, line: Int, file: String)(
       val vars: Seq[F.Variable],
       val out: F.Variable,
       val f: F.Formula,
       j: JUSTIFICATION
   ) extends DEFINITION(line, file) {
-    def repr: String =
+    override def repr: String =
       s" ${if (withSorry) " Sorry" else ""} Definition of function symbol ${label(vars)} := the ${out} such that ${(out === label(vars)) <=> f})\n"
 
     // val expr = LambdaExpression[Term, Formula, N](vars, f, valueOf[N])
@@ -284,33 +284,25 @@ trait ProofsHelpers {
   /**
    * Allows to make definitions "by equality" of a function symbol
    */
-  class SimpleFunctionDefinition[N <: F.Arity](using om: OutputManager)(name: String, fullName: String, line: Int, file: String)(
+  class SimpleFunctionDefinition[N <: F.Arity](using om: OutputManager)(fullName: String, line: Int, file: String)(
       val lambda: LambdaExpression[Term, Term, N],
       out: F.Variable,
       j: JUSTIFICATION
-  ) extends FunctionDefinition[N](name, fullName, line, file)(lambda.bounds.asInstanceOf, out, out === lambda.body, j) {
-    override def repr: String =
-      s" Definition of function symbol ${label(lambda.bounds.asInstanceOf)} := ${lambda.body}${if (j.withSorry) " (!! Relies on Sorry)" else ""}\n"
-
-  }
+  ) extends FunctionDefinition[N](fullName, line, file)(lambda.bounds.asInstanceOf, out, out === lambda.body, j) {}
 
   object SimpleFunctionDefinition {
-    def apply[N <: F.Arity](using om: OutputManager)(name: String, fullName: String, line: Int, file: String)(lambda: LambdaExpression[Term, Term, N]): SimpleFunctionDefinition[N] = {
-
-      // THM(using om: OutputManager)(val statement: F.Sequent, val fullName: String, line: Int, file: String, val kind: TheoremKind)(computeProof: Proof ?=> Unit)
+    def apply[N <: F.Arity](using om: OutputManager)(fullName: String, line: Int, file: String)(lambda: LambdaExpression[Term, Term, N]): SimpleFunctionDefinition[N] = {
       val intName = "definition_" + fullName
       val out = Variable(freshId(lambda.allSchematicLabels.map(_.id), "y"))
-      val defThm = new THM(F.ExistsOne(out, out === lambda.body), intName, line, file, InternalStatement)({
+      val defThm = THM(F.ExistsOne(out, out === lambda.body), intName, line, file, InternalStatement)({
         have(SimpleDeducedSteps.simpleFunctionDefinition(lambda, out))
       })
-      new SimpleFunctionDefinition[N](name, fullName, line, file)(lambda, out, defThm)
+      new SimpleFunctionDefinition[N](fullName, line, file)(lambda, out, defThm)
     }
   }
 
-  class PredicateDefinition[N <: F.Arity](using om: OutputManager)(name: String, fullName: String, line: Int, file: String)(val lambda: LambdaExpression[Term, Formula, N])
-      extends DEFINITION(line, file) {
-    override def repr: String =
-      s" Definition of predicate symbol ${label(lambda.bounds.asInstanceOf)} := ${lambda.body}"
+  class PredicateDefinition[N <: F.Arity](using om: OutputManager)(val fullName: String, line: Int, file: String)(val lambda: LambdaExpression[Term, Formula, N]) extends DEFINITION(line, file) {
+
     lazy val vars: Seq[F.Variable] = lambda.bounds.asInstanceOf
     val arity = lambda.arity
 
diff --git a/lisa-utils/src/main/scala/lisa/prooflib/WithTheorems.scala b/lisa-utils/src/main/scala/lisa/prooflib/WithTheorems.scala
index 27a9aa6877d158226222afbc7cec71646bd96c86..2fa698e2256a674ed7d05dbe561ae3e409140128 100644
--- a/lisa-utils/src/main/scala/lisa/prooflib/WithTheorems.scala
+++ b/lisa-utils/src/main/scala/lisa/prooflib/WithTheorems.scala
@@ -18,12 +18,25 @@ import scala.collection.mutable.Stack as stack
 trait WithTheorems {
   library: Library =>
 
+  /**
+   * The main builder for proofs. It is a mutable object that can be used to build a proof step by step.
+   * It is used either to construct a theorem/lemma ([[BaseProof]]) or to construct a subproof ([[InnerProof]]).
+   * We can add proof tactics to it producing intermediate results. In the end, obtain a [[K.SCProof]] from it.
+   *
+   * @param assump list of starting assumptions, usually propagated from outer proofs.
+   */
   sealed abstract class Proof(assump: List[F.Formula]) {
     val possibleGoal: Option[F.Sequent]
     type SelfType = this.type
     type OutsideFact >: JUSTIFICATION
     type Fact = ProofStep | InstantiatedFact | OutsideFact | Int
 
+    /**
+     * A proven fact (from a previously proven step, a theorem or a definition) with specific instantiations of free variables.
+     *
+     * @param fact The base fact
+     * @param insts The instantiation of free variables
+     */
     case class InstantiatedFact(
         fact: Fact,
         insts: Seq[F.SubstPair] /*,
@@ -44,8 +57,21 @@ trait WithTheorems {
     private var assumptions: List[F.Formula] = assump
     private var discharges: List[Fact] = Nil
 
+    /**
+     * the theorem that is being proved (paritally, if subproof) by this proof.
+     *
+     * @return The theorem
+     */
     def owningTheorem: THM
 
+    /**
+     * A proof step, containing a high level ProofTactic and the corresponding K.SCProofStep. If the tactic produce more than one
+     * step, they must be encapsulated in a subproof. Usually constructed with [[ValidProofTactic.validate]]
+     *
+     * @param judgement The result of the tactic
+     * @param scps The corresponding [[K.SCProofStep]]
+     * @param position The position of the step in the proof
+     */
     case class ProofStep private (judgement: ValidProofTactic, scps: K.SCProofStep, position: Int) {
       val bot: F.Sequent = judgement.bot
       def innerBot: K.Sequent = scps.bot
@@ -69,6 +95,10 @@ trait WithTheorems {
 
       }
     }
+
+    /**
+     * A proof step can be constructed from a succesfully executed tactic
+     */
     def newProofStep(judgement: ValidProofTactic): ProofStep =
       ProofStep.newProofStep(judgement)
 
@@ -91,6 +121,11 @@ trait WithTheorems {
       instantiatedFacts = (instFact, steps.length - 1) :: instantiatedFacts
     }
 
+    /**
+     * Add an assumption the the proof, i.e. a formula that is automatically on the left side of the sequent.
+     *
+     * @param f
+     */
     def addAssumption(f: F.Formula): Unit = {
       if (!assumptions.contains(f)) assumptions = f :: assumptions
     }
@@ -123,6 +158,11 @@ trait WithTheorems {
      */
     def getDischarges: List[Fact] = discharges
 
+    /**
+     * Produce the low level [[K.SCProof]] corresponding to the proof. Automatically eliminates any formula in the discharges that is still left of the sequent.
+     *
+     * @return
+     */
     def toSCProof: K.SCProof = {
       import lisa.utils.KernelHelpers.{-<<, ->>}
       val finalSteps = discharges.foldLeft(steps.map(_.scps))((cumul, next) => {
@@ -136,6 +176,12 @@ trait WithTheorems {
       K.SCProof(finalSteps.reverse.toIndexedSeq, getImports.map(of => of._2.underlying).toIndexedSeq)
     }
 
+    /**
+     * For a fact, returns the sequent that the fact proove and the position of the fact in the proof.
+     *
+     * @param fact Any fact, possibly instantiated, belonging to the proof
+     * @return its proven sequent and position
+     */
     def sequentAndIntOfFact(fact: Fact): (F.Sequent, Int) = fact match {
       case i: Int =>
         (
@@ -195,10 +241,22 @@ trait WithTheorems {
     def getSequent(f: Fact): F.Sequent = sequentOfFact(f)
     def mostRecentStep: ProofStep = steps.head
 
+    /**
+     * The number of steps in the proof. This is not the same as the number of steps in the corresponding [[K.SCProof]].
+     * This also does not count the number of steps in the subproof.
+     *
+     * @return
+     */
     def length: Int = steps.length
 
+    /**
+     * The set of symbols that can't be instantiated because they are free in an assumption.
+     */
     def lockedSymbols: Set[F.SchematicLabel[?]] = assumptions.toSet.flatMap(f => f.freeSchematicLabels.toSet)
 
+    /**
+     * Used to "lift" the type of a justification when the compiler can't infer it.
+     */
     def asOutsideFact(j: JUSTIFICATION): OutsideFact
 
     @nowarn("msg=.*It would fail on pattern case: _: InnerProof.*")
@@ -207,6 +265,10 @@ trait WithTheorems {
       case _: BaseProof => 0
     }
 
+    /**
+     * Create a subproof inside the current proof. The subproof will have the same assumptions as the current proof.
+     * Can have a goal known in advance (usually for a user-written subproof) or not (usually for a tactic-generated subproof).
+     */
     def newInnerProof(possibleGoal: Option[F.Sequent]) = new InnerProof(possibleGoal)
     final class InnerProof(val possibleGoal: Option[F.Sequent]) extends Proof(this.getAssumptions) {
       val parent: Proof.this.type = Proof.this
@@ -262,19 +324,58 @@ trait WithTheorems {
     }
   }
 
-  sealed class BaseProof(val owningTheorem: THM) extends Proof(Nil) {
+  /**
+   * Top-level instance of [[Proof]] directly proving a theorem
+   */
+  sealed class BaseProof(val owningTheorem: THMFromProof) extends Proof(Nil) {
     val goal: F.Sequent = owningTheorem.goal
     val possibleGoal: Option[F.Sequent] = Some(goal)
     type OutsideFact = JUSTIFICATION
     override inline def asOutsideFact(j: JUSTIFICATION): OutsideFact = j
 
     override def sequentOfOutsideFact(j: JUSTIFICATION): F.Sequent = j.statement
+
+    def justifications: List[JUSTIFICATION] = getImports.map(_._1)
   }
 
+  /**
+   * Abstract class representing theorems, axioms and different kinds of definitions. Corresponds to a [[theory.Justification]].
+   */
   sealed abstract class JUSTIFICATION {
+
+    /**
+     * A pretty representation of the justification
+     */
     def repr: String
+
+    /**
+     * The inner kernel justification
+     */
     def innerJustification: theory.Justification
+
+    /**
+     * The sequent that the justification proves
+     */
     def statement: F.Sequent
+
+    /**
+     * The complete name of the justification. Two justifications should never have the same full name. Typically, path is used to disambiguate.
+     */
+    def fullName: String
+
+    /**
+     * The short name of the justification (without the path).
+     */
+    val name: String = fullName.split("\\.").last
+
+    /**
+     * The "owning" object of the justification. Typically, the package/object in which it is defined.
+     */
+    val owner = fullName.split("\\.").dropRight(1).mkString(".")
+
+    /**
+     * Returns if the statement is unconditionaly proven or if it depends on some sorry step (including in the other justifications it relies on)
+     */
     def withSorry: Boolean = innerJustification match {
       case thm: theory.Theorem => thm.withSorry
       case fd: theory.FunctionDefinition => fd.withSorry
@@ -283,7 +384,10 @@ trait WithTheorems {
     }
   }
 
-  class AXIOM(innerAxiom: theory.Axiom, val axiom: F.Formula, val name: String) extends JUSTIFICATION {
+  /**
+   * A Justification, corresponding to [[K.Axiom]]
+   */
+  class AXIOM(innerAxiom: theory.Axiom, val axiom: F.Formula, val fullName: String) extends JUSTIFICATION {
     def innerJustification: theory.Axiom = innerAxiom
     val statement: F.Sequent = F.Sequent(Set(), Set(axiom))
     if (statement.underlying != theory.sequentFromJustification(innerAxiom)) {
@@ -292,40 +396,160 @@ trait WithTheorems {
     def repr: String = s" Axiom $name := $axiom"
   }
 
-  def Axiom(name: String, axiom: F.Formula): AXIOM = {
-    val ax: Option[theory.Axiom] = theory.addAxiom(name, axiom.underlying)
+  /**
+   * Introduces a new axiom in the theory.
+   *
+   * @param fullName The name of the axiom, including the path. Usually fetched automatically by the compiler.
+   * @param axiom The axiomatized formula.
+   * @return
+   */
+  def Axiom(using fullName: sourcecode.FullName)(axiom: F.Formula): AXIOM = {
+    val ax: Option[theory.Axiom] = theory.addAxiom(fullName.value, axiom.underlying)
     ax match {
-      case None => throw new InvalidAxiomException("Not all symbols belong to the theory", name, axiom, library)
-      case Some(value) => AXIOM(value, axiom, name)
+      case None => throw new InvalidAxiomException("Not all symbols belong to the theory", fullName.value, axiom, library)
+      case Some(value) => AXIOM(value, axiom, fullName.value)
     }
   }
 
-  // def Axiom(using om: OutputManager, line: Int, file: String)(ax: theory.Axiom): AXIOM = AXIOM(line, file, ax.)
+  /**
+   * A Justification, corresponding to [[K.FunctionDefinition]] or [[K.PredicateDefinition]]
+   */
   abstract class DEFINITION(line: Int, file: String) extends JUSTIFICATION {
+    val fullName: String
+    def repr: String = innerJustification.repr
 
     def label: F.ConstantLabel[?]
     knownDefs.update(label, Some(this))
 
   }
 
-  class THM(using om: OutputManager)(val statement: F.Sequent, val fullName: String, line: Int, file: String, val kind: TheoremKind)(computeProof: Proof ?=> Unit) extends JUSTIFICATION {
+  /**
+   * A proven, reusable statement. A justification corresponding to [[K.Theorem]].
+   */
+  sealed abstract class THM extends JUSTIFICATION {
+
+    /**
+     * The underlying Kernel proof [[K.SCProof]], if it is still available. Proofs are not kept in memory for efficiency.
+     */
+    def kernelProof: Option[K.SCProof]
+
+    /**
+     * The high level [[Proof]], if one was used to obtain the theorem. If the theorem was not produced by such high level proof but directly by a low level one, this is None.
+     */
+    def highProof: Option[BaseProof]
+    val innerJustification: theory.Theorem
+
+    /**
+     * A pretty representation of the goal of the theorem
+     */
+    def prettyGoal: String = lisa.utils.FOLPrinter.prettySequent(statement.underlying)
+  }
+  object THM {
+
+    /**
+     * Standard way to construct a theorem using a high level proof.
+     *
+     * @param om The output manager, available in any file extending [[lisa.utils.BasicMain]]
+     * @param statement The statement of the theorem
+     * @param fullName The full name of the theorem, including the path. Usually fetched automatically by the compiler.
+     * @param line The line at which the theorem is defined. Usually fetched automatically by the compiler. Used for error reporting
+     * @param file The file in which the theorem is defined. Usually fetched automatically by the compiler. Used for error reporting
+     * @param kind The kind of theorem (Theorem, Lemma, Corollary)
+     * @param computeProof The proof computation. The proof is built by adding proof steps to the proof object. The proof object is an impicit argument of computeProof,
+     * @see <a href="https://docs.scala-lang.org/scala3/reference/contextual/context-functions.html">Context Functions in Scala</a>
+     * @return
+     */
+    def apply(using om: OutputManager)(statement: F.Sequent, fullName: String, line: Int, file: String, kind: TheoremKind)(computeProof: Proof ?=> Unit) =
+      THMFromProof(statement, fullName, line, file, kind)(computeProof)
+
+    /**
+     * Constructs a "high level" theorem from an existing theorem in the
+     *
+     * @param om The output manager, available in any file extending [[lisa.utils.BasicMain]]
+     * @param statement The statement of the theorem
+     * @param fullName The full name of the theorem, including the path/package.
+     * @param kind The kind of theorem (Theorem, Lemma, Corollary)
+     * @param innerThm The inner theorem, coming from the kernel
+     * @param getProof If available, a way to compute the Kernel proof again.
+     */
+    def fromKernel(using om: OutputManager)(statement: F.Sequent, fullName: String, kind: TheoremKind, innerThm: theory.Theorem, getProof: () => Option[K.SCProof]) =
+      THMFromKernel(statement, fullName, kind, innerThm, getProof)
+
+    /**
+     * Construct a theorem (both in the kernel and high level) from a proof.
+     *
+     * @param om The output manager, available in any file extending [[lisa.utils.BasicMain]]
+     * @param statement The statement of the theorem
+     * @param fullName The full name of the theorem, including the path/package.
+     * @param kind The kind of theorem (Theorem, Lemma, Corollary)
+     * @param getProof The kernel proof.
+     * @param justifs low level justifications used to justify the proof's imports
+     * @return
+     */
+    def fromSCProof(using om: OutputManager)(statement: F.Sequent, fullName: String, kind: TheoremKind, getProof: () => K.SCProof, justifs: Seq[theory.Justification]): THM =
+      val proof = getProof()
+      theory.theorem(fullName, statement.underlying, proof, justifs) match {
+        case K.Judgement.ValidJustification(just) =>
+          fromKernel(statement, fullName, kind, just.asInstanceOf, () => Some(getProof()))
+        case wrongJudgement: K.Judgement.InvalidJustification[?] =>
+          om.lisaThrow(
+            LisaException.InvalidKernelJustificationComputation(
+              "The proof was rejected by LISA's logical kernel. ",
+              wrongJudgement,
+              None
+            )
+          )
+      }
+
+  }
+
+  /**
+   * A theorem that was produced from a kernel theorem and not from a high level proof. See [[THM.fromKernel]].
+   * Those are typically theorems imported from another tool, or from serialization.
+   */
+  class THMFromKernel(using om: OutputManager)(val statement: F.Sequent, val fullName: String, val kind: TheoremKind, innerThm: theory.Theorem, getProof: () => Option[K.SCProof]) extends THM {
+
+    def repr: String = innerJustification.repr
+    val innerJustification: theory.Theorem = innerThm
+    assert(innerThm.name == fullName)
+    def kernelProof: Option[K.SCProof] = getProof()
+    def highProof: Option[BaseProof] = None
+
+    val goal: F.Sequent = statement
+
+  }
+
+  /**
+   * A theorem that was produced from a high level proof. See [[THM.apply]].
+   * Typical way to construct a theorem in the library, but serialization for example will produce a [[THMFromKernel]].
+   */
+  class THMFromProof(using om: OutputManager)(val statement: F.Sequent, val fullName: String, line: Int, file: String, val kind: TheoremKind)(computeProof: Proof ?=> Unit) extends THM {
 
     val goal: F.Sequent = statement
-    val name: String = fullName
 
     val proof: BaseProof = new BaseProof(this)
+    def kernelProof: Option[K.SCProof] = Some(proof.toSCProof)
+    def highProof: Option[BaseProof] = Some(proof)
 
-    val innerJustification: theory.Theorem = prove(computeProof)
+    import lisa.utils.Serialization.*
+    val innerJustification: theory.Theorem =
+      if library._withCache then
+        oneThmFromFile("cache/" + name, library.theory) match {
+          case Some(thm) => thm // try to get the theorem from file
 
-    def prettyGoal: String = lisa.utils.FOLPrinter.prettySequent(goal.underlying)
-    def repr: String = s" ${if (withSorry) " Sorry" else ""} Theorem $name := $statement\n"
+          case None =>
+            val (thm, scp, justifs) = prove(computeProof) // if fail, prove it
+            thmsToFile("cache/" + name, theory, List((name, scp, justifs))) // and save it to the file
+            thm
+        }
+      else prove(computeProof)._1
+    def repr: String = innerJustification.repr
 
-    private def prove(computeProof: Proof ?=> Unit): theory.Theorem = {
+    library.last = Some(this)
+    private def prove(computeProof: Proof ?=> Unit): (theory.Theorem, SCProof, List[(String, theory.Justification)]) = {
       try {
         computeProof(using proof)
       } catch {
-        /*case e: NotImplementedError =>
-          om.lisaThrow(new UnimplementedProof(this))*/
         case e: UserLisaException =>
           om.lisaThrow(e)
       }
@@ -334,10 +558,10 @@ trait WithTheorems {
         om.lisaThrow(new UnimplementedProof(this))
 
       val scp = proof.toSCProof
-      theory.theorem(name, goal.underlying, scp, proof.getImports.map(_._1.innerJustification)) match {
+      val justifs = proof.getImports.map(e => (e._1.owner, e._1.innerJustification))
+      theory.theorem(name, goal.underlying, scp, justifs.map(_._2)) match {
         case K.Judgement.ValidJustification(just) =>
-          library.last = Some(this)
-          just
+          (just, scp, justifs)
         case wrongJudgement: K.Judgement.InvalidJustification[?] =>
           om.lisaThrow(
             LisaException.InvalidKernelJustificationComputation(
@@ -355,19 +579,33 @@ trait WithTheorems {
 
   trait TheoremKind {
     val kind2: String
-    def apply(using om: OutputManager, name: sourcecode.Name, line: sourcecode.Line, file: sourcecode.File)(statement: F.Sequent)(computeProof: Proof ?=> Unit): THM = {
-      val thm = new THM(statement, name.value, line.value, file.value, this)(computeProof) {}
-      if (this == Theorem) {
-        show(thm)
-      }
+
+    def apply(using om: OutputManager, name: sourcecode.FullName, line: sourcecode.Line, file: sourcecode.File)(statement: F.Sequent)(computeProof: Proof ?=> Unit): THM = {
+      val thm = THM(statement, name.value, line.value, file.value, this)(computeProof)
+      if this == Theorem then show(thm)
       thm
     }
 
   }
+
+  /**
+   * A "Theorem" kind of theorem, by opposition with a lemma or corollary. The difference is that theorem are always printed when a file defining one is run.
+   */
   object Theorem extends TheoremKind { val kind2: String = "Theorem" }
+
+  /**
+   * Lemmas are like theorems, but are conceptually less importants and are not printed when a file defining one is run.
+   */
   object Lemma extends TheoremKind { val kind2: String = "Lemma" }
+
+  /**
+   * Corollaries are like theorems, but are conceptually less importants and are not printed when a file defining one is run.
+   */
   object Corollary extends TheoremKind { val kind2: String = "Corollary" }
 
+  /**
+   * Internal statements are internally produced theorems, for example as intermediate step in definitions.
+   */
   object InternalStatement extends TheoremKind { val kind2: String = "Internal, automatically produced" }
 
 }
diff --git a/lisa-utils/src/main/scala/lisa/utils/KernelHelpers.scala b/lisa-utils/src/main/scala/lisa/utils/KernelHelpers.scala
index fa327fe3df6633f23c0c3258b38e77009940d3bb..62381a492a9fe2196e8ef6c769bc4ab35b5d2175 100644
--- a/lisa-utils/src/main/scala/lisa/utils/KernelHelpers.scala
+++ b/lisa-utils/src/main/scala/lisa/utils/KernelHelpers.scala
@@ -463,6 +463,10 @@ object KernelHelpers {
    */
   def checkProof(proof: SCProof, output: String => Unit = println): Unit = {
     val judgement = SCProofChecker.checkSCProof(proof)
-    output(FOLPrinter.prettySCProof(judgement))
+    val pl = proof.totalLength
+    if pl > 100 then
+      output("...")
+      output(s"Proof is too long to be displayed [$pl steps]")
+    else output(FOLPrinter.prettySCProof(judgement))
   }
 }
diff --git a/lisa-utils/src/main/scala/lisa/utils/LisaException.scala b/lisa-utils/src/main/scala/lisa/utils/LisaException.scala
index 7b945d40ec9884f6f797bef69e7d13a43a21e00e..dcef0ce6b31ad1f542b028e077eccb59c445d671 100644
--- a/lisa-utils/src/main/scala/lisa/utils/LisaException.scala
+++ b/lisa-utils/src/main/scala/lisa/utils/LisaException.scala
@@ -44,6 +44,9 @@ abstract class UserLisaException(var errorMessage: String)(using line: sourcecod
   def fixTrace(): Unit = ()
 }
 object UserLisaException {
+  class InvalidProofFromFileException(errorMessage: String, file: String)(using sourcecode.Line, sourcecode.File) extends UserLisaException(errorMessage) {
+    def showError: String = errorMessage
+  }
   class InvalidAxiomException(errorMessage: String, name: String, formula: lisa.fol.FOL.Formula, library: lisa.prooflib.Library)(using sourcecode.Line, sourcecode.File)
       extends UserLisaException(errorMessage) {
     def showError: String = s"The desired axiom \"$name\" contains symbol that are not part of the theory.\n" +
diff --git a/lisa-utils/src/main/scala/lisa/utils/ProofsShrink.scala b/lisa-utils/src/main/scala/lisa/utils/ProofsShrink.scala
index e005fa3fa0c21b00b4b8ed6d2baf9dda2ff706b6..cc8980574b7242aa7cf59fd712e331ea29677743 100644
--- a/lisa-utils/src/main/scala/lisa/utils/ProofsShrink.scala
+++ b/lisa-utils/src/main/scala/lisa/utils/ProofsShrink.scala
@@ -94,14 +94,14 @@ object ProofsShrink {
               val (k, sequent) = resolve(i)
               val imported = subProof.imports(j)
               if (sequent != imported) {
-                Some((Restate(imported, k), -(j - 1) -> imported))
+                Some((Restate(imported, k), j -> imported))
               } else {
-                None
+                Some((Restate(imported, k), j -> imported))
               }
             }.unzip
-            val rewrittenMap = rewrittenSeq.zipWithIndex.map { case ((i, sequent), j) => i -> (offset + acc.size + j, sequent) }.toMap
-            val childTopPremises = subPremises.map(i => rewrittenMap.getOrElse(i, resolve(i))).toIndexedSeq
-            acc ++ rewrittenPremises ++ flattenProofRecursive(subProof.steps, childTopPremises, offset + acc.size + rewrittenPremises.size)
+            val rewrittenMap = rewrittenSeq.map { case (j, sequent) => j -> (offset + acc.size - j - 1 + rewrittenSeq.size, sequent) }.toMap
+            val childTopPremises = subPremises.zipWithIndex.map((i, j) => rewrittenMap(j)).toIndexedSeq
+            acc ++ rewrittenPremises.reverse ++ flattenProofRecursive(subProof.steps, childTopPremises, offset + acc.size + rewrittenPremises.size)
           case _ =>
             acc :+ mapPremises(step, i => resolve(i)._1)
         }
@@ -299,4 +299,5 @@ object ProofsShrink {
     optimizeFixedPoint(flattenProof(proof))
   }
 
+  def minimizeProofOnce(proof: SCProof): SCProof = deadStepsElimination(factorProof(simplifyProof(flattenProof(proof))))
 }
diff --git a/lisa-utils/src/main/scala/lisa/utils/Serialization.scala b/lisa-utils/src/main/scala/lisa/utils/Serialization.scala
new file mode 100644
index 0000000000000000000000000000000000000000..d89d473f7c98be058aed74279a12372e08fa8d5e
--- /dev/null
+++ b/lisa-utils/src/main/scala/lisa/utils/Serialization.scala
@@ -0,0 +1,721 @@
+package lisa.utils
+
+import lisa.utils.K.*
+import lisa.utils.ProofsShrink.*
+
+import java.io._
+import scala.collection.mutable.{Map => MutMap}
+
+object Serialization {
+
+  // Define codes for the various proof steps
+  inline def restate: Byte = 0
+  inline def restateTrue: Byte = 1
+  inline def hypothesis: Byte = 2
+  inline def cut: Byte = 3
+  inline def leftAnd: Byte = 4
+  inline def leftOr: Byte = 5
+  inline def leftImplies: Byte = 6
+  inline def leftIff: Byte = 7
+  inline def leftNot: Byte = 8
+  inline def leftForall: Byte = 9
+  inline def leftExists: Byte = 10
+  inline def leftExistsOne: Byte = 11
+  inline def rightAnd: Byte = 12
+  inline def rightOr: Byte = 13
+  inline def rightImplies: Byte = 14
+  inline def rightIff: Byte = 15
+  inline def rightNot: Byte = 16
+  inline def rightForall: Byte = 17
+  inline def rightExists: Byte = 18
+  inline def rightExistsOne: Byte = 19
+  inline def weakening: Byte = 20
+  inline def leftRefl: Byte = 21
+  inline def rightRefl: Byte = 22
+  inline def leftSubstEq: Byte = 23
+  inline def rightSubstEq: Byte = 24
+  inline def leftSubstIff: Byte = 25
+  inline def rightSubstIff: Byte = 26
+  inline def instSchema: Byte = 27
+  inline def scSubproof: Byte = 28
+  inline def sorry: Byte = 29
+
+  type Line = Int
+
+  // Injectively represent a TermLabel as a string
+  def termLabelToString(label: TermLabel): String =
+    label match
+      case l: ConstantFunctionLabel => "cfl_" + l.id.name + "_" + l.id.no + "_" + l.arity
+      case l: SchematicFunctionLabel => "sfl_" + l.id.name + "_" + l.id.no + "_" + l.arity
+      case l: VariableLabel => "vl_" + l.id.name + "_" + l.id.no
+
+  // Injectively represent a FormulaLabel as a string.
+  def formulaLabelToString(label: FormulaLabel): String =
+    label match
+      case l: ConstantPredicateLabel => "cpl_" + l.id.name + "_" + l.id.no + "_" + l.arity
+      case l: SchematicPredicateLabel => "spl_" + l.id.name + "_" + l.id.no + "_" + l.arity
+      case l: ConstantConnectorLabel => "ccl_" + l.id.name + "_" + l.id.no + "_" + l.arity
+      case l: SchematicConnectorLabel => "scl_" + l.id.name + "_" + l.id.no + "_" + l.arity
+      case l: VariableFormulaLabel => "vfl_" + l.id.name + "_" + l.id.no
+      case l: BinderLabel => "bl_" + l.id.name + "_" + l.id.no
+
+  // write a term label to an OutputStream
+  def termLabelToDOS(label: TermLabel, dos: DataOutputStream): Unit =
+    label match
+      case l: ConstantFunctionLabel =>
+        dos.writeByte(0)
+        dos.writeUTF(l.id.name)
+        dos.writeInt(l.id.no)
+        dos.writeInt(l.arity)
+      case l: SchematicFunctionLabel =>
+        dos.writeByte(1)
+        dos.writeUTF(l.id.name)
+        dos.writeInt(l.id.no)
+        dos.writeInt(l.arity)
+      case l: VariableLabel =>
+        dos.writeByte(2)
+        dos.writeUTF(l.id.name)
+        dos.writeInt(l.id.no)
+  // write a formula label to an OutputStream
+  def formulaLabelToDOS(label: FormulaLabel, dos: DataOutputStream): Unit =
+    label match
+      case l: ConstantPredicateLabel =>
+        dos.writeByte(3)
+        dos.writeUTF(l.id.name)
+        dos.writeInt(l.id.no)
+        dos.writeInt(l.arity)
+      case l: SchematicPredicateLabel =>
+        dos.writeByte(4)
+        dos.writeUTF(l.id.name)
+        dos.writeInt(l.id.no)
+        dos.writeInt(l.arity)
+      case l: ConstantConnectorLabel =>
+        dos.writeByte(5)
+        dos.writeUTF(l.id.name)
+      case l: SchematicConnectorLabel =>
+        dos.writeByte(6)
+        dos.writeUTF(l.id.name)
+        dos.writeInt(l.id.no)
+        dos.writeInt(l.arity)
+      case l: VariableFormulaLabel =>
+        dos.writeByte(7)
+        dos.writeUTF(l.id.name)
+        dos.writeInt(l.id.no)
+      case l: BinderLabel =>
+        dos.writeByte(8)
+        dos.writeUTF(l.id.name)
+
+  /**
+   * Main function that, when given a proof, will serialize it to a file. It will also serialize all the formulas appearing in it to another file.
+   */
+  def proofsToDataStream(treesDOS: DataOutputStream, proofDOS: DataOutputStream, theorems: Seq[(String, SCProof, List[String])]): Unit = {
+
+    val termMap = MutMap[Long, Line]()
+    val formulaMap = MutMap[Long, Line]()
+
+    var line = -1
+
+    // Compute the line of a term. If it is not in the map, add it to the map and write it to the tree file
+    def lineOfTerm(term: Term): Line =
+      termMap.get(term.uniqueNumber) match
+        case Some(line) => line
+        case None =>
+          val na = term.args.map(t => lineOfTerm(t))
+          termLabelToDOS(term.label, treesDOS)
+          na.foreach(t => treesDOS.writeInt(t))
+          line = line + 1
+          termMap(term.uniqueNumber) = line
+          line
+
+    // Compute the line of a formula. If it is not in the map, add it to the map and write it to the tree file
+    def lineOfFormula(formula: Formula): Line =
+      formulaMap.get(formula.uniqueNumber) match
+        case Some(line) => line
+        case None =>
+          val nextLine = formula match
+            case PredicateFormula(label, args) =>
+              val na = args.map(t => lineOfTerm(t))
+              formulaLabelToDOS(label, treesDOS)
+              na.foreach(t => treesDOS.writeInt(t))
+            case ConnectorFormula(label, args) =>
+              val na = args.map(t => lineOfFormula(t))
+              formulaLabelToDOS(label, treesDOS)
+              treesDOS.writeShort(na.size)
+              na.foreach(t => treesDOS.writeInt(t))
+            case BinderFormula(label, bound, inner) =>
+              val ni = lineOfFormula(inner)
+              formulaLabelToDOS(label, treesDOS)
+              termLabelToDOS(bound, treesDOS)
+              treesDOS.writeInt(ni)
+          line = line + 1
+          formulaMap(formula.uniqueNumber) = line
+          line
+
+    // Write a sequent to the proof file.
+    def sequentToProofDOS(sequent: Sequent): Unit =
+      proofDOS.writeShort(sequent.left.size)
+      sequent.left.foreach(f => proofDOS.writeInt(lineOfFormula(f)))
+      proofDOS.writeShort(sequent.right.size)
+      sequent.right.foreach(f => proofDOS.writeInt(lineOfFormula(f)))
+
+    def lttToProofDOS(ltt: LambdaTermTerm): Unit =
+      val body = lineOfTerm(ltt.body)
+      proofDOS.writeShort(ltt.vars.size)
+      ltt.vars.foreach(v => termLabelToDOS(v, proofDOS))
+      proofDOS.writeInt(body)
+
+    def ltfToProofDOS(ltf: LambdaTermFormula): Unit =
+      val body = lineOfFormula(ltf.body)
+      proofDOS.writeShort(ltf.vars.size)
+      ltf.vars.foreach(v => termLabelToDOS(v, proofDOS))
+      proofDOS.writeInt(body)
+
+    def lffToProofDOS(lff: LambdaFormulaFormula): Unit =
+      val body = lineOfFormula(lff.body)
+      proofDOS.writeShort(lff.vars.size)
+      lff.vars.foreach(v => formulaLabelToDOS(v, proofDOS))
+      proofDOS.writeInt(body)
+
+    /**
+     * Write a proof step to the proof file.
+     * First write the code describing the proof step, then the "bot" sequent, then the various parameters in order.
+     * List are described by first writing (as a short) the number of elements in the list.
+     *
+     * @param ps
+     */
+    def proofStepToProofDOS(ps: SCProofStep): Unit = {
+      ps match {
+        case Restate(bot, t1) =>
+          proofDOS.writeByte(restate)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+        case RestateTrue(bot) =>
+          proofDOS.writeByte(restateTrue)
+          sequentToProofDOS(bot)
+        case Hypothesis(bot, phi) =>
+          proofDOS.writeByte(hypothesis)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(lineOfFormula(phi))
+        case Cut(bot, t1, t2, phi) =>
+          proofDOS.writeByte(cut)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(t2)
+          proofDOS.writeInt(lineOfFormula(phi))
+        case LeftAnd(bot, t1, phi, psi) =>
+          proofDOS.writeByte(leftAnd)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          proofDOS.writeInt(lineOfFormula(psi))
+        case LeftOr(bot, t, disjuncts) =>
+          proofDOS.writeByte(leftOr)
+          sequentToProofDOS(bot)
+          proofDOS.writeShort(t.size)
+          t.foreach(proofDOS.writeInt)
+          proofDOS.writeShort(disjuncts.size)
+          disjuncts.foreach(f => proofDOS.writeInt(lineOfFormula(f)))
+        case LeftImplies(bot, t1, t2, phi, psi) =>
+          proofDOS.writeByte(leftImplies)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(t2)
+          proofDOS.writeInt(lineOfFormula(phi))
+          proofDOS.writeInt(lineOfFormula(psi))
+        case LeftIff(bot, t1, phi, psi) =>
+          proofDOS.writeByte(leftIff)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          proofDOS.writeInt(lineOfFormula(psi))
+        case LeftNot(bot, t1, phi) =>
+          proofDOS.writeByte(leftNot)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+        case LeftForall(bot, t1, phi, x, t) =>
+          proofDOS.writeByte(leftForall)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          termLabelToDOS(x, proofDOS)
+          proofDOS.writeInt(lineOfTerm(t))
+        case LeftExists(bot, t1, phi, x) =>
+          proofDOS.writeByte(leftExists)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          termLabelToDOS(x, proofDOS)
+        case LeftExistsOne(bot, t1, phi, x) =>
+          proofDOS.writeByte(leftExistsOne)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          termLabelToDOS(x, proofDOS)
+        case RightAnd(bot, t, conjuncts) =>
+          proofDOS.writeByte(rightAnd)
+          sequentToProofDOS(bot)
+          proofDOS.writeShort(t.size)
+          t.foreach(proofDOS.writeInt)
+          proofDOS.writeShort(conjuncts.size)
+          conjuncts.foreach(f => proofDOS.writeInt(lineOfFormula(f)))
+        case RightOr(bot, t1, phi, psi) =>
+          proofDOS.writeByte(rightOr)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          proofDOS.writeInt(lineOfFormula(psi))
+        case RightImplies(bot, t1, phi, psi) =>
+          proofDOS.writeByte(rightImplies)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          proofDOS.writeInt(lineOfFormula(psi))
+        case RightIff(bot, t1, t2, phi, psi) =>
+          proofDOS.writeByte(rightIff)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(t2)
+          proofDOS.writeInt(lineOfFormula(phi))
+          proofDOS.writeInt(lineOfFormula(psi))
+        case RightNot(bot, t1, phi) =>
+          proofDOS.writeByte(rightNot)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+        case RightForall(bot, t1, phi, x) =>
+          proofDOS.writeByte(rightForall)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          termLabelToDOS(x, proofDOS)
+        case RightExists(bot, t1, phi, x, t) =>
+          proofDOS.writeByte(rightExists)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          termLabelToDOS(x, proofDOS)
+          proofDOS.writeInt(lineOfTerm(t))
+        case RightExistsOne(bot, t1, phi, x) =>
+          proofDOS.writeByte(rightExistsOne)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(phi))
+          termLabelToDOS(x, proofDOS)
+        case Weakening(bot, t1) =>
+          proofDOS.writeByte(weakening)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+        case LeftRefl(bot, t1, fa) =>
+          proofDOS.writeByte(leftRefl)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeInt(lineOfFormula(fa))
+        case RightRefl(bot, fa) =>
+          proofDOS.writeByte(rightRefl)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(lineOfFormula(fa))
+        case LeftSubstEq(bot, t1, equals, lambdaPhi) =>
+          proofDOS.writeByte(leftSubstEq)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeShort(equals.size)
+          equals.foreach(t =>
+            proofDOS.writeInt(lineOfTerm(t._1))
+            proofDOS.writeInt(lineOfTerm(t._2))
+          )
+          ltfToProofDOS(lambdaPhi)
+        case RightSubstEq(bot, t1, equals, lambdaPhi) =>
+          proofDOS.writeByte(rightSubstEq)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeShort(equals.size)
+          equals.foreach(t =>
+            proofDOS.writeInt(lineOfTerm(t._1))
+            proofDOS.writeInt(lineOfTerm(t._2))
+          )
+          ltfToProofDOS(lambdaPhi)
+        case LeftSubstIff(bot, t1, equals, lambdaPhi) =>
+          proofDOS.writeByte(leftSubstIff)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeShort(equals.size)
+          equals.foreach(t =>
+            proofDOS.writeInt(lineOfFormula(t._1))
+            proofDOS.writeInt(lineOfFormula(t._2))
+          )
+          lffToProofDOS(lambdaPhi)
+        case RightSubstIff(bot, t1, equals, lambdaPhi) =>
+          proofDOS.writeByte(rightSubstIff)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeShort(equals.size)
+          equals.foreach(t =>
+            proofDOS.writeInt(lineOfFormula(t._1))
+            proofDOS.writeInt(lineOfFormula(t._2))
+          )
+          lffToProofDOS(lambdaPhi)
+        case InstSchema(bot, t1, mCon, mPred, mTerm) =>
+          proofDOS.writeByte(instSchema)
+          sequentToProofDOS(bot)
+          proofDOS.writeInt(t1)
+          proofDOS.writeShort(mCon.size)
+          mCon.foreach(t =>
+            formulaLabelToDOS(t._1, proofDOS)
+            lffToProofDOS(t._2)
+          )
+          proofDOS.writeShort(mPred.size)
+          mPred.foreach(t =>
+            formulaLabelToDOS(t._1, proofDOS)
+            ltfToProofDOS(t._2)
+          )
+          proofDOS.writeShort(mTerm.size)
+          mTerm.foreach(t =>
+            termLabelToDOS(t._1, proofDOS)
+            lttToProofDOS(t._2)
+          )
+        case SCSubproof(sp, premises) => throw new Exception("Cannot support subproofs, flatten the proof first.")
+        case Sorry(bot) =>
+          proofDOS.writeByte(sorry)
+          sequentToProofDOS(bot)
+      }
+    }
+
+    proofDOS.writeShort(theorems.size)
+    theorems.foreach((thmName, proof, justifications) =>
+      proofDOS.writeUTF(thmName)
+      proofDOS.writeShort(justifications.size)
+      justifications.foreach(j => proofDOS.writeUTF(j))
+      proofDOS.writeInt(proof.imports.size)
+      proof.imports.foreach(sequent => sequentToProofDOS(sequent))
+      proofDOS.writeInt(proof.steps.size)
+      proof.steps.foreach(ps => proofStepToProofDOS(ps))
+    )
+
+  }
+
+  /**
+   * This functions reverses the effect of proofToDataStream
+   *
+   * @param lines The lines of the "file" where the proof is stored
+   */
+  def proofsFromDataStream(treesDIS: DataInputStream, proofDIS: DataInputStream): Seq[(String, SCProof, List[String])] = {
+
+    val termMap = MutMap[Line, Term]()
+    val formulaMap = MutMap[Line, Formula]()
+
+    // Read a label from the tree file, reversing the effect of termLabelToDOS and formulaLabelToDOS.
+    def labelFromInputStream(dis: DataInputStream): Label = {
+      val labelType = dis.readByte()
+      labelType match
+        case 0 =>
+          val name = dis.readUTF()
+          val no = dis.readInt()
+          val arity = dis.readInt()
+          ConstantFunctionLabel(Identifier(name, no), arity)
+        case 1 =>
+          val name = dis.readUTF()
+          val no = dis.readInt()
+          val arity = dis.readInt()
+          SchematicFunctionLabel(Identifier(name, no), arity)
+        case 2 =>
+          val name = dis.readUTF()
+          val no = dis.readInt()
+          VariableLabel(Identifier(name, no))
+        case 3 =>
+          val name = dis.readUTF()
+          val no = dis.readInt()
+          val arity = dis.readInt()
+          ConstantPredicateLabel(Identifier(name, no), arity)
+        case 4 =>
+          val name = dis.readUTF()
+          val no = dis.readInt()
+          val arity = dis.readInt()
+          SchematicPredicateLabel(Identifier(name, no), arity)
+        case 5 =>
+          val name = dis.readUTF()
+          name match
+            case And.id.name => And
+            case Or.id.name => Or
+            case Implies.id.name => Implies
+            case Iff.id.name => Iff
+            case Neg.id.name => Neg
+        case 6 =>
+          val name = dis.readUTF()
+          val no = dis.readInt()
+          val arity = dis.readInt()
+          SchematicConnectorLabel(Identifier(name, no), arity)
+        case 7 =>
+          val name = dis.readUTF()
+          val no = dis.readInt()
+          VariableFormulaLabel(Identifier(name, no))
+        case 8 =>
+          dis.readUTF() match
+            case Forall.id.name => Forall
+            case Exists.id.name => Exists
+            case ExistsOne.id.name => ExistsOne
+
+    }
+
+    // Read and reconstruct all the terms and formulas in the tree file. Fill the table with it.
+    var lineNo = -1
+    try {
+      while true do
+        lineNo = lineNo + 1
+        val label = labelFromInputStream(treesDIS)
+        label match
+          case l: TermLabel =>
+            val args = (1 to l.arity).map(_ => termMap(treesDIS.readInt())).toSeq
+            termMap(lineNo) = Term(l, args)
+          case l: FormulaLabel =>
+            val formula = label match
+              case l: PredicateLabel =>
+                val args = (1 to l.arity).map(_ => termMap(treesDIS.readInt())).toSeq
+                PredicateFormula(l, args)
+              case l: ConnectorLabel =>
+                val ar = treesDIS.readShort()
+                val args = (1 to ar).map(_ => formulaMap(treesDIS.readInt())).toSeq
+                ConnectorFormula(l, args)
+              case l: BinderLabel =>
+                BinderFormula(l, labelFromInputStream(treesDIS).asInstanceOf[VariableLabel], formulaMap(treesDIS.readInt()))
+            formulaMap(lineNo) = formula
+    } catch
+      case _: EOFException =>
+        ()
+
+    // Terms and Formulas finished, deal with the proof now.
+
+    def lttFromProofDIS(): LambdaTermTerm =
+      val vars = (1 to proofDIS.readShort()).map(_ => labelFromInputStream(proofDIS).asInstanceOf[VariableLabel]).toSeq
+      val body = termMap(proofDIS.readInt())
+      LambdaTermTerm(vars, body)
+
+    def ltfFromProofDIS(): LambdaTermFormula =
+      val vars = (1 to proofDIS.readShort()).map(_ => labelFromInputStream(proofDIS).asInstanceOf[VariableLabel]).toSeq
+      val body = formulaMap(proofDIS.readInt())
+      LambdaTermFormula(vars, body)
+
+    def lffFromProofDIS(): LambdaFormulaFormula =
+      val vars = (1 to proofDIS.readShort()).map(_ => labelFromInputStream(proofDIS).asInstanceOf[VariableFormulaLabel]).toSeq
+      val body = formulaMap(proofDIS.readInt())
+      LambdaFormulaFormula(vars, body)
+
+    def sequentFromProofDIS(): Sequent =
+      val leftSize = proofDIS.readShort()
+      val left = (1 to leftSize).map(_ => formulaMap(proofDIS.readInt())).toSet
+      val rightSize = proofDIS.readShort()
+      val right = (1 to rightSize).map(_ => formulaMap(proofDIS.readInt())).toSet
+      Sequent(left, right)
+
+    // Read a proof step from the proof file. Inverse of proofStepToProofDOS
+    def proofStepFromProofDIS(): SCProofStep = {
+      val psType = proofDIS.readByte()
+      if (psType == restate) Restate(sequentFromProofDIS(), proofDIS.readInt())
+      else if (psType == restateTrue) RestateTrue(sequentFromProofDIS())
+      else if (psType == hypothesis) Hypothesis(sequentFromProofDIS(), formulaMap(proofDIS.readInt()))
+      else if (psType == cut) Cut(sequentFromProofDIS(), proofDIS.readInt(), proofDIS.readInt(), formulaMap(proofDIS.readInt()))
+      else if (psType == leftAnd) LeftAnd(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), formulaMap(proofDIS.readInt()))
+      else if (psType == leftOr)
+        LeftOr(
+          sequentFromProofDIS(),
+          (1 to proofDIS.readShort()).map(_ => proofDIS.readInt()).toSeq,
+          (1 to proofDIS.readShort()).map(_ => formulaMap(proofDIS.readInt())).toSeq
+        )
+      else if (psType == leftImplies) LeftImplies(sequentFromProofDIS(), proofDIS.readInt(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), formulaMap(proofDIS.readInt()))
+      else if (psType == leftIff) LeftIff(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), formulaMap(proofDIS.readInt()))
+      else if (psType == leftNot) LeftNot(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()))
+      else if (psType == leftForall)
+        LeftForall(
+          sequentFromProofDIS(),
+          proofDIS.readInt(),
+          formulaMap(proofDIS.readInt()),
+          labelFromInputStream(proofDIS).asInstanceOf[VariableLabel],
+          termMap(proofDIS.readInt())
+        )
+      else if (psType == leftExists) LeftExists(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), labelFromInputStream(proofDIS).asInstanceOf[VariableLabel])
+      else if (psType == leftExistsOne) LeftExistsOne(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), labelFromInputStream(proofDIS).asInstanceOf[VariableLabel])
+      else if (psType == rightAnd)
+        RightAnd(
+          sequentFromProofDIS(),
+          (1 to proofDIS.readShort()).map(_ => proofDIS.readInt()).toSeq,
+          (1 to proofDIS.readShort()).map(_ => formulaMap(proofDIS.readInt())).toSeq
+        )
+      else if (psType == rightOr) RightOr(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), formulaMap(proofDIS.readInt()))
+      else if (psType == rightImplies) RightImplies(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), formulaMap(proofDIS.readInt()))
+      else if (psType == rightIff) RightIff(sequentFromProofDIS(), proofDIS.readInt(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), formulaMap(proofDIS.readInt()))
+      else if (psType == rightNot) RightNot(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()))
+      else if (psType == rightForall) RightForall(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), labelFromInputStream(proofDIS).asInstanceOf[VariableLabel])
+      else if (psType == rightExists)
+        RightExists(
+          sequentFromProofDIS(),
+          proofDIS.readInt(),
+          formulaMap(proofDIS.readInt()),
+          labelFromInputStream(proofDIS).asInstanceOf[VariableLabel],
+          termMap(proofDIS.readInt())
+        )
+      else if (psType == rightExistsOne) RightExistsOne(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()), labelFromInputStream(proofDIS).asInstanceOf[VariableLabel])
+      else if (psType == weakening) Weakening(sequentFromProofDIS(), proofDIS.readInt())
+      else if (psType == leftRefl) LeftRefl(sequentFromProofDIS(), proofDIS.readInt(), formulaMap(proofDIS.readInt()))
+      else if (psType == rightRefl) RightRefl(sequentFromProofDIS(), formulaMap(proofDIS.readInt()))
+      else if (psType == leftSubstEq)
+        LeftSubstEq(
+          sequentFromProofDIS(),
+          proofDIS.readInt(),
+          (1 to proofDIS.readShort()).map(_ => (termMap(proofDIS.readInt()), termMap(proofDIS.readInt()))).toList,
+          ltfFromProofDIS()
+        )
+      else if (psType == rightSubstEq)
+        RightSubstEq(
+          sequentFromProofDIS(),
+          proofDIS.readInt(),
+          (1 to proofDIS.readShort()).map(_ => (termMap(proofDIS.readInt()), termMap(proofDIS.readInt()))).toList,
+          ltfFromProofDIS()
+        )
+      else if (psType == leftSubstIff)
+        LeftSubstIff(
+          sequentFromProofDIS(),
+          proofDIS.readInt(),
+          (1 to proofDIS.readShort()).map(_ => (formulaMap(proofDIS.readInt()), formulaMap(proofDIS.readInt()))).toList,
+          lffFromProofDIS()
+        )
+      else if (psType == rightSubstIff)
+        RightSubstIff(
+          sequentFromProofDIS(),
+          proofDIS.readInt(),
+          (1 to proofDIS.readShort()).map(_ => (formulaMap(proofDIS.readInt()), formulaMap(proofDIS.readInt()))).toList,
+          lffFromProofDIS()
+        )
+      else if (psType == instSchema)
+        InstSchema(
+          sequentFromProofDIS(),
+          proofDIS.readInt(),
+          (1 to proofDIS.readShort()).map(_ => (labelFromInputStream(proofDIS).asInstanceOf[SchematicConnectorLabel], lffFromProofDIS())).toMap,
+          (1 to proofDIS.readShort()).map(_ => (labelFromInputStream(proofDIS).asInstanceOf[SchematicVarOrPredLabel], ltfFromProofDIS())).toMap,
+          (1 to proofDIS.readShort()).map(_ => (labelFromInputStream(proofDIS).asInstanceOf[SchematicTermLabel], lttFromProofDIS())).toMap
+        )
+      else if (psType == sorry) Sorry(sequentFromProofDIS())
+      else throw new Exception("Unknown proof step type: " + psType)
+    }
+
+    // for each given theorem, write it to the file.
+    val numberThm = proofDIS.readShort()
+    (1 to numberThm)
+      .map(_ =>
+        val thmName = proofDIS.readUTF()
+        val justificationsSize = proofDIS.readShort()
+        val justifications = (1 to justificationsSize).map(_ => proofDIS.readUTF()).toList
+        val importsSize = proofDIS.readInt()
+        val imports = (1 to importsSize).map(_ => sequentFromProofDIS()).toSeq
+        val noSteps = proofDIS.readInt()
+        val steps = (1 to noSteps).map(_ => proofStepFromProofDIS()).toSeq
+
+        (thmName, new SCProof(steps.toIndexedSeq, imports.toIndexedSeq), justifications)
+      )
+      .toSeq
+
+  }
+
+  /**
+   * Write a list of theorems to a pair of OutputStrem, one for the formulas and term trees, one for the proof.
+   * Each theorem has a name, a proof and a list of justifications, with a name for those justifications that can be fetched in the code base.
+   */
+  def thmsToDataStream(treesDOS: DataOutputStream, proofDOS: DataOutputStream, theory: RunningTheory, theorems: List[(String, SCProof, List[(String, theory.Justification)])]): Unit = {
+    proofsToDataStream(
+      treesDOS,
+      proofDOS,
+      theorems.map((name, proof, justs) =>
+        val justNames = justs.map {
+          case (obj, theory.Axiom(name, ax)) => "a" + obj + "$" + name
+          case (obj, theory.Theorem(name, proposition, withSorry)) => "t" + obj + "$" + name
+          case (obj, theory.FunctionDefinition(label, out, expression, withSorry)) => "f" + obj + "$" + label.id.name + "_" + label.id.no + "_" + label.arity
+          case (obj, theory.PredicateDefinition(label, expression)) => "p" + obj + "$" + label.id.name + "_" + label.id.no + "_" + label.arity
+        }
+        (name, minimizeProofOnce(proof), justNames)
+      )
+    )
+  }
+
+  /**
+   * Read theorems from two files, one for the formulas and term trees, one for the proof.
+   * Theorems are validated in the kernel. Justifications are fetched from the code base using the name written in the file.
+   * This uses java reflection, for example to obtain the theorem [[scala.mathematics.settheory.SetTheoryLibrary.russelsParadox]] from the
+   * string "scala.mathematics.settheory.SetTheoryLibrary.russelsParadox".
+   * A bit ugly, but don't really have better for now.
+   */
+  def thmsFromDataStream(treesDIS: DataInputStream, proofDIS: DataInputStream, theory: RunningTheory, debug: Boolean = false): Seq[(theory.Theorem, SCProof)] = {
+    proofsFromDataStream(treesDIS, proofDIS).map { (name, proof, justifications) =>
+      val justs = justifications.map { j =>
+        val nl = j.tail
+        val Array(obj, name) = nl.split("\\$")
+        try {
+          Class.forName(obj + "$").getField("MODULE$").get(null)
+        } catch { case _ => "Not found: " + obj }
+        Class.forName("lisa.settheory.SetTheoryLibrary" + "$").getField("MODULE$").get(null)
+
+        j(0) match
+          case 'a' => theory.getAxiom(name).get
+          case 't' =>
+            theory.getTheorem(name).get
+          case 'f' =>
+            name.split("_") match
+              case Array(name, no, arity) => theory.getDefinition(ConstantFunctionLabel(Identifier(name, no.toInt), arity.toInt)).get
+          case 'p' =>
+            name.split("_") match
+              case Array(name, no, arity) => theory.getDefinition(ConstantPredicateLabel(Identifier(name, no.toInt), arity.toInt)).get
+      }
+      if debug then
+        // To avoid conflicts where a theorem already exists, for example in test suits.
+        (theory.makeTheorem(name + "_test", proof.conclusion, proof, justs).get, proof)
+      else (theory.makeTheorem(name, proof.conclusion, proof, justs).get, proof)
+    }
+
+  }
+
+  /**
+   * Write a list of theorems to a pair file, one for the formulas and term trees, one for the proof.
+   * Each theorem has a name, a proof and a list of justifications, with a name for those justifications that can be fetched in the code base.
+   */
+  def thmsToFile(filename: String, theory: RunningTheory, theorems: List[(String, SCProof, List[(String, theory.Justification)])]): Unit = {
+    val directory = File(filename).getParentFile()
+    if (directory != null) && !directory.exists() then directory.mkdirs()
+    val treeFile = File(filename + ".trees")
+    if !treeFile.exists() then treeFile.createNewFile()
+    val proofFile = File(filename + ".proof")
+    if !proofFile.exists() then proofFile.createNewFile()
+    val treesDOS = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(treeFile)))
+    val proofDOS = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(proofFile)))
+    thmsToDataStream(treesDOS, proofDOS, theory, theorems)
+    treesDOS.close()
+    proofDOS.close()
+  }
+
+  /**
+   * Read theorems from two files, one for the formulas and term trees, one for the proof.
+   * Theorems are validated in the kernel. Justifications are fetched from the code base using the name written in the file.
+   */
+  def thmsFromFile(filename: String, theory: RunningTheory): Seq[(theory.Theorem, SCProof)] = {
+    val treesDIS = new DataInputStream(new BufferedInputStream(new FileInputStream(File(filename + ".trees"))))
+    val proofDIS = new DataInputStream(new BufferedInputStream(new FileInputStream(File(filename + ".proof"))))
+    val thm = thmsFromDataStream(treesDIS, proofDIS, theory, false)
+    treesDIS.close()
+    proofDIS.close()
+    thm
+  }
+
+  /**
+   * Same as [[thmsFromFile]] but only returns the first theorem (usually because we know there is only one theorem in the file).
+   */
+  def oneThmFromFile(filename: String, theory: RunningTheory): Option[theory.Theorem] = {
+    val treeFile = File(filename + ".trees")
+    val proofFile = File(filename + ".proof")
+    if treeFile.isFile() && proofFile.isFile() then
+      val treesDIS = new DataInputStream(new BufferedInputStream(new FileInputStream(treeFile)))
+      val proofDIS = new DataInputStream(new BufferedInputStream(new FileInputStream(proofFile)))
+      val thm = thmsFromDataStream(treesDIS, proofDIS, theory, false)
+      treesDIS.close()
+      proofDIS.close()
+      Some(thm.head._1)
+    else None
+  }
+
+}
diff --git a/lisa-utils/src/test/scala/lisa/test/utils/ProofTacticTestLib.scala b/lisa-utils/src/test/scala/lisa/test/utils/ProofTacticTestLib.scala
index a963d446af61c5fd4b1dc5eaca3d627891559fa1..5d7ccd074dbd1c53be9350b867fabab26ba57175 100644
--- a/lisa-utils/src/test/scala/lisa/test/utils/ProofTacticTestLib.scala
+++ b/lisa-utils/src/test/scala/lisa/test/utils/ProofTacticTestLib.scala
@@ -17,7 +17,7 @@ class ProofTacticTestLib extends AnyFunSuite with BasicMain {
   private val P = predicate[1]
 
   // generate a placeholde theorem to take ownership of proofs for test
-  val placeholderTheorem = Theorem(P(x) |- P(x)) { have(P(x) |- P(x)) by Hypothesis }
+  val placeholderTheorem: THMFromProof = Theorem(P(x) |- P(x)) { have(P(x) |- P(x)) by Hypothesis }.asInstanceOf
 
   // generates an empty proof owned by the placeholder theorem for testing
   def generateTestProof() = new BaseProof(placeholderTheorem)
diff --git a/src/main/scala/lisa/settheory/SetTheoryTGAxioms.scala b/src/main/scala/lisa/settheory/SetTheoryTGAxioms.scala
index ff036871a58a4843e4282d65d098789952eb693b..9f4b15a4360f643fa829cea5f562f48019f81ff9 100644
--- a/src/main/scala/lisa/settheory/SetTheoryTGAxioms.scala
+++ b/src/main/scala/lisa/settheory/SetTheoryTGAxioms.scala
@@ -12,7 +12,6 @@ private[settheory] trait SetTheoryTGAxioms extends SetTheoryZFAxioms {
   private val z = variable
 
   final val tarskiAxiom: AXIOM = Axiom(
-    "tarskiAxiom",
     forall(
       x,
       in(x, universe(x)) /\
diff --git a/src/main/scala/lisa/settheory/SetTheoryZAxioms.scala b/src/main/scala/lisa/settheory/SetTheoryZAxioms.scala
index 6e11a9f933fa198340c115db9e1181acb492b77e..192ff736a32165b38ac7f532830a8a56af23138e 100644
--- a/src/main/scala/lisa/settheory/SetTheoryZAxioms.scala
+++ b/src/main/scala/lisa/settheory/SetTheoryZAxioms.scala
@@ -19,7 +19,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    *
    * `() |- (x = y) ⇔ ∀ z. z ∈ x ⇔ z ∈ y`
    */
-  final val extensionalityAxiom: this.AXIOM = Axiom("extensionalityAxiom", forall(z, in(z, x) <=> in(z, y)) <=> (x === y))
+  final val extensionalityAxiom: this.AXIOM = Axiom(forall(z, in(z, x) <=> in(z, y)) <=> (x === y))
 
   /**
    * Pairing Axiom --- For any sets `x` and `y`, there is a set that contains
@@ -31,7 +31,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    * This axiom defines [[unorderedPair]] as the function symbol representing
    * this set.
    */
-  final val pairAxiom: AXIOM = Axiom("pairAxiom", in(z, unorderedPair(x, y)) <=> (x === z) \/ (y === z))
+  final val pairAxiom: AXIOM = Axiom(in(z, unorderedPair(x, y)) <=> (x === z) \/ (y === z))
 
   /**
    * Comprehension/Separation Schema --- For a formula `ϕ(_, _)` and a set `z`,
@@ -44,7 +44,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    * This schema represents an infinite collection of axioms, one for each
    * formula `ϕ(x, z)`.
    */
-  final val comprehensionSchema: AXIOM = Axiom("comprehensionSchema", exists(y, forall(x, in(x, y) <=> (in(x, z) /\ φ(x, z)))))
+  final val comprehensionSchema: AXIOM = Axiom(exists(y, forall(x, in(x, y) <=> (in(x, z) /\ φ(x, z)))))
 
   /**
    * Empty Set Axiom --- From the Comprehension Schema follows the existence of
@@ -56,7 +56,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    *
    * `() |- !(x ∈ ∅)`
    */
-  final val emptySetAxiom: AXIOM = Axiom("emptySetAxiom", !in(x, emptySet))
+  final val emptySetAxiom: AXIOM = Axiom(!in(x, emptySet))
 
   /**
    * Union Axiom --- For any set `x`, there exists a set `union(x)` which is the
@@ -69,7 +69,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    *
    * This axiom defines [[union]] as the function symbol representing this set.
    */
-  final val unionAxiom: AXIOM = Axiom("unionAxiom", in(z, union(x)) <=> exists(y, in(y, x) /\ in(z, y)))
+  final val unionAxiom: AXIOM = Axiom(in(z, union(x)) <=> exists(y, in(y, x) /\ in(z, y)))
 
   /**
    * Subset Axiom --- For sets `x` and `y`, `x` is a subset of `y` iff every
@@ -79,7 +79,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    *
    * This axiom defines the [[subset]] symbol as this predicate.
    */
-  final val subsetAxiom: AXIOM = Axiom("subsetAxiom", subset(x, y) <=> forall(z, in(z, x) ==> in(z, y)))
+  final val subsetAxiom: AXIOM = Axiom(subset(x, y) <=> forall(z, in(z, x) ==> in(z, y)))
 
   /**
    * Power Set Axiom --- For a set `x`, there exists a power set of `x`, denoted
@@ -90,7 +90,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    * This axiom defines [[powerSet]] as the function symbol representing this
    * set.
    */
-  final val powerAxiom: AXIOM = Axiom("powerAxiom", in(x, powerSet(y)) <=> subset(x, y))
+  final val powerAxiom: AXIOM = Axiom(in(x, powerSet(y)) <=> subset(x, y))
 
   /**
    * Infinity Axiom --- There exists an infinite set.
@@ -105,7 +105,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    *
    * `() |- ∃ x. inductive(x)`
    */
-  final val infinityAxiom: AXIOM = Axiom("infinityAxiom", exists(x, in(emptySet, x) /\ forall(y, in(y, x) ==> in(union(unorderedPair(y, unorderedPair(y, y))), x))))
+  final val infinityAxiom: AXIOM = Axiom(exists(x, in(emptySet, x) /\ forall(y, in(y, x) ==> in(union(unorderedPair(y, unorderedPair(y, y))), x))))
 
   /**
    * Foundation/Regularity Axiom --- Every non-empty set `x` has an `∈`-minimal
@@ -114,7 +114,7 @@ private[settheory] trait SetTheoryZAxioms extends SetTheoryDefinitions {
    *
    * `() |- (x != ∅) ==> ∃ y ∈ x. ∀ z. z ∈ x ⇒ ! z ∈ y`
    */
-  final val foundationAxiom: AXIOM = Axiom("foundationAxiom", !(x === emptySet) ==> exists(y, in(y, x) /\ forall(z, in(z, x) ==> !in(z, y))))
+  final val foundationAxiom: AXIOM = Axiom(!(x === emptySet) ==> exists(y, in(y, x) /\ forall(z, in(z, x) ==> !in(z, y))))
 
   private val zAxioms: Set[(String, AXIOM)] = Set(
     ("EmptySet", emptySetAxiom),
diff --git a/src/main/scala/lisa/settheory/SetTheoryZFAxioms.scala b/src/main/scala/lisa/settheory/SetTheoryZFAxioms.scala
index aa363204de7cb3e14c0655dd560accb205740c9e..56c314f0503ea0b7e8c619d811bfa2adef9c5e01 100644
--- a/src/main/scala/lisa/settheory/SetTheoryZFAxioms.scala
+++ b/src/main/scala/lisa/settheory/SetTheoryZFAxioms.scala
@@ -22,7 +22,6 @@ private[settheory] trait SetTheoryZFAxioms extends SetTheoryZAxioms {
    * satisfy `P` for each `a ∈ x`.
    */
   final val replacementSchema: AXIOM = Axiom(
-    "replacementSchema",
     forall(A, in(A, x) ==> existsOne(B, P(x, A, B))) ==>
       exists(y, forall(B, in(B, y) <=> exists(A, in(A, x) /\ P(x, A, B))))
   )
diff --git a/src/test/scala/lisa/automation/TableauTest.scala b/src/test/scala/lisa/automation/TableauTest.scala
new file mode 100644
index 0000000000000000000000000000000000000000..4e11402ec8396ec594bbddcd14411c7eeb3793be
--- /dev/null
+++ b/src/test/scala/lisa/automation/TableauTest.scala
@@ -0,0 +1,130 @@
+package lisa.test.automation
+
+import lisa.automation.Tableau._
+import lisa.prooflib.Exports.*
+import lisa.prooflib.Substitution.*
+import lisa.settheory.SetTheoryLibrary.{_, given}
+import lisa.utils.K.SCProofChecker.checkSCProof
+import lisa.utils.parsing.FOLPrinter.prettyFormula
+import lisa.utils.parsing.FOLPrinter.prettySCProof
+import lisa.utils.parsing.FOLPrinter.prettyTerm
+import org.scalatest.funsuite.AnyFunSuite
+
+class TableauTest extends AnyFunSuite {
+  import TableauTest.*
+
+  test(s"Propositional Positive cases (${posi.size})") {
+    assert(posi.forall(_._3), posi.map((i, f, b, proof, judg) => s"$i $b" + (if !b then s" $f" else "")).mkString("\n"))
+    if posi.exists(tup => tup._5.nonEmpty & !tup._5.get.isValid) then
+      fail("A proof is wrong: " + posi.map(tup => if tup._5.nonEmpty & !tup._5.get.isValid then prettySCProof(tup._5.get) + "\n").mkString("\n"))
+  }
+
+  test(s"First Order Quantifier Free Positive cases (${posqf.size})") {
+    assert(posqf.forall(_._3), posqf.map((i, f, b, proof, judg) => (s"$i $b" + (if !b then s" $f" else ""))).mkString("\n"))
+    if posqf.exists(tup => tup._5.nonEmpty & !tup._5.get.isValid) then
+      fail("A proof is wrong: " + posi.map(tup => if tup._5.nonEmpty & !tup._5.get.isValid then prettySCProof(tup._5.get) + "\n").mkString("\n"))
+  }
+
+  test(s"First Order Easy Positive cases (${poseasy.size})") {
+    assert(poseasy.forall(_._3), poseasy.map((i, f, b, proof, judg) => (s"$i $b" + (if !b then s" $f" else ""))).mkString("\n"))
+    if poseasy.exists(tup => tup._5.nonEmpty & !tup._5.get.isValid) then
+      fail("A proof is wrong: " + posi.map(tup => if tup._5.nonEmpty & !tup._5.get.isValid then prettySCProof(tup._5.get) + "\n").mkString("\n"))
+  }
+
+  test(s"First Order Hard Positive cases (${poshard.size})") {
+    assert(poshard.forall(_._3), poshard.map((i, f, b, proof, judg) => (s"$i $b" + (if !b then s" $f" else ""))).mkString("\n"))
+    if poshard.exists(tup => tup._5.nonEmpty & !tup._5.get.isValid) then
+      fail("A proof is wrong: " + posi.map(tup => if tup._5.nonEmpty & !tup._5.get.isValid then prettySCProof(tup._5.get) + "\n").mkString("\n"))
+  }
+
+}
+object TableauTest {
+
+  val w = variable
+  val x = variable
+  val y = variable
+  val z = variable
+
+  val a = formulaVariable
+  val b = formulaVariable
+  val c = formulaVariable
+  val d = formulaVariable
+  val e = formulaVariable
+
+  val f = function[1]
+  val g = function[1]
+  val h = function[2]
+
+  val P = predicate[1]
+  val Q = predicate[1]
+  val R = predicate[2]
+
+  var doprint: Boolean = false
+  def printif(s: Any) = if doprint then println(s) else ()
+
+  val posi = List(
+    a <=> a,
+    a \/ !a,
+    ((a ==> b) /\ (b ==> c)) ==> (a ==> c),
+    (a <=> b) <=> ((a /\ b) \/ (!a /\ !b)),
+    ((a ==> c) /\ (b ==> c)) <=> ((a \/ b) ==> c),
+    ((a ==> b) /\ (c ==> d)) ==> ((a \/ c) ==> (b \/ d))
+  ).zipWithIndex.map(f =>
+    val res = solve(() |- f._1)
+    (f._2, f._1, res.nonEmpty, res, res.map(checkSCProof))
+  )
+
+  // Quantifier Free
+
+  val posqf = List(
+    posi.map(fo => fo._2.substitute(a := P(h(x, y)), b := P(x), c := R(x, h(x, y)))),
+    posi.map(fo => fo._2.substitute(a := P(h(x, y)), b := P(h(x, y)), c := R(x, h(x, f(x))))),
+    posi.map(fo => fo._2.substitute(a := R(y, y), b := P(h(y, y)), c := R(h(x, y), h(z, y))))
+  ).flatten.zipWithIndex.map(f =>
+    val res = solve(() |- f._1)
+    (f._2, f._1, res.nonEmpty, res, res.map(checkSCProof))
+  )
+
+  // First Order Easy
+
+  val poseasy = List(
+    posi.map(fo => fo._2.substitute(a := forall(x, P(x)), b := forall(x, P(y)), c := exists(x, P(x)))),
+    posi.map(fo => fo._2.substitute(a := forall(x, P(x) /\ Q(f(x))), b := forall(x, P(x) \/ R(y, x)), c := exists(x, Q(x) ==> R(x, y)))),
+    posi.map(fo => fo._2.substitute(a := exists(y, forall(x, P(x) /\ Q(f(y)))), b := forall(y, exists(x, P(x) \/ R(y, x))), c := forall(y, exists(x, Q(x) ==> R(x, y)))))
+  ).flatten.zipWithIndex.map(f =>
+    val res = solve(() |- f._1)
+    (f._2, f._1, res.nonEmpty, res, res.map(checkSCProof))
+  )
+
+  // First Order Hard, from https://isabelle.in.tum.de/library/FOL/FOL-ex/Quantifiers_Cla.html
+
+  val poshard = List(
+    forall(x, P(x) ==> Q(x)) ==> (forall(x, P(x)) ==> forall(x, Q(x))),
+    forall(x, forall(y, R(x, y))) ==> forall(y, forall(x, R(x, y))),
+    forall(x, forall(y, R(x, y))) ==> forall(y, forall(x, R(y, x))),
+    exists(x, exists(y, R(x, y))) ==> exists(y, exists(x, R(x, y))),
+    exists(x, exists(y, R(x, y))) ==> exists(y, exists(x, R(y, x))),
+    (forall(x, P(x)) \/ forall(x, Q(x))) ==> forall(x, P(x) \/ Q(x)),
+    forall(x, a ==> Q(x)) <=> (a ==> forall(x, Q(x))),
+    forall(x, P(x) ==> a) <=> (exists(x, P(x)) ==> a),
+    exists(x, P(x) \/ Q(x)) <=> (exists(x, P(x)) \/ exists(x, Q(x))),
+    exists(x, P(x) /\ Q(x)) ==> (exists(x, P(x)) /\ exists(x, Q(x))),
+    exists(y, forall(x, R(x, y))) ==> forall(x, exists(y, R(x, y))),
+    forall(x, Q(x)) ==> exists(x, Q(x)),
+    (forall(x, P(x) ==> Q(x)) /\ exists(x, P(x))) ==> exists(x, Q(x)),
+    ((a ==> exists(x, Q(x))) /\ a) ==> exists(x, Q(x)),
+    forall(x, P(x) ==> Q(f(x))) /\ forall(x, Q(x) ==> R(g(x), x)) ==> (P(y) ==> R(g(f(y)), f(y))),
+    forall(x, forall(y, P(x) ==> Q(y))) ==> (exists(x, P(x)) ==> forall(y, Q(y))),
+    (exists(x, P(x)) ==> forall(y, Q(y))) ==> forall(x, forall(y, P(x) ==> Q(y))),
+    forall(x, forall(y, P(x) ==> Q(y))) <=> (exists(x, P(x)) ==> forall(y, Q(y))),
+    exists(x, exists(y, P(x) /\ R(x, y))) ==> (exists(x, P(x) /\ exists(y, R(x, y)))),
+    (exists(x, P(x) /\ exists(y, R(x, y)))) ==> exists(x, exists(y, P(x) /\ R(x, y))),
+    exists(x, exists(y, P(x) /\ R(x, y))) <=> (exists(x, P(x) /\ exists(y, R(x, y)))),
+    exists(y, forall(x, P(x) ==> R(x, y))) ==> (forall(x, P(x)) ==> exists(y, R(x, y))),
+    forall(x, P(x)) ==> P(y)
+  ).zipWithIndex.map(f =>
+    val res = solve(() |- f._1)
+    (f._2, f._1, res.nonEmpty, res, res.map(checkSCProof))
+  )
+
+}
diff --git a/src/test/scala/lisa/utilities/SerializationTest.scala b/src/test/scala/lisa/utilities/SerializationTest.scala
new file mode 100644
index 0000000000000000000000000000000000000000..da2ba1aa5de8d535300ba6020f8315ef9e1ee609
--- /dev/null
+++ b/src/test/scala/lisa/utilities/SerializationTest.scala
@@ -0,0 +1,187 @@
+package lisa.test.utils
+
+import lisa.test.automation.TableauTest
+import lisa.utils.K
+import lisa.utils.K.getJustification
+import lisa.utils.K.|-
+import lisa.utils.Serialization.*
+import org.scalatest.funsuite.AnyFunSuite
+
+import java.io._
+
+//import lisa.automation.TableauTest
+
+class SerializationTest extends AnyFunSuite {
+
+  val theory = K.RunningTheory()
+
+  val testfile = "SerializationTestuioavrebvtaevslbxfgh" // chances of collision with an existing file is quite low
+
+  def test(proof: K.SCProof, name: String, theory: K.RunningTheory, justs: List[theory.Justification]) = {
+    thmsToFile(testfile, theory, List((name, proof, justs.map(("test", _)))))
+    val thm = thmsFromFile(testfile, theory)
+    File(testfile + ".trees").delete()
+    File(testfile + ".proof").delete()
+    thm.head
+  }
+
+  def testMulti(theory: K.RunningTheory, thms: List[(String, K.SCProof, List[theory.Justification])]) = {
+    thmsToFile(testfile, theory, thms.map(thm => (thm._1, thm._2, thm._3.map(("test", _)))))
+    val thm = thmsFromFile(testfile, theory)
+    File(testfile + "test.trees").delete()
+    File(testfile + "test.proof").delete()
+    thm
+  }
+
+  test("exporting a proof to a file and back should work, propositional tableau") {
+    val proofs = TableauTest.posi
+    proofs.foreach(p =>
+      try {
+        val proof = p._4.get
+        val no = p._1
+        test(proof, "posi" + no, theory, Nil)
+      } catch {
+        case e: Exception =>
+          println("Exception thrown for test case posi" + p._1)
+          throw e
+      }
+    )
+  }
+
+  test("exporting a proof to a file and back should work, propositional OL tautology") {
+    val proofs = TableauTest.posi
+    proofs.foreach(p =>
+      try {
+        val formula = p._2
+        val no = p._1
+        val proof = lisa.automation.kernel.OLPropositionalSolver.solveSequent(() |- formula.underlying) match {
+          case Left(proof) => proof
+          case Right(_) => throw new Exception("OLPropositionalSolver failed to prove a tautology")
+        }
+        test(proof, "posiOL" + no, theory, Nil)
+      } catch {
+        case e: Exception =>
+          println("Exception thrown for test case posi" + p._1)
+          throw e
+      }
+    )
+  }
+
+  test("exporting a proof to a file and back should work, first order quantifier free tableau") {
+    val proofs = TableauTest.posqf
+    proofs.foreach(p =>
+      try {
+        val proof = p._4.get
+        val no = p._1
+        test(proof, "posqf" + no, theory, Nil)
+      } catch {
+        case e: Exception =>
+          println("Exception thrown for test case posqf" + p._1)
+          throw e
+      }
+    )
+  }
+
+  test("exporting a proof to a file and back should work, first order quantifier free OL tautology") {
+    val proofs = TableauTest.posqf
+    proofs.foreach(p =>
+      try {
+        val formula = p._2
+        val no = p._1
+        val proof = lisa.automation.kernel.OLPropositionalSolver.solveSequent(() |- formula.underlying) match {
+          case Left(proof) => proof
+          case Right(_) => throw new Exception("OLPropositionalSolver failed to prove a tautology")
+        }
+        test(proof, "posqfOL" + no, theory, Nil)
+      } catch {
+        case e: Exception =>
+          println("Exception thrown for test case posqf" + p._1)
+          throw e
+      }
+    )
+  }
+
+  test("exporting a proof to a file and back should work, first order easy tableau") {
+    val proofs = TableauTest.poseasy
+    proofs.foreach(p =>
+      try {
+        val proof = p._4.get
+        val no = p._1
+        test(proof, "poseasy" + no, theory, Nil)
+      } catch {
+        case e: Exception =>
+          println("Exception thrown for test case poseasy" + p._1)
+          throw e
+      }
+    )
+  }
+
+  test("exporting a proof to a file and back should work, first order hard tableau") {
+    val proofs = TableauTest.poshard
+    proofs.foreach(p =>
+      try {
+        val proof = p._4.get
+        val no = p._1
+        test(proof, "poshard" + no, theory, Nil)
+      } catch {
+        case e: Exception =>
+          println("Exception thrown for test case poshard" + p._1)
+          throw e
+      }
+    )
+  }
+
+  test("exporting a proof to a file and back should work, with imports") {
+    import lisa.mathematics.settheory.SetTheory as ST
+    val thms = List(
+      // ("russelsParadox", ST.russelsParadox),
+      ("setUnionMembership", ST.setUnionMembership),
+      ("inductiveSetExists", ST.inductiveSetExists),
+      ("setWithNoElementsIsEmpty", ST.setWithNoElementsIsEmpty),
+      ("emptySetIsItsOwnOnlySubset", ST.emptySetIsItsOwnOnlySubset)
+    )
+    thms.foreach(thm =>
+      try {
+        val proof = thm._2.kernelProof.get
+        val justifs = thm._2.highProof.get.justifications.map(_.innerJustification)
+
+        test(proof, thm._1 + "_test", ST.theory, justifs)
+      } catch {
+        case e: Exception =>
+          println("Exception thrown for string encoding-decoding of theorem " + thm._1)
+          throw e
+      }
+    )
+  }
+
+  test("exporting multiple theorems at once to a file and back should work") {
+    import lisa.mathematics.settheory.SetTheory as ST
+    val thms = List(
+      // ("russelsParadox", ST.russelsParadox),
+      ("setUnionMembership", ST.setUnionMembership),
+      ("inductiveSetExists", ST.inductiveSetExists),
+      ("setWithNoElementsIsEmpty", ST.setWithNoElementsIsEmpty),
+      ("emptySetIsItsOwnOnlySubset", ST.emptySetIsItsOwnOnlySubset)
+    )
+
+    val thmBack = testMulti(
+      ST.theory,
+      thms.map(thm =>
+        val proof = thm._2.kernelProof.get
+        val justifs = thm._2.highProof.get.justifications.map(_.innerJustification)
+
+        (thm._1, proof, justifs)
+      )
+    )
+    assert(thmBack.length == thms.length)
+    thmBack
+      .zip(thms)
+      .foreach(pair => {
+        val thm = pair._1
+        val thmOrig = pair._2
+        assert(thm._1.name == thmOrig._1)
+        assert(thm._1.proposition == thmOrig._2.innerJustification.proposition)
+      })
+  }
+
+}