| /- | |
| Tests that `{given}` in Verso module docs does not leak metavariables or | |
| typeclass constraints into subsequent declarations, and that unsolved | |
| metavariables in `{given}` types are reported as errors. | |
| -/ | |
| import Lean | |
| set_option doc.verso true | |
| set_option doc.verso.module true | |
| -- Universe metavariable indices are fed by a global counter and can shift if | |
| -- anything upstream changes; suppress them so `#guard_msgs` stays stable. | |
| set_option pp.mvars.levels false | |
| open Lean Doc Elab Term | |
| /-! | |
| # Helpers | |
| This section contains helpers used in the tests in this file. | |
| -/ | |
| -- Helpers to inspect elaborated doc content | |
| private def showDocHl (code : Array (String × Option DocHighlight)) : Std.Format := | |
| .group (behavior := .fill) <| code.foldl (init := .nil) fun | |
| | soFar, (str, none) => soFar.append str | |
| | soFar, (str, some hl) => soFar ++ .line ++ (entaggen hl str) | |
| where | |
| entaggen hl str := (opener hl : Format) ++ str ++ closer hl | |
| opener | |
| | .const x sig => s!"<const name=\"{x}\" sig=\"{sig}\">" | |
| | .var x _fv ty => s!"<var name=\"{x}\" type=\"{ty}\">" | |
| | .field x sig => s!"<field name=\"{x}\" sig=\"{sig}\">" | |
| | .option x d => s!"<option name=\"{x}\" decl=\"{d}\">" | |
| | .keyword => "<kw>" | |
| | .literal k (some ty) => s!"<lit kind=\"{k}\" type=\"{ty}\">" | |
| | .literal k none => s!"<lit kind=\"{k}\" type=none>" | |
| closer | |
| | .const .. => "</const>" | |
| | .var .. => "</var>" | |
| | .field .. => "</field>" | |
| | .option .. => "</option>" | |
| | .keyword => "</kw>" | |
| | .literal .. => "</lit>" | |
| private def docCodeStr (dc : DocCode) : String := | |
| showDocHl dc.code |>.pretty (width := 70) | |
| /-- | |
| Finds the highlighted code segments in an inline element. | |
| -/ | |
| private partial def findInInline (name : Name) : Inline ElabInline → Array DocCode | |
| | .other container _ => | |
| if container.val.typeName == name then | |
| if let some (lt : Data.LeanTerm) := container.val.get? Data.LeanTerm then | |
| #[lt.term] | |
| else #[] | |
| else #[] | |
| | .emph xs | .bold xs | .concat xs | .link xs _ | .footnote _ xs => | |
| xs.flatMap (findInInline name) | |
| | .text .. | .code .. | .math .. | .linebreak .. | .image .. => #[] | |
| /-- | |
| Finds the highlighted code segments in a block element. | |
| -/ | |
| private partial def findInBlock (name : Name) : Block ElabInline ElabBlock → Array DocCode | |
| | .other container _ => | |
| if container.val.typeName == name then | |
| if let some (lb : Data.LeanBlock) := container.val.get? Data.LeanBlock then | |
| #[lb.commands] | |
| else if let some (lt : Data.LeanTerm) := container.val.get? Data.LeanTerm then | |
| #[lt.term] | |
| else #[] | |
| else #[] | |
| | .para inlines => inlines.flatMap (findInInline name) | |
| | .concat blocks | .blockquote blocks => blocks.flatMap (findInBlock name) | |
| | .dl items => items.flatMap fun ⟨x, y⟩ => x.flatMap (findInInline name) ++ y.flatMap (findInBlock name) | |
| | .ol _ xs | .ul xs => xs.flatMap fun ⟨x⟩ => x.flatMap (findInBlock name) | |
| | .code .. => #[] | |
| /-! # Tests -/ | |
| section ExplicitlyTypedGiven | |
| /-! | |
| Test that {name}`given` produces correct highlights. | |
| -/ | |
| -- No error: | |
| #guard_msgs in | |
| /-! | |
| Given {given}`α : Type` and {given}`x : α, y : α` with {givenInstance}`Add α`, | |
| we have {lean}`x + y : α`. | |
| -/ | |
| -- Check that the explicitly-typed case produces proper highlights. | |
| #eval show TermElabM Unit from do | |
| let env ← getEnv | |
| let docs := getMainVersoModuleDocs env | |
| let s := docs.snippets.get! 0 | |
| for lt in s.text.flatMap (findInBlock ``Data.LeanTerm) do | |
| let str := docCodeStr lt | |
| if str.contains "?" then | |
| throwError "BUG: metavariable in explicitly-typed given highlight: {str}" | |
| end ExplicitlyTypedGiven | |
| section UnsolvedMetaError | |
| /-! | |
| A {lit}`{given}` with no explicit type that is never constrained should be an error. | |
| -/ | |
| /-- | |
| error: unresolved metavariable in `given` (type of variable `k`): | |
| ?m.2 | |
| -/ | |
| #guard_msgs in | |
| /-! | |
| Given {given}`k`, we consider {lean}`k`. | |
| -/ | |
| /-! | |
| A {lit}`{given}` with no explicit type that is only partially constrained should also be an error. | |
| -/ | |
| /-- | |
| error: don't know how to synthesize implicit argument `α` | |
| @List.length ?m.6 xs | |
| context: | |
| xs : List ?m.6 | |
| ⊢ Type _ | |
| --- | |
| error: unresolved metavariable in `given` (type of variable `xs`): | |
| List ?m.6 | |
| -/ | |
| #guard_msgs in | |
| /-! | |
| Given {given}`xs`, we can check the length {lean}`List.length xs` | |
| -/ | |
| /-! | |
| Occurrences of a defined name in its own docstring are not automatically instantiated with the | |
| implicit arguments, so this is also an error. | |
| -/ | |
| /-- | |
| error: don't know how to synthesize implicit argument `α` | |
| @doubleList ?m.13 xs | |
| context: | |
| α : Type u_1 | |
| xs✝ : List α | |
| xs : List ?m.13 | |
| ⊢ Type _ | |
| --- | |
| error: unresolved metavariable in `given` (type of variable `xs`): | |
| List ?m.13 | |
| -/ | |
| #guard_msgs in | |
| /-- | |
| Given {given}`xs`, we can double elements with {lean}`doubleList xs` | |
| -/ | |
| def doubleList (xs : List α) : List α := xs.flatMap fun x => [x, x] | |
| /-! | |
| A {lit}`{given}` whose type contains a {lit}`_` placeholder that is never constrained should | |
| report the unresolved type once at the role syntax, plus the standard placeholder diagnostic at | |
| the underscore. | |
| -/ | |
| /-- | |
| error: unresolved metavariable in `given` (type of variable `xs`): | |
| List ?m.1 | |
| --- | |
| error: don't know how to synthesize placeholder for argument `α` | |
| context: | |
| ⊢ Type _ | |
| -/ | |
| #guard_msgs in | |
| /-! | |
| Given {given}`xs : List _`, we have {lean}`xs`. | |
| -/ | |
| /-! | |
| A partial type with multiple unresolved holes should still produce a single doc-context error, | |
| plus one standard placeholder error per hole. | |
| -/ | |
| /-- | |
| error: unresolved metavariable in `given` (type of variable `xs`): | |
| ?m.1 ⊕ ?m.2 | |
| --- | |
| error: don't know how to synthesize placeholder for argument `β` | |
| context: | |
| ⊢ Type _ | |
| --- | |
| error: don't know how to synthesize placeholder for argument `α` | |
| context: | |
| ⊢ Type _ | |
| -/ | |
| #guard_msgs in | |
| /-! | |
| Given {given}`xs : Sum _ _`, we have {lean}`xs`. | |
| -/ | |
| /-! | |
| A {lit}`{given}` whose value contains an unresolved hole should produce a single doc-context error | |
| naming the variable, plus the standard placeholder/implicit-arg errors for each hole in the value. | |
| -/ | |
| /-- | |
| error: unresolved metavariable in `given` (value of variable `k`): | |
| List.length ?m.2 | |
| --- | |
| error: don't know how to synthesize implicit argument `α` | |
| @List.length ?m.1 ?m.2 | |
| context: | |
| ⊢ Type _ | |
| --- | |
| error: don't know how to synthesize placeholder | |
| context: | |
| ⊢ List ?m.1 | |
| -/ | |
| #guard_msgs in | |
| /-! | |
| Given {given}`k := List.length _ : Nat`, we have {lean}`k`. | |
| -/ | |
| end UnsolvedMetaError | |
| section UnconstrainedInstance | |
| -- A givenInstance whose type contains an unresolved hole. One doc-context error points at the | |
| -- role syntax; the standard placeholder error points at the underscore. Neither is attached to | |
| -- the surrounding declaration or a subsequent lean snippet. | |
| /-- | |
| error: unresolved metavariable in `givenInstance` (instance type): | |
| Add ?m.1 | |
| --- | |
| error: don't know how to synthesize placeholder for argument `α` | |
| context: | |
| ⊢ Type _ | |
| -/ | |
| #guard_msgs in | |
| /-! | |
| Adding {givenInstance}`Add _`, we mention {lean}`Nat.zero`. | |
| -/ | |
| -- The fvar instance `Add ?m` is universally applicable to any `Add τ` goal | |
| -- by unifying `?m := τ`, but synth can't commit to assigning an external | |
| -- metavariable, so it gets stuck rather than failing. Default instances | |
| -- don't fire on stuck searches, and the placeholder error is suppressed since | |
| -- `?m` is no longer pending. This matches general Lean TC behavior; recorded | |
| -- here so a future change is noticed. | |
| /-- | |
| error: typeclass instance problem is stuck, it is often due to metavariables | |
| HAdd Nat Nat ?m.10 | |
| -/ | |
| #guard_msgs in | |
| /-! | |
| Adding {givenInstance}`Add _`, we compute {lean}`(0 : Nat) + 0`. | |
| -/ | |
| end UnconstrainedInstance | |
| section InferredTypeResolved | |
| /-! | |
| A {lit}`{given}` with no explicit type that IS constrained by a later {lit}`{lean}` should be fine. | |
| -/ | |
| /-! | |
| Given {given}`k`, we have {lean}`k + (1 : Nat)`. | |
| -/ | |
| end InferredTypeResolved | |
| section TypeclassConstraintLeak | |
| /-! | |
| Given {given}`β` and {given}`a : β, b : β`, with {givenInstance}`Add β`, | |
| we can form {lean}`a + b`. | |
| -/ | |
| -- If typeclass constraints from the preceding moduledoc leak, this theorem could acquire an | |
| -- unwanted `Add` constraint or fail to elaborate. The theorem should have exactly the signature we | |
| -- write, nothing extra. | |
| theorem noExtraConstraints (n m : Nat) : n + m = m + n := Nat.add_comm n m | |
| open Lean in | |
| #eval show TermElabM Unit from do | |
| let env ← getEnv | |
| let ci := env.constants.find? ``noExtraConstraints |>.get! | |
| -- The type should be `∀ (n m : Nat), n + m = m + n` with no metavariables | |
| if ci.type.hasMVar then | |
| throwError "BUG: metavariable leaked into theorem type: {ci.type}" | |
| -- Check that the type doesn't mention `Add` (no extra typeclass constraint) | |
| let typeStr := toString (← Lean.Meta.MetaM.run' (Lean.PrettyPrinter.ppExpr ci.type)) | |
| if typeStr.contains "Add" then | |
| throwError "BUG: unwanted `Add` constraint leaked into theorem: {typeStr}" | |
| end TypeclassConstraintLeak | |
| section VariableInteraction | |
| -- Test interaction between section variables and moduledoc {given} | |
| variable (γ : Type) [Add γ] | |
| -- Succeeds because it refers to the section variables: | |
| #guard_msgs in | |
| /-! | |
| Given {given}`c : γ, d : γ`, we have {lean}`c + d`. | |
| -/ | |
| -- This theorem uses section variable γ. The moduledoc's `{given}` should not | |
| -- interfere with how γ is used here. | |
| theorem sectionVarClean (x y : γ) : x + y = x + y := rfl | |
| open Lean in | |
| #eval show TermElabM Unit from do | |
| let env ← getEnv | |
| let ci := env.constants.find? ``sectionVarClean |>.get! | |
| if ci.type.hasMVar then | |
| throwError "BUG: metavariable leaked into section theorem type: {ci.type}" | |
| end VariableInteraction | |
| section DecidableClassicalLeak | |
| /- | |
| If a `Decidable` instance from `givenInstance` in a moduledoc leaks into | |
| the elaboration of a subsequent theorem, the theorem would pick up the leaked | |
| `[Decidable p]` constraint instead of using `Classical.dec` from | |
| `open Classical`. This would manifest as `Decidable p` appearing in the | |
| theorem's type. | |
| -/ | |
| variable (p : Prop) | |
| /-! | |
| Given {givenInstance}`Decidable p`, we can write {lean}`if p then True else False`. | |
| -/ | |
| -- This theorem intentionally relies on Classical decidability, not a | |
| -- `[Decidable p]` instance. If the moduledoc's `givenInstance` leaked, | |
| -- the elaborator would use the leaked instance, adding `[Decidable p]` to | |
| -- the theorem's type instead of using `Classical.dec`. | |
| open Classical in | |
| theorem classicalOnly : p ∨ ¬p := em p | |
| -- The theorem's type must be `∀ (p : Prop), p ∨ ¬p` with no `Decidable` | |
| -- parameter. If the constraint leaked, it would be | |
| -- `∀ (p : Prop) [inst : Decidable p], p ∨ ¬p`. | |
| /-- | |
| info: classicalOnly (p : Prop) : p ∨ ¬p | |
| -/ | |
| #guard_msgs in | |
| #check classicalOnly | |
| -- Also verify programmatically | |
| open Lean in | |
| #eval show Elab.Term.TermElabM Unit from do | |
| let env ← getEnv | |
| let ci := env.constants.find? ``classicalOnly |>.get! | |
| let typeStr := toString (← Lean.Meta.MetaM.run' (Lean.PrettyPrinter.ppExpr ci.type)) | |
| if typeStr.contains "Decidable" then | |
| throwError "BUG: Decidable constraint leaked from moduledoc into theorem: {typeStr}" | |
| -- This is a more subtle case: a theorem that uses `decide`-like operations within its _body_ via | |
| -- `open Classical`, where a leaked Decidable instance would silently change the theorem's | |
| -- signature. | |
| open Classical in | |
| theorem classicalIte (q : Prop) : (if p then q else q) = q := by | |
| split <;> rfl | |
| -- Should be `∀ (p : Prop) (q : Prop), ...` with no `Decidable p`. | |
| /-- | |
| info: classicalIte (p q : Prop) : (if p then q else q) = q | |
| -/ | |
| #guard_msgs in | |
| #check classicalIte | |
| end DecidableClassicalLeak | |
| section DeclDocUnsolvedMeta | |
| -- Unsolved `given` metavariables in declaration docs should also be errors. | |
| /-- | |
| error: unresolved metavariable in `given` (type of variable `k`): | |
| ?m.3 | |
| -/ | |
| #guard_msgs in | |
| /-- | |
| Given {given}`k`, we have {lean}`k`. | |
| -/ | |
| def declDocUnsolved := () | |
| -- But a `given` whose type is resolved by a later `lean` should be fine. | |
| /-- | |
| Given {given}`k`, we have {lean}`k + (1 : Nat)`. | |
| -/ | |
| def declDocResolved := () | |
| end DeclDocUnsolvedMeta |