lean4-htt/tests/elab/tactic_config.lean
Kyle Miller 047f6aaf89
feat: efficient tactic configuration elaborators and configurability (#13651)
This PR replaces the previous tactic configuration system with a
significantly more efficient one that supports custom configuration
syntaxes and processing. On a simple benchmark, configuration evaluation
takes 6.2% of the time it used to. The `declare_config_elab` command
generates a configuration elaborator that now directly constructs
configuration objects; previously it relied on `Meta.evalExpr'`, which
involved running a configuration through the full term elaboration,
compilation, and evaluation processes. The generated configuration
elaborators now also have the capability to do direct `Syntax`
evaluation in common cases, skipping term elaboration. Furthermore, the
elaborator accepts configurations more liberally: any user-defined
syntax that has the form of an `optConfig`-style configuration or
configuration item (including, e.g., `namedArgument`s) is accepted.
Import `Lean.Elab.ConfigEval` to use the system; see this module for
some documentation in addition to the docstrings in
`Lean.Elab.ConfigEval.Commands`. Furthermore, the `simp` tactic now also
has `(user.optionName := ...)` user configuration options, which can be
declared using a global `tactic.simp.user.optionName` option; use
`getUserConfigOption` and `withUserConfig` to access and set these in
metaprograms.

Other features:
- `declare_config_elab` creates a function that exposes an `init`
parameter for the configuration that will be modified. It also now has a
`where` clause, enabling defining custom handlers for specific keys.
- Elaborators can be given additional binders, to make parameterized
elaborators. This is used by `simp` and `grind ` to support multiple
default configurations with the correct expected type for `(config :=
...)` elaboration.
- The `EvalTerm` class supports direct evaluation of `Syntax`, skipping
term elaboration. The system will attempt to automatically derive this
class when generating the elaborator.
- In case `EvalTerm` does not recognize the term, then the syntax is
elaborated to an expression and an `EvalExpr` instance is applied to
evaluate the expression. The system will similarly automatically derive
these instances if possible.
- Automatic derivation is *transitive*. It is able to seek instances
through other instances; e.g. if it needs an `EvalTerm (List T)`
instance it will be able to reduce this to seeking an `EvalTerm T`
instance.
- The system is designed to be flexible, and the various components can
be combined to construct a configuration elaborator. There are also now
`declare_core_config_elab` and `declare_term_config_elab` for
conveniently generating elaborators for `CoreM` and `TermElabM`. The
difference is that the first takes an explicit flag for whether to log
exceptions, and the second uses the current `errToSorry` state.
**Warning:** if you use the `TermElabM` one from `TacticM`, it will be
unaware of the current `recover` state. The only differences between
these macros are the ways error recovery is handled per monad.

Other changes:
- `#reduce` tactic configuration now makes use of this system and has
more options
- The module `Lean.Elab.Tactic.ConfigSetter` is removed; the
`declare_config_elab`-family macros subsume its functionality.
- The module `Lean.Elab.Tactic.Config` is deprecated and will be
removed; migration notes appear in the module docs there. Import
`Lean.Elab.ConfigEval` instead.
- One of the mvcgen benchmarks got significantly slower, but it turned
out to be caused by the new tactic configuration elaboration no longer
resetting the MetaM caches. Adding an explicit `resetCache` into the
test driver fixed the benchmark.

### Notes for metaprogram authors

If you are using the module system, you just need a `meta import
Lean.Elab.ConfigEval` to use the macros, and it should serve as a
drop-in replacement to the previous system so long as

1. your configuration type is a `structure` with no parameters, indices,
or universes (only `Type` is supported);
2. default values are self-contained and not dependent on other fields;
and
3. all fields have types that are composed from `Option`, `List`,
`Array`, `String`, `DataValue`, and inductive types in `Type` with no
parameters or indices, whose fields are similarly composed.

The macros synthesize a self-contained configuration elaboration
procedure, analyzing the `EvalTerm` and `EvalExpr` instances that are
currently available or can be automatically derived. These are the
components of the system:
- `EvalTerm` instances provide `Term → TermElabM α` functions for
evaluation of raw syntax; these handle the common cases where an option
value is a identifier, application, or other simple expression. They are
responsible for adding TermInfo when info is enabled, to support hovers.
One can invoke derivation of a `EvalTerm T` instance with the
`ensure_eval_term_instance T` command (after `open scoped
Lean.Elab.ConfigEval`).
- `EvalExpr` instances provide `Expr → TermElabM α` functions for
evaluation of elaborated expressions; these handle cases where an option
value may require reduction to evaluate. Similarly, one can invoke
derivation of an `EvalExpr T` instance with the
`ensure_eval_expr_instance T` command. If needed, there's also
`derive_eval_expr_instance_using_meta_eval T` for creating a
`Meta.evalExpr'`-based evaluator.
- Functions like `ConfigEval.evalExprWithElab` compose `EvalTerm` and
`EvalExpr` instances into a single procedure that first attempts
`EvalTerm`, and, if that fails, applies the standard term elaborator and
then attempts `EvalExpr`. This way term elaboration can be skipped in
all but uncommon cases.
- Configuration item interpretation is through `ConfigEval.foldConfigM`,
which is a procedure with a lax specification for what counts as a
configuration item, calling the provided function on each recognized
configuration item. The idea is:
  - Null nodes are lists of configurations
- One-argument nodes are considered to be wrappers like `optConfig` or
`configItem`
- Two-argument nodes of the form `("+"<|>"-") (atom<|>ident)` are
considered to be boolean options
- Five-argument nodes of the form `"(" (atom<|>ident) ":=" syntax ")"`
are considered to be general configuration items. (It only checks for
the presence of `(` and that there are two-to-five arguments.)
  - Bare atoms are considered to be positive boolean options
- Configuration evaluation then uses `EvalConfigItem.set` on each item
produced by the fold, for an `EvalConfigItem` structure defined for the
given configuration type. The `def_eval_config_item` command can be used
to generate this structure. It analyzes which `EvalTerm` and `EvalExpr`
instances exist and derives missing ones, then builds an efficient
procedure to process configuration items and apply evaluators.
- Lastly, there are the `declare_core_config_elab`,
`declare_term_config_elab`, `declare_config_elab`, and
`declare_command_config_elab` macros for conveniently running the
`def_eval_config_item` command and constructing a self-contained
elaboration function.

The derivation procedures analyze which `EvalTerm`/`EvalExpr` instances
already exist and only derive the "leaf" instances that are necessary to
construct `EvalTerm` and `EvalExpr` instances. The derived instances are
made `private local` — since configuration elaborators are meant to be
self-contained, we decided not to let the additional instances be a side
effect of the macros. The instances can be globally added by manually
using the `ensure_*` commands.

The macros support making parameterized elaborators with arbitrary
additional binders. See `make_elab_grind_config` and
`make_elab_simp_config` in core Lean for examples of generating a single
elaborator that's used with multiple default value configurations.

To see how to create a key handler that matches all configuration keys
with a given prefix, see `make_elab_simp_config`.

There is a todo item at `Lean.Elab.ConfigEval.ReflectConfigItems` for
reflecting configurations back to syntax, which is not yet supported.

### Performance evaluation

A legacy configuration parser was temporarily added to
`Lean.Elab.Tactic.Grind.Config` using `declare_term_config_elab_legacy
elabGrindConfigLegacy Grind.Config`, and then this file was used for
measuring elaboration time:

```lean
import Lean

open Lean Elab Meta Tactic Parser

def cfgs : Array Syntax := Unhygienic.run do
  return #[
    ← `(Tactic.optConfig| ),
    ← `(Tactic.optConfig| +clean),
    ← `(Tactic.optConfig| +trace +markInstances -lookahead -useSorry),
    ← `(Tactic.optConfig| (trace := true) (markInstances := true) (lookahead := false) (useSorry := false)),
    ← `(Tactic.optConfig| -trace (splits := 20) +revert (maxSuggestions := some 3) (ematch := 2)),
    ← `(Tactic.optConfig| (gen := 5) -reducible +splitImp -funCC),
    ← `(Tactic.optConfig| (config := { trace := true, lookahead := false, maxSuggestions := some 3 })),
  ]

