lean4-htt/src/Lean/Meta/SplitSparseCasesOn.lean
Kim Morrison e01cbf2b8f
feat: add structured TraceResult to TraceData (#12698)
This PR adds a `result? : Option TraceResult` field to `TraceData` and
populates it in `withTraceNode` and `withTraceNodeBefore`, so that
metaprograms walking trace trees can determine success/failure
structurally instead of string-matching on emoji.

`TraceResult` has three cases: `.success` (checkEmoji), `.failure`
(crossEmoji), and `.error` (bombEmoji, exception thrown). An
`ExceptToTraceResult` typeclass converts `Except` results to
`TraceResult` directly, with instances for `Bool` and `Option`.
`TraceResult.toEmoji` converts back to emoji for display. This replaces
the previous `ExceptToEmoji` typeclass — `TraceResult` is now the
primary representation rather than being derived from emoji strings.

`withTraceNodeBefore` (used by `isDefEq`) uses
`ExceptToTraceResult.toTraceResult` directly, correctly handling `Bool`
(`.ok false` = failure) and `Option` (`.ok none` = failure), with
`Except.error` mapping to `.error`.

For `withTraceNode`, `result?` defaults to `none`. Callers can pass
`mkResult?` to provide structured results; when set, the corresponding
emoji is auto-prepended to the message.

Motivated by mathlib's `#defeq_abuse` diagnostic tactic
(https://github.com/leanprover-community/mathlib4/pull/35750) which
currently string-matches on emoji to determine trace node outcomes. See
https://leanprover.zulipchat.com/#narrow/channel/113488-general/topic/backward.2EisDefEq.2ErespectTransparency

🤖 Prepared with Claude Code

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 02:42:57 +00:00

80 lines
3.9 KiB
Text

/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Joachim Breitner
-/
module
prelude
public import Lean.Meta.Basic
import Lean.Meta.Tactic.Rewrite
import Lean.Meta.Constructions.SparseCasesOn
import Lean.Meta.Constructions.SparseCasesOnEq
import Lean.Meta.HasNotBit
import Lean.Meta.Tactic.Cases
import Lean.Meta.Tactic.Replace
namespace Lean.Meta
private def rewriteGoalUsingEq (goal : MVarId) (eq : Expr) (symm : Bool := false) : MetaM MVarId := do
let rewriteResult ← goal.rewrite (←goal.getType) eq symm
goal.replaceTargetEq rewriteResult.eNew rewriteResult.eqProof
/--
Reduces a sparse casesOn applied to a concrete constructor.
-/
public def reduceSparseCasesOn (mvarId : MVarId) : MetaM (Array MVarId) := do
let some (_, lhs) ← matchEqHEqLHS? (← mvarId.getType) | throwError "Target not an equality"
lhs.withApp fun f xs => do
let .const matchDeclName _ := f | throwError "Not a const application"
let some sparseCasesOnInfo ← getSparseCasesOnInfo matchDeclName | throwError "Not a sparse casesOn application"
withTraceNode `Meta.Match.matchEqs (msg := (fun _ => return m!"splitSparseCasesOn")) do
if xs.size < sparseCasesOnInfo.arity then
throwError "Not enough arguments for sparse casesOn application"
let majorIdx := sparseCasesOnInfo.majorPos
let major := xs[majorIdx]!
let some ctorInfo ← isConstructorApp'? major
| throwError "Major premise is not a constructor application:{indentExpr major}"
if sparseCasesOnInfo.insterestingCtors.contains ctorInfo.name then
let mvarId' ← mvarId.modifyTargetEqLHS fun lhs =>
unfoldDefinition lhs
return #[mvarId']
else
let sparseCasesOnEqName ← getSparseCasesOnEq matchDeclName
let eqProof := mkConst sparseCasesOnEqName lhs.getAppFn.constLevels!
let eqProof := mkAppN eqProof lhs.getAppArgs[:sparseCasesOnInfo.arity]
let eqProof := mkApp eqProof (← mkHasNotBitProof (mkRawNatLit ctorInfo.cidx) (← sparseCasesOnInfo.insterestingCtors.mapM (fun n => return (← getConstInfoCtor n).cidx)))
let mvarId' ← rewriteGoalUsingEq mvarId eqProof
return #[mvarId']
public def splitSparseCasesOn (mvarId : MVarId) : MetaM (Array MVarId) := do
let some (_, lhs) ← matchEqHEqLHS? (← mvarId.getType) | throwError "Target not an equality"
lhs.withApp fun f xs => do
let .const matchDeclName _ := f | throwError "Not a const application"
let some sparseCasesOnInfo ← getSparseCasesOnInfo matchDeclName | throwError "Not a sparse casesOn application"
withTraceNode `Meta.Match.matchEqs (msg := (fun _ => return m!"splitSparseCasesOn")) do
try
trace[Meta.Match.matchEqs] "splitSparseCasesOn running on\n{mvarId}"
if xs.size < sparseCasesOnInfo.arity then
throwError "Not enough arguments for sparse casesOn application"
let majorIdx := sparseCasesOnInfo.majorPos
unless xs[majorIdx]!.isFVar do
throwError "Major premise is not a free variable:{indentExpr xs[majorIdx]!}"
let fvarId := xs[majorIdx]!.fvarId!
let subgoals ← mvarId.cases fvarId (interestingCtors? := sparseCasesOnInfo.insterestingCtors)
subgoals.mapM fun s => s.mvarId.withContext do
if s.ctorName.isNone then
unless s.fields.size = 1 do
throwError "Unexpected number of fields for catch-all branch: {s.fields}"
let sparseCasesOnEqName ← getSparseCasesOnEq matchDeclName
let some (_, lhs) ← matchEqHEqLHS? (← s.mvarId.getType) | throwError "Target not an equality"
let eqProof := mkConst sparseCasesOnEqName lhs.getAppFn.constLevels!
let eqProof := mkAppN eqProof lhs.getAppArgs[:sparseCasesOnInfo.arity]
let eqProof := mkApp eqProof s.fields[0]!
rewriteGoalUsingEq s.mvarId eqProof
else
s.mvarId.modifyTargetEqLHS fun lhs =>
unfoldDefinition lhs
catch e =>
trace[Meta.Match.matchEqs] "splitSparseCasesOn failed{indentD e.toMessageData}"
throw e