lean4-htt/src/LeanChecker.lean
Sebastian Ullrich 1361d733a6
feat: re-integrate lean4checker as leanchecker (#11887)
This PR makes the external checker lean4checker available as the
existing `leanchecker` binary already known to elan, allowing for
out-of-the-box access to it.

---------

Co-authored-by: Kim Morrison <kim@tqft.net>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 09:41:33 +00:00

115 lines
4.7 KiB
Text

/-
Copyright (c) 2023 Kim Morrison. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Kim Morrison, Sebastian Ullrich
-/
import Lean.CoreM
import Lean.Replay
import LeanChecker.Replay
import Lake.Load.Manifest
open Lean
unsafe def replayFromImports (module : Name) : IO Unit := do
let mFile ← findOLean module
unless (← mFile.pathExists) do
throw <| IO.userError s!"object file '{mFile}' of module {module} does not exist"
-- Load all module data parts (exported, server, private)
let mut fnames := #[mFile]
let sFile := OLeanLevel.server.adjustFileName mFile
if (← sFile.pathExists) then
fnames := fnames.push sFile
let pFile := OLeanLevel.private.adjustFileName mFile
if (← pFile.pathExists) then
fnames := fnames.push pFile
let parts ← readModuleDataParts fnames
if h : parts.size = 0 then throw <| IO.userError "failed to read module data" else
let (mod, _) := parts[0]
let (_, s) ← importModulesCore mod.imports |>.run
let env ← finalizeImport s mod.imports {} 0 false false (isModule := true)
let mut newConstants := {}
-- Collect constants from last ("most private") part, which subsumes all prior ones
for name in parts[parts.size-1].1.constNames, ci in parts[parts.size-1].1.constants do
newConstants := newConstants.insert name ci
let env' ← env.replay' newConstants
env'.freeRegions
unsafe def replayFromFresh (module : Name) : IO Unit := do
Lean.withImportModules #[{module}] {} fun env => do
discard <| (← mkEmptyEnvironment).replay' env.constants.map₁
/-- Read the name of the main module from the `lake-manifest`. -/
-- This has been copied from `ImportGraph.getCurrentModule` in the
-- https://github.com/leanprover-community/import-graph repository.
def getCurrentModule : IO Name := do
match (← Lake.Manifest.load? ⟨"lake-manifest.json"⟩) with
| none =>
-- TODO: should this be caught?
pure .anonymous
| some manifest =>
-- TODO: This assumes that the `package` and the default `lean_lib`
-- have the same name up to capitalisation.
-- Would be better to read the `.defaultTargets` from the
-- `← getRootPackage` from `Lake`, but I can't make that work with the monads involved.
return manifest.name.capitalize
/--
Run as e.g. `leanchecker` to check everything in the current project.
or e.g. `leanchecker Mathlib.Data.Nat` to check everything with module name
starting with `Mathlib.Data.Nat`.
This will replay all the new declarations from the target file into the `Environment`
as it was at the beginning of the file, using the kernel to check them.
You can also use `leanchecker --fresh Mathlib.Data.Nat.Prime.Basic`
to replay all the constants (both imported and defined in that file) into a fresh environment.
This can only be used on a single file.
This is not an external verifier, simply a tool to detect "environment hacking".
-/
unsafe def main (args : List String) : IO UInt32 := do
-- Contributor's note: lean4lean is intended to have a CLI interface matching leanchecker,
-- so if you want to make a change here please either make a sibling PR to
-- https://github.com/digama0/lean4lean or ping @digama0 (Mario Carneiro) to go fix it.
initSearchPath (← findSysroot)
let (flags, args) := args.partition fun s => s.startsWith "-"
let verbose := "-v" ∈ flags || "--verbose" ∈ flags
let fresh := "--fresh" ∈ flags
let targets ← do
match args with
| [] => pure [← getCurrentModule]
| args => args.mapM fun arg => do
let mod := arg.toName
if mod.isAnonymous then
throw <| IO.userError s!"Could not resolve module: {arg}"
else
pure mod
let mut targetModules := []
let sp ← searchPathRef.get
for target in targets do
let mut found := false
for path in (← SearchPath.findAllWithExt sp "olean") do
if let some m := (← searchModuleNameOfFileName path sp) then
if !fresh && target.isPrefixOf m || target == m then
targetModules := targetModules.insert m
found := true
if not found then
throw <| IO.userError s!"Could not find any oleans for: {target}"
if fresh then
if targetModules.length != 1 then
throw <| IO.userError s!"--fresh flag is only valid when specifying a single module:\n\
{targetModules}"
for m in targetModules do
if verbose then IO.println s!"replaying {m} with --fresh"
replayFromFresh m
else
let mut tasks := #[]
for m in targetModules do
tasks := tasks.push (m, ← IO.asTask (replayFromImports m))
for (m, t) in tasks do
if verbose then IO.println s!"replaying {m}"
if let .error e := t.get then
IO.eprintln s!"leanchecker found a problem in {m}"
throw e
return 0