From cd632b033deae9d423a078fb9a49d5b04ae24217 Mon Sep 17 00:00:00 2001 From: Kim Morrison <477956+kim-em@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:19:32 +1100 Subject: [PATCH] feat: add `grind +locals` to include local definitions (#11946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a `+locals` configuration option to the `grind` tactic that automatically adds all definitions from the current file as e-match theorems. This provides a convenient alternative to manually adding `[local grind]` attributes to each definition. In the form `grind? +locals`, it is also helpful for discovering which local declarations it may be useful to add `[local grind]` attributes to. Example usage: ```lean def foo (n : Nat) : Nat := n + 1 -- Without +locals, grind doesn't know about foo example (n : Nat) : foo n = n + 1 := by grind -- fails -- With +locals, grind can use the equation example (n : Nat) : foo n = n + 1 := by grind +locals -- succeeds ``` Instance definitions and internal details are filtered out. ๐Ÿค– Prepared with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- src/Init/Grind/Config.lean | 2 + src/Lean/Elab/Tactic/Grind/Main.lean | 56 ++++++++++++++++--------- src/Lean/Meta/Tactic/Grind.lean | 2 + tests/lean/run/grind_ite.lean | 17 ++++---- tests/lean/run/grind_locals.lean | 51 ++++++++++++++++++++++ tests/lean/run/grind_locals_module.lean | 56 +++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 tests/lean/run/grind_locals.lean create mode 100644 tests/lean/run/grind_locals_module.lean diff --git a/src/Init/Grind/Config.lean b/src/Init/Grind/Config.lean index 49047bcb65..a29189ef7e 100644 --- a/src/Init/Grind/Config.lean +++ b/src/Init/Grind/Config.lean @@ -21,6 +21,8 @@ structure Config where /-- If `suggestions` is `true`, `grind` will invoke the currently configured library suggestion engine on the current goal, and add attempt to use the resulting suggestions as additional parameters to the `grind` tactic. -/ suggestions : Bool := false + /-- If `locals` is `true`, `grind` will add all definitions from the current file. -/ + locals : Bool := false /-- Maximum number of case-splits in a proof search branch. It does not include splits performed during normalization. -/ splits : Nat := 9 /-- Maximum number of E-matching (aka heuristic theorem instantiation) rounds before each case split. -/ diff --git a/src/Lean/Elab/Tactic/Grind/Main.lean b/src/Lean/Elab/Tactic/Grind/Main.lean index 552c0ebcde..6169c1bc63 100644 --- a/src/Lean/Elab/Tactic/Grind/Main.lean +++ b/src/Lean/Elab/Tactic/Grind/Main.lean @@ -182,6 +182,7 @@ open LibrarySuggestions in def elabGrindSuggestions (params : Grind.Params) (suggestions : Array Suggestion := #[]) : MetaM Grind.Params := do let mut params := params + let mut added : Array Name := #[] for p in suggestions do let attr โ† match p.flag with | some flag => parseModifier flag @@ -190,6 +191,7 @@ def elabGrindSuggestions | .ematch kind => try params โ† addEMatchTheorem params (mkIdent p.name) p.name kind false (warn := false) + added := added.push p.name catch _ => pure () -- Don't worry if library suggestions gave bad theorems. | _ => -- We could actually support arbitrary grind modifiers, @@ -197,26 +199,40 @@ def elabGrindSuggestions -- but this would require a larger refactor. -- Let's only do this if there is a prospect of a library suggestion engine supporting this. throwError "unexpected modifier {p.flag}" + unless added.isEmpty do + trace[grind.debug.suggestions] "{added}" return params -open LibrarySuggestions in -def elabGrindParamsAndSuggestions - (params : Grind.Params) - (ps : TSyntaxArray ``Parser.Tactic.grindParam) - (suggestions : Array Suggestion := #[]) - (only : Bool) (lax : Bool := false) : TermElabM Grind.Params := do - let params โ† elabGrindParams params ps (lax := lax) (only := only) - elabGrindSuggestions params suggestions +/-- Add all definitions from the current file. -/ +def elabGrindLocals (params : Grind.Params) : MetaM Grind.Params := do + let env โ† getEnv + let mut params := params + let mut added : Array Name := #[] + for (name, ci) in env.constants.mapโ‚‚.toList do + -- Filter similar to LibrarySuggestions.isDeniedPremise (but inlined to avoid dependency) + -- Skip internal details, but allow private names (which are accessible from current module) + if name.isInternalDetail && !isPrivateName name then continue + if (โ† Meta.isInstance name) then continue + match ci with + | .defnInfo _ => + try + params โ† addEMatchTheorem params (mkIdent name) name (.default false) false (warn := false) + added := added.push name + catch _ => pure () + | _ => continue + unless added.isEmpty do + trace[grind.debug.locals] "{added}" + return params def mkGrindParams (config : Grind.Config) (only : Bool) (ps : TSyntaxArray ``Parser.Tactic.grindParam) (mvarId : MVarId) : TermElabM Grind.Params := do let params โ† if only then Grind.mkOnlyParams config else Grind.mkDefaultParams config - let suggestions โ† if config.suggestions then - LibrarySuggestions.select mvarId { caller := some "grind" } - else - pure #[] - let mut params โ† elabGrindParamsAndSuggestions params ps suggestions (only := only) (lax := config.lax) + let mut params โ† elabGrindParams params ps (lax := config.lax) (only := only) + if config.suggestions then + params โ† elabGrindSuggestions params (โ† LibrarySuggestions.select mvarId { caller := some "grind" }) + if config.locals then + params โ† elabGrindLocals params trace[grind.debug.inj] "{params.extensions[0]!.inj.getOrigins.map (ยท.pp)}" if params.anchorRefs?.isSome then /- @@ -289,17 +305,19 @@ def setGrindParams (stx : TSyntax `tactic) (params : Array Syntax) : TSyntax `ta def getGrindParams (stx : TSyntax `tactic) : Array Syntax := stx.raw[grindParamsPos][1].getSepArgs -/-- Filter out `+suggestions` from the config syntax -/ -def filterSuggestionsFromGrindConfig (config : TSyntax ``Lean.Parser.Tactic.optConfig) : +/-- Filter out `+suggestions` and `+locals` from the config syntax -/ +def filterSuggestionsAndLocalsFromGrindConfig (config : TSyntax ``Lean.Parser.Tactic.optConfig) : TSyntax ``Lean.Parser.Tactic.optConfig := let configItems := config.raw.getArgs let filteredItems := configItems.filter fun item => - -- Keep all items except +suggestions + -- Keep all items except +suggestions and +locals -- Structure: null node -> configItem -> posConfigItem -> ["+", ident] match item[0]? with | some configItem => match configItem[0]? with | some posConfigItem => match posConfigItem[1]? with - | some ident => !(posConfigItem.getKind == ``Lean.Parser.Tactic.posConfigItem && ident.getId == `suggestions) + | some ident => + let id := ident.getId + !(posConfigItem.getKind == ``Lean.Parser.Tactic.posConfigItem && (id == `suggestions || id == `locals)) | none => true | none => true | none => true @@ -345,7 +363,7 @@ def evalGrindTraceCore (stx : Syntax) (trace := true) (verbose := true) (useSorr let finish โ† Grind.Action.mkFinish let goal :: _ โ† Grind.getGoals | -- Goal was closed during initialization - let configStx' := filterSuggestionsFromGrindConfig configStx + let configStx' := filterSuggestionsAndLocalsFromGrindConfig configStx if termParamStxs.isEmpty then let tac โ† `(tactic| grind $configStx':optConfig only) return #[tac] @@ -357,7 +375,7 @@ def evalGrindTraceCore (stx : Syntax) (trace := true) (verbose := true) (useSorr -- let saved โ† saveState match (โ† finish.run goal) with | .closed seq => - let configStx' := filterSuggestionsFromGrindConfig configStx + let configStx' := filterSuggestionsAndLocalsFromGrindConfig configStx let tacs โ† Grind.mkGrindOnlyTactics configStx' seq termParamStxs let seq := Grind.Action.mkGrindSeq seq let tac โ† `(tactic| grind $configStx':optConfig => $seq:grindSeq) diff --git a/src/Lean/Meta/Tactic/Grind.lean b/src/Lean/Meta/Tactic/Grind.lean index c1d5151dfe..4d0d5b7fec 100644 --- a/src/Lean/Meta/Tactic/Grind.lean +++ b/src/Lean/Meta/Tactic/Grind.lean @@ -101,5 +101,7 @@ builtin_initialize registerTraceClass `grind.debug.proveEq builtin_initialize registerTraceClass `grind.debug.pushNewFact builtin_initialize registerTraceClass `grind.debug.appMap builtin_initialize registerTraceClass `grind.debug.ext +builtin_initialize registerTraceClass `grind.debug.suggestions +builtin_initialize registerTraceClass `grind.debug.locals end Lean diff --git a/tests/lean/run/grind_ite.lean b/tests/lean/run/grind_ite.lean index 2f022bf4c2..ecce6552ba 100644 --- a/tests/lean/run/grind_ite.lean +++ b/tests/lean/run/grind_ite.lean @@ -145,14 +145,15 @@ def normalize (assign : Std.HashMap Nat Bool) : IfExpr โ†’ IfExpr | some b => normalize assign (ite (lit b) t e) termination_by e => e.normSize --- We tell `grind` to unfold our definitions above. -attribute [local grind] normalized hasNestedIf hasConstantIf hasRedundantIf disjoint vars eval List.disjoint +-- We could tell `grind` to unfold our definitions above. +-- attribute [local grind] normalized hasNestedIf hasConstantIf hasRedundantIf disjoint vars eval List.disjoint +-- Instead we just rely on `grind +locals`, which tells `grind` it is allowed to unfold definitions in the current file. theorem normalize_spec (assign : Std.HashMap Nat Bool) (e : IfExpr) : (normalize assign e).normalized โˆง (โˆ€ f, (normalize assign e).eval f = e.eval fun w => assign[w]?.getD (f w)) โˆง โˆ€ (v : Nat), v โˆˆ vars (normalize assign e) โ†’ ยฌ v โˆˆ assign := by - fun_induction normalize with grind + fun_induction normalize with grind +locals -- We can also prove other variations, where we spell "`v` is not in `assign`" -- different ways, and `grind` doesn't mind. @@ -161,13 +162,13 @@ example (assign : Std.HashMap Nat Bool) (e : IfExpr) : (normalize assign e).normalized โˆง (โˆ€ f, (normalize assign e).eval f = e.eval fun w => assign[w]?.getD (f w)) โˆง โˆ€ (v : Nat), v โˆˆ vars (normalize assign e) โ†’ assign.contains v = false := by - fun_induction normalize with grind + fun_induction normalize with grind +locals example (assign : Std.HashMap Nat Bool) (e : IfExpr) : (normalize assign e).normalized โˆง (โˆ€ f, (normalize assign e).eval f = e.eval fun w => assign[w]?.getD (f w)) โˆง โˆ€ (v : Nat), v โˆˆ vars (normalize assign e) โ†’ assign[v]? = none := by - fun_induction normalize with grind + fun_induction normalize with grind +locals /-- We recall the statement of the if-normalization problem. @@ -203,18 +204,18 @@ theorem normalize'_spec (assign : Std.TreeMap Nat Bool) (e : IfExpr) : (normalize' assign e).normalized โˆง (โˆ€ f, (normalize' assign e).eval f = e.eval fun w => assign[w]?.getD (f w)) โˆง โˆ€ (v : Nat), v โˆˆ vars (normalize' assign e) โ†’ ยฌ v โˆˆ assign := by - fun_induction normalize' with grind + fun_induction normalize' with grind +locals example (assign : Std.TreeMap Nat Bool) (e : IfExpr) : (normalize' assign e).normalized โˆง (โˆ€ f, (normalize' assign e).eval f = e.eval fun w => assign[w]?.getD (f w)) โˆง โˆ€ (v : Nat), v โˆˆ vars (normalize' assign e) โ†’ assign.contains v = false := by - fun_induction normalize' with grind + fun_induction normalize' with grind +locals example (assign : Std.TreeMap Nat Bool) (e : IfExpr) : (normalize' assign e).normalized โˆง (โˆ€ f, (normalize' assign e).eval f = e.eval fun w => assign[w]?.getD (f w)) โˆง โˆ€ (v : Nat), v โˆˆ vars (normalize' assign e) โ†’ assign[v]? = none := by - fun_induction normalize' with grind + fun_induction normalize' with grind +locals end IfExpr diff --git a/tests/lean/run/grind_locals.lean b/tests/lean/run/grind_locals.lean new file mode 100644 index 0000000000..7579e33a16 --- /dev/null +++ b/tests/lean/run/grind_locals.lean @@ -0,0 +1,51 @@ +/-! +# Test for `grind +locals` flag + +This tests that `grind +locals` adds local definitions from the current file. +-/ + +-- A simple definition that provides an equation grind can use +def foo (n : Nat) : Nat := n + 1 + +-- Without +locals, grind shouldn't know about foo +-- (This test verifies +locals is actually doing something) +/-- +error: `grind` failed +case grind +n : Nat +h : ยฌfoo n = n + 1 +โŠข False +[grind] Goal diagnostics + [facts] Asserted facts + [prop] ยฌfoo n = n + 1 + [eqc] False propositions + [prop] foo n = n + 1 + [cutsat] Assignment satisfying linear constraints + [assign] n := 0 + [assign] foo n := 2 +-/ +#guard_msgs in +example (n : Nat) : foo n = n + 1 := by grind + +-- Test that grind +locals can use the equation `foo n = n + 1` +example (n : Nat) : foo n = n + 1 := by grind +locals + +-- An irrelevant definition that should NOT appear in grind? suggestions +def bar (n : Nat) : Nat := n * 2 + +-- Test that grind? +locals suggests only the relevant definition (foo), not bar +/-- +info: Try these: + [apply] grind only [foo] + [apply] grind => instantiate only [foo] +-/ +#guard_msgs in +example (n : Nat) : foo n = n + 1 := by grind? +locals + +-- Test with a definition that has multiple equations via pattern matching +def isZero : Nat โ†’ Bool + | 0 => true + | _ + 1 => false + +example : isZero 0 = true := by grind +locals +example (n : Nat) : isZero (n + 1) = false := by grind +locals diff --git a/tests/lean/run/grind_locals_module.lean b/tests/lean/run/grind_locals_module.lean new file mode 100644 index 0000000000..41c05672b5 --- /dev/null +++ b/tests/lean/run/grind_locals_module.lean @@ -0,0 +1,56 @@ + +module +/-! +# Test for `grind +locals` flag (using the module system) + +This tests that `grind +locals` adds local definitions from the current file. + +We have a separate test here as the current semantics for `isImplementationDetail` are poorly specified, +and we've had to work around it in deciding which declarations should be included via `+locals`. +-/ + +-- A simple definition that provides an equation grind can use +def foo (n : Nat) : Nat := n + 1 + +-- Without +locals, grind shouldn't know about foo +-- (This test verifies +locals is actually doing something) +/-- +error: `grind` failed +case grind +n : Nat +h : ยฌfoo n = n + 1 +โŠข False +[grind] Goal diagnostics + [facts] Asserted facts + [prop] ยฌfoo n = n + 1 + [eqc] False propositions + [prop] foo n = n + 1 + [cutsat] Assignment satisfying linear constraints + [assign] n := 0 + [assign] foo n := 2 +-/ +#guard_msgs in +example (n : Nat) : foo n = n + 1 := by grind + +-- Test that grind +locals can use the equation `foo n = n + 1` +example (n : Nat) : foo n = n + 1 := by grind +locals + +-- An irrelevant definition that should NOT appear in grind? suggestions +def bar (n : Nat) : Nat := n * 2 + +-- Test that grind? +locals suggests only the relevant definition (foo), not bar +/-- +info: Try these: + [apply] grind only [foo] + [apply] grind => instantiate only [foo] +-/ +#guard_msgs in +example (n : Nat) : foo n = n + 1 := by grind? +locals + +-- Test with a definition that has multiple equations via pattern matching +def isZero : Nat โ†’ Bool + | 0 => true + | _ + 1 => false + +example : isZero 0 = true := by grind +locals +example (n : Nat) : isZero (n + 1) = false := by grind +locals