lean4-htt/tests/elab/versoDocModuleGiven.lean
David Thrane Christiansen 659249a7df
fix: always fail on unsolved metavariables after Verso docstring elab (#13574)
This PR ensures consistent metavariable behavior between Verso
docstrings and Verso moduledocs by sharing more code between their
elaborators. It also improves the error message when a metavariable leak
is prevented.
2026-05-01 22:34:52 +00:00

393 lines
12 KiB
Text
Raw Permalink 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.

/-
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.name == 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.name == 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