def testGrindElab (cfgs : Array Syntax) (n : Nat) : TacticM Unit := do
  profileitM Exception "test grind elab" (← getOptions) do
    let mut ematch := 0
    for _ in [0:n] do
      for cfg in cfgs do
        let c ← Tactic.elabGrindConfig cfg
        ematch := ematch + c.ematch
    logInfo m!"sum = {ematch}"

def testGrindElabLegacy (cfgs : Array Syntax) (n : Nat) : TacticM Unit := do
  profileitM Exception "test grind elab legacy" (← getOptions) do
    let mut ematch := 0
    for _ in [0:n] do
      for cfg in cfgs do
        let c ← Tactic.elabGrindConfigLegacy cfg
        ematch := ematch + c.ematch
    logInfo m!"sum = {ematch}"

def runTest (info : Bool) (test : TacticM Unit) : TermElabM Unit := do
  withEnableInfoTree info do
    let mvar ← mkFreshExprMVar none
    discard <| Tactic.run mvar.mvarId! test

set_option maxHeartbeats 0
set_option profiler true
set_option profiler.threshold 1
def iters : Nat := 1000
#eval runTest false <| testGrindElab cfgs iters
#eval runTest true <| testGrindElab cfgs iters
#eval runTest false <| testGrindElabLegacy cfgs iters
#eval runTest true <| testGrindElabLegacy cfgs iters
```

A representative output is
```
test grind elab took 315ms
test grind elab took 333ms
test grind elab legacy took 5.22s
test grind elab legacy took 5.33s
```
Computing `(315.0 + 333.0) / (5220 + 5330)` and rounding up to the
nearest tenth gives the 6.2% figure.

---

The #13426 draft PR includes some LSP modifications to support
completions for `simp` user configuration options.
2026-05-14 17:20:57 +00:00

440 lines
10 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.

import Lean
/-!
# Tests for tactic configuration elaboration
-/
open Lean
/-!
Simple tactic configuration
-/
structure MyTacticConfig where
x : Nat := 0
y : Bool := false
deriving Repr
declare_config_elab elabMyTacticConfig MyTacticConfig
elab "my_tactic" cfg:Parser.Tactic.optConfig : tactic => do
let config ← elabMyTacticConfig cfg
logInfo m!"config is {repr config}"
/--
info: config is { x := 0, y := false }
---
info: config is { x := 0, y := true }
---
info: config is { x := 1, y := false }
---
info: config is { x := 2, y := false }
---
info: config is { x := 1, y := true }
---
info: config is { x := 0, y := false }
-/
#guard_msgs in
example : True := by
my_tactic
my_tactic +y
my_tactic (x := 1)
my_tactic -y (x := 2)
my_tactic (config := {x := 1, y := true})
my_tactic +y (config := {y := false})
trivial
/-!
Basic errors
-/
/--
error: Option is not boolean-valued, so `(x := ...)` syntax must be used
---
info: config is { x := 0, y := false }
---
error: unsolved goals
⊢ True
-/
#guard_msgs in example : True := by my_tactic +x
/--
error: Invalid configuration option `w` for `MyTacticConfig`
---
info: config is { x := 0, y := false }
---
error: unsolved goals
⊢ True
-/
#guard_msgs in example : True := by my_tactic +w
/--
error: Invalid configuration option `x.a` for `MyTacticConfig`
---
info: config is { x := 0, y := false }
---
error: unsolved goals
⊢ True
-/
#guard_msgs in example : True := by my_tactic +x.a
/-!
A tactic configuration extending another with different default values.
-/
structure MyTacticConfig' extends MyTacticConfig where
x := 22
y := true
deriving Repr
declare_config_elab elabMyTacticConfig' MyTacticConfig'
elab "my_tactic'" cfg:Parser.Tactic.optConfig : tactic => do
let config ← elabMyTacticConfig' cfg
logInfo m!"config is {repr config}"
/--
info: config is { toMyTacticConfig := { x := 22, y := true } }
---
info: config is { toMyTacticConfig := { x := 22, y := true } }
---
info: config is { toMyTacticConfig := { x := 1, y := true } }
---
info: config is { toMyTacticConfig := { x := 2, y := false } }
---
info: config is { toMyTacticConfig := { x := 2, y := false } }
---
info: config is { toMyTacticConfig := { x := 1, y := true } }
---
info: config is { toMyTacticConfig := { x := 22, y := false } }
-/
#guard_msgs in
example : True := by
my_tactic'
my_tactic' +y
my_tactic' (x := 1)
my_tactic' -y (x := 2)
my_tactic' (x := 2) -y
my_tactic' (config := {x := 1, y := true})
my_tactic' +y (config := {y := false})
trivial
/-!
Evaluation failure
-/
opaque fooNat : Nat := 22
/--
error: Could not evaluate the expression:
fooNat
of type `Nat`.
---
info: config is { x := 0, y := true }
-/
#guard_msgs in
example : True := by
my_tactic (x := fooNat) +y
trivial
/-!
Tactic configurations with hierarchical fields.
The `toA` parent projections are not made available for use.
-/
structure A where
x : Bool := true
deriving Repr
structure B extends A
deriving Repr
structure C where
b : B := {}
deriving Repr
declare_config_elab elabC C
elab "ctac" cfg:Parser.Tactic.optConfig : tactic => do
let config ← elabC cfg
logInfo m!"config is {repr config}"
/-- info: config is { b := { toA := { x := false } } } -/
#guard_msgs in
example : True := by
ctac -b.x
trivial
/--
error: Invalid configuration option `b.toA.x` for `C`
---
info: config is { b := { toA := { x := true } } }
-/
#guard_msgs in
example : True := by
ctac -b.toA.x
trivial
/-!
Responds to recovery mode. In these, `ctac` continues even though configuration elaboration failed.
-/
/--
error: Invalid configuration option `x` for `C`
---
info: config is { b := { toA := { x := true } } }
---
trace: ⊢ True
-/
#guard_msgs in
example : True := by
ctac -x
trace_state
trivial
-- Check that when recovery mode is false, no error is reported, since there was an exception.
/-- trace: ⊢ True -/
#guard_msgs in
example : True := by
fail_if_success ctac -x
trace_state
trivial
/--
error: Invalid configuration option `x` for `C`
---
info: config is { b := { toA := { x := true } } }
---
error: unsolved goals
⊢ True
-/
#guard_msgs in
example : True := by
ctac -x
done
/-!
Responds to recovery mode. In this, `ctac` fails, doesn't report anything, and then execution continues to `exact`.
-/
/-- error: Unknown identifier `blah` -/
#guard_msgs in
example : True := by
first | ctac +x | exact blah
/-!
Elaboration errors cause the tactic to use the default configuration.
-/
/--
error: Type mismatch
"oops"
has type
String
but is expected to have type
Bool
---
info: config is { b := { toA := { x := true } } }
---
error: unsolved goals
⊢ True
-/
#guard_msgs in
example : True := by
ctac (b.x := "oops")
done
/-!
Elaboration for command configuration
-/
structure MyCommandConfig where
x : Nat := 0
y : Bool := false
deriving Repr
declare_command_config_elab elabMyCommandConfig MyCommandConfig
elab "my_command" cfg:Parser.Tactic.optConfig : command => do
let config ← elabMyCommandConfig cfg
logInfo m!"config is {repr config}"
/-- info: config is { x := 0, y := false } -/
#guard_msgs in my_command
/-- info: config is { x := 0, y := true } -/
#guard_msgs in my_command +y
/-- info: config is { x := 1, y := true } -/
#guard_msgs in my_command (x := 1) (y := true)
/-- info: config is { x := 0, y := false } -/
#guard_msgs in my_command (x := 1) (y := true) (config := {})
/--
error: Type mismatch
true
has type
Bool
but is expected to have type
Nat
---
info: config is { x := 0, y := false }
-/
#guard_msgs in my_command (x := true)
/-!
Testing `Occurrences.pos`
-/
/--
trace: a : Nat
this : a = 0 + a
⊢ 0 + a = 0 + a
-/
#guard_msgs in
example (a : Nat) : a = 0 + a := by
have : a = 0 + a := by rw [Nat.zero_add]
rewrite (occs := .pos [1]) [this]
trace_state
rfl
/--
trace: a : Nat
this : a = 0 + a
⊢ 0 + a = 0 + a
-/
#guard_msgs in
example (a : Nat) : a = 0 + a := by
have : a = 0 + a := by rw [Nat.zero_add]
rewrite (occs := [1]) [this]
trace_state
rfl
/-!
Pretty printing of configuration, checking whitespace is present.
-/
elab "#pp_tac " t:tactic : command => Elab.Command.liftTermElabM do
logInfo (← PrettyPrinter.ppTactic t)
/-- info: simp +contextual -/
#guard_msgs in #pp_tac simp +contextual
/-- info: simp +contextual -/
#guard_msgs in #pp_tac simp+contextual
/-- info: simp (contextual := true) +zeta -/
#guard_msgs in #pp_tac simp (contextual := true) +zeta
/-- info: simp (contextual := true) +zeta -/
#guard_msgs in #pp_tac simp(contextual := true)+zeta
/-!
Simp user configuration.
-/
open Meta.Simp Elab.Tactic in
simproc testUserConfig (_) := fun _ => do
let v1 ← getUserConfigOption tactic.simp.user.exampleBool
let v2 ← getUserConfigOption tactic.simp.user.exampleNat
let v3 ← getUserConfigOption tactic.simp.user.exampleInt
let v4 ← getUserConfigOption tactic.simp.user.exampleString
logInfo m!"exampleBool: {v1} exampleNat: {v2} exampleInt: {v3} exampleString: {repr v4}"
return .continue
/--
info: exampleBool: false exampleNat: 0 exampleInt: 0 exampleString: ""
---
info: exampleBool: true exampleNat: 0 exampleInt: 0 exampleString: ""
---
info: exampleBool: false exampleNat: 22 exampleInt: 0 exampleString: ""
---
info: exampleBool: false exampleNat: 0 exampleInt: -22 exampleString: ""
---
info: exampleBool: false exampleNat: 0 exampleInt: 0 exampleString: "hi"
---
info: exampleBool: true exampleNat: 22 exampleInt: -22 exampleString: "hi"
---
error: User options are of the form `user.optionName`
---
info: exampleBool: false exampleNat: 0 exampleInt: 0 exampleString: ""
-/
#guard_msgs in
example (h : False) : False := by
simp -failIfUnchanged
simp -failIfUnchanged +user.exampleBool
simp -failIfUnchanged (user.exampleNat := 22)
simp -failIfUnchanged (user.exampleInt := -22)
simp -failIfUnchanged (user.exampleString := "hi")
simp -failIfUnchanged +user.exampleBool (user.exampleNat := 22) (user.exampleInt := -22) (user.exampleString := "hi")
simp -failIfUnchanged +user
exact h
/-!
Testing the `derive_eval_expr_instance_using_meta_eval` instance.
-/
section
open Lean.Elab.ConfigEval
structure MetaEvalTest where
x : Nat
b : Bool
f : Nat → Nat
derive_eval_expr_instance_using_meta_eval MetaEvalTest
/-- info: x: 3, b: true, f 10: 12, f 100: 102 -/
#guard_msgs in
#eval do
let stx ← `({ x := 3, b := true, f := (·+2) })
let c ← evalExprWithElab (α := MetaEvalTest) stx
logInfo m!"x: {c.x}, b: {c.b}, f 10: {c.f 10}, f 100: {c.f 100}"
/-!
Testing bare atoms for positive options
-/
structure TestBareConfig where
only : Bool := false
x : Nat := 0
deriving Repr
syntax testBareConfigOnly := &"only"
syntax testBareConfigCfg := many(testBareConfigOnly <|> Parser.Term.configItem)
declare_command_config_elab elabTestBareConfig TestBareConfig
elab "#test_bare_config" cfg:testBareConfigCfg : command => do
let config ← elabTestBareConfig cfg
logInfo m!"config is {repr config}"
/-- info: config is { only := false, x := 0 } -/
#guard_msgs in #test_bare_config
/-- info: config is { only := true, x := 0 } -/
#guard_msgs in #test_bare_config only
/-- info: config is { only := true, x := 0 } -/
#guard_msgs in #test_bare_config +only
/-- info: config is { only := true, x := 0 } -/
#guard_msgs in #test_bare_config (only := true)
/-- info: config is { only := true, x := 2 } -/
#guard_msgs in #test_bare_config (x := 2) only
/-- info: config is { only := true, x := 2 } -/
#guard_msgs in #test_bare_config only (x := 2)
/-!
Testing auto-derivations
-/
namespace AutoDeriveTest
structure A where
n : Nat
inductive B where
| ctor1
| ctor2 (a : Option A)
structure C where
opt1 : List A
opt2 : Option (Array B)
open scoped Lean.Elab.ConfigEval
ensure_eval_term_expr_instances C
/-- info: instEvalTermA -/
#guard_msgs in #synth EvalTerm A
/-- info: instEvalTermB -/
#guard_msgs in #synth EvalTerm B
/-- info: instEvalTermC -/
#guard_msgs in #synth EvalTerm C
/-- info: instEvalExprA -/
#guard_msgs in #synth EvalExpr A
/-- info: instEvalExprB -/
#guard_msgs in #synth EvalExpr B
/-- info: instEvalExprC -/
#guard_msgs in #synth EvalExpr C
end AutoDeriveTest