lean4-htt/src/Lean/Compiler/LCNF/Testing.lean
2025-05-18 21:12:35 +00:00

253 lines
9.7 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2022 Henrik Böving. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Henrik Böving
-/
prelude
import Lean.Compiler.LCNF.PassManager
import Lean.Compiler.LCNF.PrettyPrinter
namespace Lean.Compiler.LCNF
partial def Code.containsConst (constName : Name) (code : Code) : Bool :=
match code with
| .let decl k => goLetValue decl.value || containsConst constName k
| .fun decl k => containsConst constName decl.value || containsConst constName k
| .jp decl k => containsConst constName decl.value || containsConst constName k
| .cases cs => cs.alts.any fun alt => containsConst constName alt.getCode
| .return .. | .unreach .. | .jmp .. => false
where
goExpr (e : Expr) : Bool :=
match e with
| .const name .. => name == constName
| .app fn arg .. => goExpr fn || goExpr arg
| .lam _ _ body .. => goExpr body
| .proj _ _ struct .. => goExpr struct
| .letE .. => unreachable! -- not possible in LCNF
| _ => false
goLetValue (l : LetValue) : Bool :=
match l with
| .lit .. | .erased | .proj .. | .fvar .. => false
| .const name .. => name == constName
namespace Testing
structure TestInstallerContext where
passUnderTestName : Name
testName : Name
structure TestContext where
passUnderTest : Pass
testName : Name
structure SimpleAssertionContext where
decls : Array Decl
structure InOutAssertionContext where
input : Array Decl
output : Array Decl
abbrev TestInstallerM := ReaderM TestInstallerContext
abbrev TestInstaller := TestInstallerM PassInstaller
abbrev TestM := ReaderT TestContext CompilerM
abbrev SimpleAssertionM := ReaderT SimpleAssertionContext TestM
abbrev InOutAssertionM := ReaderT InOutAssertionContext TestM
abbrev SimpleTest := SimpleAssertionM Unit
abbrev InOutTest := InOutAssertionM Unit
def TestInstaller.install (x : TestInstaller) (passUnderTestName testName : Name) : PassInstaller :=
x { passUnderTestName, testName }
def TestM.run (x : TestM α) (passUnderTest : Pass) (testName : Name) : CompilerM α :=
x { passUnderTest, testName }
def SimpleAssertionM.run (x : SimpleAssertionM α) (decls : Array Decl) (passUnderTest : Pass) (testName : Name) : CompilerM α :=
x { decls } { passUnderTest, testName }
def InOutAssertionM.run (x : InOutAssertionM α) (input output : Array Decl) (passUnderTest : Pass) (testName : Name) : CompilerM α :=
x { input, output } { passUnderTest, testName }
def getTestName : TestM Name := do
return (←read).testName
def getPassUnderTest : TestM Pass := do
return (←read).passUnderTest
def getDecls : SimpleAssertionM (Array Decl) := do
return (←read).decls
def getInputDecls : InOutAssertionM (Array Decl) := do
return (←read).input
def getOutputDecls : InOutAssertionM (Array Decl) := do
return (←read).output
/--
If `property` is `false` throw an exception with `msg`.
-/
def assert (property : Bool) (msg : String) : TestM Unit := do
unless property do
throwError msg
private def assertAfterTest (test : SimpleTest) : TestInstallerM (Pass → Pass) := do
let testName := (←read).testName
return fun passUnderTest => {
phase := passUnderTest.phase
name := testName
run := fun decls => do
trace[Compiler.test] "Starting post condition test {testName} for {passUnderTest.name} occurrence {passUnderTest.occurrence}"
test.run decls passUnderTest testName
trace[Compiler.test] "Post condition test {testName} for {passUnderTest.name} occurrence {passUnderTest.occurrence} successful"
return decls
}
/--
Install an assertion pass right after a specific occurrence of a pass,
default is first.
-/
def assertAfter (test : SimpleTest) (occurrence : Nat := 0): TestInstaller := do
let passUnderTestName := (←read).passUnderTestName
let assertion ← assertAfterTest test
return .installAfter passUnderTestName assertion occurrence
/--
Install an assertion pass right after each occurrence of a pass.
-/
def assertAfterEachOccurrence (test : SimpleTest) : TestInstaller := do
let passUnderTestName := (←read).passUnderTestName
let assertion ← assertAfterTest test
return .installAfterEach passUnderTestName assertion
/--
Install an assertion pass right after a specific occurrence of a pass,
default is first. The assertion operates on a per declaration basis.
-/
def assertForEachDeclAfter (assertion : Pass → Decl → Bool) (msg : String) (occurrence : Nat := 0) : TestInstaller :=
let assertion := do
let pass ← getPassUnderTest
(←getDecls).forM (fun decl => assert (assertion pass decl) msg)
assertAfter assertion occurrence
/--
Install an assertion pass right after the each occurrence of a pass. The
assertion operates on a per declaration basis.
-/
def assertForEachDeclAfterEachOccurrence (assertion : Pass → Decl → Bool) (msg : String) : TestInstaller :=
assertAfterEachOccurrence <| do
let pass ← getPassUnderTest
(←getDecls).forM (fun decl => assert (assertion pass decl) msg)
private def assertAroundTest (test : InOutTest) : TestInstallerM (Pass → Pass) := do
let testName := (←read).testName
return fun passUnderTest => {
phase := passUnderTest.phase
name := passUnderTest.name
run := fun decls => do
trace[Compiler.test] "Starting wrapper test {testName} for {passUnderTest.name} occurrence {passUnderTest.occurrence}"
let newDecls ← passUnderTest.run decls
test.run decls newDecls passUnderTest testName
trace[Compiler.test] "Wrapper test {testName} for {passUnderTest.name} occurrence {passUnderTest.occurrence} successful"
return newDecls
}
/--
Replace a specific occurrence, default is first, of a pass with a wrapper one that allows
the user to provide an assertion which takes into account both the
declarations that were sent to and produced by the pass under test.
-/
def assertAround (test : InOutTest) (occurrence : Nat := 0) : TestInstaller := do
let passUnderTestName := (←read).passUnderTestName
let assertion ← assertAroundTest test
return .replacePass passUnderTestName assertion occurrence
/--
Replace all occurrences of a pass with a wrapper one that allows
the user to provide an assertion which takes into account both the
declarations that were sent to and produced by the pass under test.
-/
def assertAroundEachOccurrence (test : InOutTest) : TestInstaller := do
let passUnderTestName := (←read).passUnderTestName
let assertion ← assertAroundTest test
return .replaceEachOccurrence passUnderTestName assertion
private def throwFixPointError (err : String) (firstResult secondResult : Array Decl) : CompilerM Unit := do
let mut err := err
err := err ++ "Result after usual run:"
let folder := fun err decl => do return err ++ s!"\n{←ppDecl decl}"
err ← firstResult.foldlM (init := err) folder
err := err ++ "Result after further run:"
err ← secondResult.foldlM (init := err) folder
throwError err
/--
Insert a pass after `passUnderTestName`, that ensures, that if
`passUnderTestName` is executed twice in a row, no change in the resulting
expression will occur, i.e. the pass is at a fix point.
-/
def assertIsAtFixPoint : TestInstaller :=
let test := do
let passUnderTest ← getPassUnderTest
let decls ← getDecls
let secondResult ← passUnderTest.run decls
if decls.size < secondResult.size then
let err := s!"Pass {passUnderTest.name} did not reach a fixpoint, it added declarations on further runs:\n"
throwFixPointError err decls secondResult
else if decls.size > secondResult.size then
let err := s!"Pass {passUnderTest.name} did not reach a fixpoint, it removed declarations on further runs:\n"
throwFixPointError err decls secondResult
else if decls != secondResult then
let err := s!"Pass {passUnderTest.name} did not reach a fixpoint, it either changed declarations or their order:\n"
throwFixPointError err decls secondResult
assertAfterEachOccurrence test
/--
Compare the overall sizes of the input and output of `passUnderTest` with `assertion`.
If `assertion inputSize outputSize` is `false` throw an exception with `msg`.
-/
def assertSize (assertion : Nat → Nat → Bool) (msg : String) : TestInstaller :=
let sumDeclSizes := fun decls => decls.map Decl.size |>.foldl (init := 0) (· + ·)
let assertion := (fun inputS outputS => Testing.assert (assertion inputS outputS) s!"{msg}: input size {inputS} output size {outputS}")
assertAroundEachOccurrence (do assertion (sumDeclSizes (←getInputDecls)) (sumDeclSizes (←getOutputDecls)))
/--
Assert that the overall size of the `Decl`s in the compilation pipeline does not change
after `passUnderTestName`.
-/
def assertPreservesSize (msg : String) : TestInstaller :=
assertSize (· == ·) msg
/--
Assert that the overall size of the `Decl`s in the compilation pipeline gets reduced by `passUnderTestName`.
-/
def assertReducesSize (msg : String) : TestInstaller :=
assertSize (· > ·) msg
/--
Assert that the overall size of the `Decl`s in the compilation pipeline gets reduced or stays unchanged
by `passUnderTestName`.
-/
def assertReducesOrPreservesSize (msg : String) : TestInstaller :=
assertSize (· ≥ ·) msg
/--
Assert that the pass under test produces `Decl`s that do not contain
`Expr.const constName` in their `Code.let` values anymore.
-/
def assertDoesNotContainConstAfter (constName : Name) (msg : String) : TestInstaller :=
assertForEachDeclAfterEachOccurrence
fun _ decl =>
match decl.value with
| .code c => !c.containsConst constName
| .extern .. => true
msg
def assertNoFun : TestInstaller :=
assertAfter do
for decl in (← getDecls) do
decl.value.forCodeM fun
| .fun .. => throwError "declaration `{decl.name}` contains a local function declaration"
| _ => return ()
end Testing
end Lean.Compiler.LCNF