From 4786e082dca22873d14d2a5b9b7c8843380c6e78 Mon Sep 17 00:00:00 2001 From: Kim Morrison <477956+kim-em@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:20:49 +1100 Subject: [PATCH] doc: update `inferInstanceAs` docstring and rename `normalizeInstance` to `wrapInstance` (#13115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates the `inferInstanceAs` docstring to reflect current behavior: it requires an expected type from context and should not be used as a simple `inferInstance` synonym. The old example (`#check inferInstanceAs (Inhabited Nat)`) no longer works, so it's replaced with one demonstrating the intended transport use case. Additionally, renames `InstanceNormalForm.lean` to `WrapInstance.lean`, `normalizeInstance` to `wrapInstance`, and the trace class `Meta.instanceNormalForm` to `Meta.wrapInstance`, removing the "instance normal form" terminology from both documentation and code. Context: https://leanprover.zulipchat.com/#narrow/channel/270676-lean4/topic/inferInstanceAs.20is.20broken/near/581449313 🤖 Prepared with Claude Code --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/Init/Prelude.lean | 20 ++++++---- src/Lean/Elab/BuiltinTerm.lean | 6 +-- src/Lean/Elab/Deriving/Basic.lean | 10 ++--- src/Lean/Elab/PreDefinition/Basic.lean | 2 +- src/Lean/Meta.lean | 2 +- ...tanceNormalForm.lean => WrapInstance.lean} | 37 +++++++++---------- src/Lean/Parser/Term.lean | 13 +++++-- tests/elab/12897.lean | 4 +- tests/elab/Reparen.lean.out.expected | 20 ++++++---- 9 files changed, 65 insertions(+), 49 deletions(-) rename src/Lean/Meta/{InstanceNormalForm.lean => WrapInstance.lean} (82%) diff --git a/src/Init/Prelude.lean b/src/Init/Prelude.lean index 96773d39dc..2dad82a0eb 100644 --- a/src/Init/Prelude.lean +++ b/src/Init/Prelude.lean @@ -185,15 +185,21 @@ example : foo.default = (default, default) := abbrev inferInstance {α : Sort u} [i : α] : α := i set_option checkBinderAnnotations false in -/-- `inferInstanceAs α` synthesizes an instance of type `α` and normalizes it to -"instance normal form": the result is a constructor application whose sub-instance fields -are canonical instances and whose types match `α` exactly. This is useful when `α` is -definitionally equal to some `α'` for which instances are registered, as it prevents -leaking the definition's RHS at lower transparencies. See `Lean.Meta.InstanceNormalForm` -for details. Example: +/-- `inferInstanceAs α` synthesizes an instance of type `α`, transporting it from a +definitionally equal type if necessary. This is useful when `α` is definitionally equal to +some `α'` for which instances are registered, as it prevents leaking the definition's RHS +at lower transparencies. + +`inferInstanceAs` requires an expected type from context. If you just need to synthesize an +instance without transporting between types, use `inferInstance` instead. + +Example: ``` -#check inferInstanceAs (Inhabited Nat) -- Inhabited Nat +def D := Nat +instance : Inhabited D := inferInstanceAs (Inhabited Nat) ``` + +See `Lean.Meta.WrapInstance` for details. -/ abbrev «inferInstanceAs» (α : Sort u) [i : α] : α := i diff --git a/src/Lean/Elab/BuiltinTerm.lean b/src/Lean/Elab/BuiltinTerm.lean index 7d3711b758..ce1cf4ae45 100644 --- a/src/Lean/Elab/BuiltinTerm.lean +++ b/src/Lean/Elab/BuiltinTerm.lean @@ -7,7 +7,7 @@ module prelude public import Lean.Meta.Diagnostics -public import Lean.Meta.InstanceNormalForm +public import Lean.Meta.WrapInstance public import Lean.Elab.Open public import Lean.Elab.SetOption public import Lean.Elab.Eval @@ -334,10 +334,10 @@ private def mkSilentAnnotationIfHole (e : Expr) : TermElabM Expr := do let type ← abstractInstImplicitArgs type let inst ← synthInstance type let inst ← if backward.inferInstanceAs.wrap.get (← getOptions) then - -- Normalize to instance normal form. + -- Wrap instance so its type matches the expected type exactly. let logCompileErrors := !(← read).isNoncomputableSection && !(← read).declName?.any (Lean.isNoncomputable (← getEnv)) let isMeta := (← read).declName?.any (isMarkedMeta (← getEnv)) - withNewMCtxDepth <| normalizeInstance inst expectedType (logCompileErrors := logCompileErrors) (isMeta := isMeta) + withNewMCtxDepth <| wrapInstance inst expectedType (logCompileErrors := logCompileErrors) (isMeta := isMeta) else pure inst ensureHasType expectedType? inst diff --git a/src/Lean/Elab/Deriving/Basic.lean b/src/Lean/Elab/Deriving/Basic.lean index a74061c69b..de0826a328 100644 --- a/src/Lean/Elab/Deriving/Basic.lean +++ b/src/Lean/Elab/Deriving/Basic.lean @@ -9,7 +9,7 @@ prelude public import Lean.Elab.App public import Lean.Elab.DeclNameGen import Lean.Compiler.NoncomputableAttr -import Lean.Meta.InstanceNormalForm +import Lean.Meta.WrapInstance public section @@ -211,10 +211,10 @@ def processDefDeriving (view : DerivingClassView) (decl : Expr) (isNoncomputable -- We don't reduce because of abbreviations such as `DecidableEq` forallTelescope classExpr fun _ classExpr => do let result ← mkInst classExpr declName decl value - -- Save the pre-normalization value for the noncomputable check below, - -- since `normalizeInstance` may inline noncomputable constants. + -- Save the pre-wrapping value for the noncomputable check below, + -- since `wrapInstance` may inline noncomputable constants. let preNormClosure ← Closure.mkValueTypeClosure result.instType result.instVal (zetaDelta := true) - -- Compute instance name early so `normalizeInstance` can use it for aux def naming. + -- Compute instance name early so `wrapInstance` can use it for aux def naming. let env ← getEnv let mut instName := (← getCurrNamespace) ++ (← NameGen.mkBaseNameWithSuffix "inst" preNormClosure.type) instName ← liftMacroM <| mkUnusedBaseName instName @@ -223,7 +223,7 @@ def processDefDeriving (view : DerivingClassView) (decl : Expr) (isNoncomputable let isMeta := (← read).declName?.any (isMarkedMeta (← getEnv)) let inst ← if backward.inferInstanceAs.wrap.get (← getOptions) then withDeclNameForAuxNaming instName <| withNewMCtxDepth <| - normalizeInstance result.instVal result.instType + wrapInstance result.instVal result.instType (logCompileErrors := false) -- covered by noncomputable check below (isMeta := isMeta) else diff --git a/src/Lean/Elab/PreDefinition/Basic.lean b/src/Lean/Elab/PreDefinition/Basic.lean index 3247a4a473..885df3685f 100644 --- a/src/Lean/Elab/PreDefinition/Basic.lean +++ b/src/Lean/Elab/PreDefinition/Basic.lean @@ -10,7 +10,7 @@ public import Lean.Compiler.NoncomputableAttr public import Lean.Util.NumApps public import Lean.Meta.Eqns public import Lean.Elab.RecAppSyntax -public import Lean.Meta.InstanceNormalForm +public import Lean.Meta.WrapInstance public import Lean.Elab.DefView public section diff --git a/src/Lean/Meta.lean b/src/Lean/Meta.lean index 6e71d999f9..420e36efb3 100644 --- a/src/Lean/Meta.lean +++ b/src/Lean/Meta.lean @@ -27,7 +27,7 @@ public import Lean.Meta.Match public import Lean.Meta.ReduceEval public import Lean.Meta.Closure public import Lean.Meta.AbstractNestedProofs -public import Lean.Meta.InstanceNormalForm +public import Lean.Meta.WrapInstance public import Lean.Meta.LetToHave public import Lean.Meta.ForEachExpr public import Lean.Meta.Transform diff --git a/src/Lean/Meta/InstanceNormalForm.lean b/src/Lean/Meta/WrapInstance.lean similarity index 82% rename from src/Lean/Meta/InstanceNormalForm.lean rename to src/Lean/Meta/WrapInstance.lean index 86d53c3851..b71e72965f 100644 --- a/src/Lean/Meta/InstanceNormalForm.lean +++ b/src/Lean/Meta/WrapInstance.lean @@ -13,17 +13,16 @@ public import Lean.Meta.CtorRecognizer public section /-! -# Instance Normal Form +# Instance Wrapping -Both `inferInstanceAs` and the default `deriving` handler normalize instance bodies to -"instance normal form". This ensures that when deriving or inferring an instance for a -semireducible type definition, the definition's RHS is not leaked when reduced at lower -than semireducible transparency. +Both `inferInstanceAs` and the default `deriving` handler wrap instance bodies to ensure +that when deriving or inferring an instance for a semireducible type definition, the +definition's RHS is not leaked when reduced at lower than semireducible transparency. ## Algorithm Given an instance `i : I` and expected type `I'` (where `I'` must be mvar-free), -`normalizeInstance` constructs a result instance as follows, executing all steps at +`wrapInstance` constructs a result instance as follows, executing all steps at `instances` transparency: 1. If `I'` is not a class, return `i` unchanged. @@ -46,7 +45,7 @@ Given an instance `i : I` and expected type `I'` (where `I'` must be mvar-free), ## Options -- `backward.inferInstanceAs.wrap`: master switch for normalization in both `inferInstanceAs` +- `backward.inferInstanceAs.wrap`: master switch for wrapping in both `inferInstanceAs` and the default `deriving` handler - `backward.inferInstanceAs.wrap.reuseSubInstances`: reuse existing instances for sub-instance fields to avoid non-defeq instance diamonds @@ -59,7 +58,7 @@ namespace Lean.Meta register_builtin_option backward.inferInstanceAs.wrap : Bool := { defValue := true - descr := "normalize instance bodies to constructor-based normal form in `inferInstanceAs` and the default `deriving` handler" + descr := "wrap instance bodies in `inferInstanceAs` and the default `deriving` handler" } register_builtin_option backward.inferInstanceAs.wrap.reuseSubInstances : Bool := { @@ -77,7 +76,7 @@ register_builtin_option backward.inferInstanceAs.wrap.data : Bool := { descr := "wrap data fields in auxiliary definitions to fix their types" } -builtin_initialize registerTraceClass `Meta.instanceNormalForm +builtin_initialize registerTraceClass `Meta.wrapInstance /-- Rebuild a type application with fresh synthetic metavariables for instance-implicit arguments. @@ -95,16 +94,16 @@ def abstractInstImplicitArgs (type : Expr) : MetaM Expr := do instantiateMVars (mkAppN fn args) /-- -Normalize an instance value to "instance normal form". +Wrap an instance value so its type matches the expected type exactly. See the module docstring for the full algorithm specification. -/ -partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true) +partial def wrapInstance (inst expectedType : Expr) (compile : Bool := true) (logCompileErrors : Bool := true) (isMeta : Bool := false) : MetaM Expr := withTransparency .instances do - withTraceNode `Meta.instanceNormalForm + withTraceNode `Meta.wrapInstance (fun _ => return m!"type: {expectedType}") do let some className ← isClass? expectedType | return inst - trace[Meta.instanceNormalForm] "class is {className}" + trace[Meta.wrapInstance] "class is {className}" if ← isProp expectedType then if backward.inferInstanceAs.wrap.instances.get (← getOptions) then @@ -117,7 +116,7 @@ partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true inst.withApp fun f args => do let some (.ctorInfo ci) ← f.constName?.mapM getConstInfo | do - trace[Meta.instanceNormalForm] "did not reduce to constructor application, returning/wrapping as is: {inst}" + trace[Meta.wrapInstance] "did not reduce to constructor application, returning/wrapping as is: {inst}" if backward.inferInstanceAs.wrap.instances.get (← getOptions) then let instType ← inferType inst if ← isDefEq expectedType instType then @@ -135,11 +134,11 @@ partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true return inst let (mvars, _, cls) ← forallMetaTelescope (← inferType f) if h₁ : args.size ≠ mvars.size then - throwError "instance normal form: incorrect number of arguments for \ + throwError "wrapInstance: incorrect number of arguments for \ constructor application `{f}`: {args}" else unless ← isDefEq expectedType cls do - throwError "instance normal form: `{expectedType}` does not unify with the conclusion of \ + throwError "wrapInstance: `{expectedType}` does not unify with the conclusion of \ `{.ofConstName ci.name}`" for h₂ : i in ci.numParams...args.size do have : i < mvars.size := by @@ -155,7 +154,7 @@ partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true if ← isDefEq argExpectedType argType then mvarId.assign arg else - trace[Meta.instanceNormalForm] "proof field {i} does not have expected type {argExpectedType} but {argType}, wrapping in auxiliary theorem: {arg}" + trace[Meta.wrapInstance] "proof field {i} does not have expected type {argExpectedType} but {argType}, wrapping in auxiliary theorem: {arg}" mvarId.assign (← mkAuxTheorem argExpectedType arg (zetaDelta := true)) -- Recurse into instance arguments of the constructor else if (← isClass? argExpectedType).isSome then @@ -165,12 +164,12 @@ partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true -- semireducible transparency. try if let .some new ← trySynthInstance argExpectedType then - trace[Meta.instanceNormalForm] "using existing instance {new}" + trace[Meta.wrapInstance] "using existing instance {new}" mvarId.assign new continue catch _ => pure () - mvarId.assign (← normalizeInstance arg argExpectedType (compile := compile) + mvarId.assign (← wrapInstance arg argExpectedType (compile := compile) (logCompileErrors := logCompileErrors) (isMeta := isMeta)) else -- For data fields, assign directly or wrap in aux def to fix types. diff --git a/src/Lean/Parser/Term.lean b/src/Lean/Parser/Term.lean index 69a3a3f95e..9617b34ef5 100644 --- a/src/Lean/Parser/Term.lean +++ b/src/Lean/Parser/Term.lean @@ -774,10 +774,15 @@ In particular, it is like a unary operation with a fixed parameter `b`, where on @[builtin_term_parser] def noImplicitLambda := leading_parser "no_implicit_lambda% " >> termParser maxPrec /-- -`inferInstanceAs α` synthesizes an instance of type `α` and normalizes it to -"instance normal form": the result is a constructor application whose sub-instance -fields are canonical instances and whose types match `α` exactly. See -`Lean.Meta.InstanceNormalForm` for details. +`inferInstanceAs α` synthesizes an instance of type `α`, transporting it from a +definitionally equal type if necessary. This is useful when `α` is definitionally equal to +some `α'` for which instances are registered, as it prevents leaking the definition's RHS +at lower transparencies. + +`inferInstanceAs` requires an expected type from context. If you just need to synthesize an +instance without transporting between types, use `inferInstance` instead. + +See `Lean.Meta.WrapInstance` for details. -/ @[builtin_term_parser] def «inferInstanceAs» := leading_parser "inferInstanceAs" >> (((" $ " <|> " <| ") >> termParser minPrec) <|> (ppSpace >> termParser argPrec)) diff --git a/tests/elab/12897.lean b/tests/elab/12897.lean index 7a7718f9f8..194552f520 100644 --- a/tests/elab/12897.lean +++ b/tests/elab/12897.lean @@ -1,4 +1,4 @@ --- Tests that `normalizeInstance` auxiliary definitions work correctly +-- Tests that `wrapInstance` auxiliary definitions work correctly -- when used inside a `meta` section, for both `inferInstanceAs` and `deriving`. module @@ -11,7 +11,7 @@ namespace Test open Lean --- `@[expose]` forces `normalizeInstance` to create aux wrapper definitions, +-- `@[expose]` forces `wrapInstance` to create aux wrapper definitions, -- which is where the meta marking matters. @[expose] def Foo := Unit deriving Inhabited diff --git a/tests/elab/Reparen.lean.out.expected b/tests/elab/Reparen.lean.out.expected index 021cd69b1a..c4c49bc1aa 100644 --- a/tests/elab/Reparen.lean.out.expected +++ b/tests/elab/Reparen.lean.out.expected @@ -185,15 +185,21 @@ example : foo.default = (default, default) := abbrev inferInstance {α : Sort u} [i : α] : α := i set_option checkBinderAnnotations false in -/-- `inferInstanceAs α` synthesizes an instance of type `α` and normalizes it to -"instance normal form": the result is a constructor application whose sub-instance fields -are canonical instances and whose types match `α` exactly. This is useful when `α` is -definitionally equal to some `α'` for which instances are registered, as it prevents -leaking the definition's RHS at lower transparencies. See `Lean.Meta.InstanceNormalForm` -for details. Example: +/-- `inferInstanceAs α` synthesizes an instance of type `α`, transporting it from a +definitionally equal type if necessary. This is useful when `α` is definitionally equal to +some `α'` for which instances are registered, as it prevents leaking the definition's RHS +at lower transparencies. + +`inferInstanceAs` requires an expected type from context. If you just need to synthesize an +instance without transporting between types, use `inferInstance` instead. + +Example: ``` -#check inferInstanceAs (Inhabited Nat) -- Inhabited Nat +def D := Nat +instance : Inhabited D := inferInstanceAs (Inhabited Nat) ``` + +See `Lean.Meta.WrapInstance` for details. -/ abbrev «inferInstanceAs» (α : Sort u) [i : α] : α := i