This is the groundwork for a tactic index in generated documentation, as there was in Lean 3. There are a few challenges to getting this to work well in Lean 4: * There's no natural notion of *tactic identity* - a tactic may be specified by multiple syntax rules (e.g. the pattern-matching version of `intro` is specified apart from the default version, but both are the same from a user perspective) * There's no natural notion of *tactic name* - here, we take the pragmatic choice of using the first keyword atom in the tactic's syntax specification, but this may need to be overridable someday. * Tactics are extensible, but we don't want to allow arbitrary imports to clobber existing tactic docstrings, which could become unpredictable in practice. For tactic identity, this PR introduces the notion of a *tactic alternative*, which is a `syntax` specification that is really "the same as" an existing tactic, but needs to be separate for technical reasons. This provides a notion of tactic identity, which we can use as the basis of a tactic index in generated documentation. Alternative forms of tactics are specified using a new `@[tactic_alt IDENT]` attribute, applied to the new tactic syntax. It is an error to declare a tactic syntax rule to be an alternative of another one that is itself an alternative. Documentation hovers now take alternatives into account, and display the docs for the canonical name. *Tactic tags*, created with the `register_tactic_tag` command, specify tags that may be applied to tactics. This is intended to be used by doc-gen and Verso. Tags may be applied using the `@[tactic_tag TAG1 TAG2 ...]` attribute on a canonical tactic parser, which may be used in any module to facilitate downstream projects introducing tags that apply to pre-existing tactics. Tags may not be removed, but it's fine to redundantly add them. The collection of tags, and the tactics to which they're applied, can be seen using the `#print tactic tags` command. *Extension documentation* provides a structured way to document extensions to tactics. The resulting documentation is gathered into a bulleted list at the bottom of the tactic's docstring. Extensions are added using the `tactic_extension TAC` command. This can be used when adding new interpretations of a tactic via `macro_rules`, when extending some table or search index used by the tactic, or in any other way. It is a command to facilitate its flexible use with various extension mechanisms.
178 lines
6.9 KiB
Text
178 lines
6.9 KiB
Text
/-
|
|
Copyright (c) 2024 Lean FRO, LLC. All rights reserved.
|
|
Released under Apache 2.0 license as described in the file LICENSE.
|
|
Authors: David Thrane Christiansen
|
|
-/
|
|
prelude
|
|
import Lean.DocString
|
|
import Lean.Elab.Command
|
|
import Lean.Parser.Tactic.Doc
|
|
import Lean.Parser.Command
|
|
|
|
namespace Lean.Elab.Tactic.Doc
|
|
open Lean.Parser.Tactic.Doc
|
|
open Lean.Elab.Command
|
|
open Lean.Parser.Command
|
|
|
|
@[builtin_command_elab «tactic_extension»] def elabTacticExtension : CommandElab
|
|
| `(«tactic_extension»|tactic_extension%$cmd $_) => do
|
|
throwErrorAt cmd "Missing documentation comment"
|
|
| `(«tactic_extension»|$docs:docComment tactic_extension $tac:ident) => do
|
|
let tacName ← liftTermElabM <| realizeGlobalConstNoOverloadWithInfo tac
|
|
|
|
if let some tgt' := alternativeOfTactic (← getEnv) tacName then
|
|
throwErrorAt tac "'{tacName}' is an alternative form of '{tgt'}'"
|
|
if !(isTactic (← getEnv) tacName) then
|
|
throwErrorAt tac "'{tacName}' is not a tactic"
|
|
|
|
modifyEnv (tacticDocExtExt.addEntry · (tacName, docs.getDocString))
|
|
pure ()
|
|
| _ => throwError "Malformed tactic extension command"
|
|
|
|
@[builtin_command_elab «register_tactic_tag»] def elabRegisterTacticTag : CommandElab
|
|
| `(«register_tactic_tag»|$[$doc:docComment]? register_tactic_tag $tag:ident $user:str) => do
|
|
let docstring ← doc.mapM getDocStringText
|
|
modifyEnv (knownTacticTagExt.addEntry · (tag.getId, user.getString, docstring))
|
|
| _ => throwError "Malformed 'register_tactic_tag' command"
|
|
|
|
/--
|
|
Gets the first string token in a parser description. For example, for a declaration like
|
|
`syntax "squish " term " with " term : tactic`, it returns `some "squish "`, and for a declaration
|
|
like `syntax tactic " <;;;> " tactic : tactic`, it returns `some " <;;;> "`.
|
|
|
|
Returns `none` for syntax declarations that don't contain a string constant.
|
|
-/
|
|
private partial def getFirstTk (e : Expr) : MetaM (Option String) := do
|
|
match (← Meta.whnf e).getAppFnArgs with
|
|
| (``ParserDescr.node, #[_, _, p]) => getFirstTk p
|
|
| (``ParserDescr.trailingNode, #[_, _, _, p]) => getFirstTk p
|
|
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "withPosition")), p]) => getFirstTk p
|
|
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "atomic")), p]) => getFirstTk p
|
|
| (``ParserDescr.binary, #[.app _ (.lit (.strVal "andthen")), p, _]) => getFirstTk p
|
|
| (``ParserDescr.nonReservedSymbol, #[.lit (.strVal tk), _]) => pure (some tk)
|
|
| (``ParserDescr.symbol, #[.lit (.strVal tk)]) => pure (some tk)
|
|
| (``Parser.withAntiquot, #[_, p]) => getFirstTk p
|
|
| (``Parser.leadingNode, #[_, _, p]) => getFirstTk p
|
|
| (``HAndThen.hAndThen, #[_, _, _, _, p1, p2]) =>
|
|
if let some tk ← getFirstTk p1 then pure (some tk)
|
|
else getFirstTk (.app p2 (.const ``Unit.unit []))
|
|
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) => pure (some tk)
|
|
| (``Parser.symbol, #[.lit (.strVal tk)]) => pure (some tk)
|
|
| _ => pure none
|
|
|
|
|
|
/--
|
|
Creates some `MessageData` for a parser name.
|
|
|
|
If the parser name maps to a description with an
|
|
identifiable leading token, then that token is shown. Otherwise, the underlying name is shown
|
|
without an `@`. The name includes metadata that makes infoview hovers and the like work. This
|
|
only works for global constants, as the local context is not included.
|
|
-/
|
|
private def showParserName (n : Name) : MetaM MessageData := do
|
|
let env ← getEnv
|
|
let params :=
|
|
env.constants.find?' n |>.map (·.levelParams.map Level.param) |>.getD []
|
|
let tok ←
|
|
if let some descr := env.find? n |>.bind (·.value?) then
|
|
if let some tk ← getFirstTk descr then
|
|
pure <| Std.Format.text tk.trim
|
|
else pure <| format n
|
|
else pure <| format n
|
|
pure <| .ofFormatWithInfos {
|
|
fmt := "'" ++ .tag 0 tok ++ "'",
|
|
infos :=
|
|
.fromList [(0, .ofTermInfo {
|
|
lctx := .empty,
|
|
expr := .const n params,
|
|
stx := .ident .none (toString n).toSubstring n [.decl n []],
|
|
elaborator := `Delab,
|
|
expectedType? := none
|
|
})] _
|
|
}
|
|
|
|
|
|
/--
|
|
Displays all available tactic tags, with documentation.
|
|
-/
|
|
@[builtin_command_elab printTacTags] def elabPrintTacTags : CommandElab := fun _stx => do
|
|
let all :=
|
|
tacticTagExt.toEnvExtension.getState (← getEnv)
|
|
|>.importedEntries |>.push (tacticTagExt.exportEntriesFn (tacticTagExt.getState (← getEnv)))
|
|
let mut mapping : NameMap NameSet := {}
|
|
for arr in all do
|
|
for (tac, tag) in arr do
|
|
mapping := mapping.insert tag (mapping.findD tag {} |>.insert tac)
|
|
|
|
let showDocs : Option String → MessageData
|
|
| none => .nil
|
|
| some d => Format.line ++ MessageData.joinSep ((d.splitOn "\n").map toMessageData) Format.line
|
|
|
|
let showTactics (tag : Name) : MetaM MessageData := do
|
|
match mapping.find? tag with
|
|
| none => pure .nil
|
|
| some tacs =>
|
|
if tacs.isEmpty then pure .nil
|
|
else
|
|
let tacs := tacs.toArray.qsort (·.toString < ·.toString) |>.toList
|
|
pure (Format.line ++ MessageData.joinSep (← tacs.mapM showParserName) ", ")
|
|
|
|
let tagDescrs ← liftTermElabM <| (← allTagsWithInfo).mapM fun (name, userName, docs) => do
|
|
pure <| m!"• " ++
|
|
MessageData.nestD (m!"'{name}'" ++
|
|
(if name.toString != userName then m!" — \"{userName}\"" else MessageData.nil) ++
|
|
showDocs docs ++
|
|
(← showTactics name))
|
|
|
|
let tagList : MessageData :=
|
|
m!"Available tags: {MessageData.nestD (Format.line ++ .joinSep tagDescrs Format.line)}"
|
|
|
|
logInfo tagList
|
|
|
|
/--
|
|
The information needed to display all documentation for a tactic.
|
|
-/
|
|
structure TacticDoc where
|
|
/-- The name of the canonical parser for the tactic -/
|
|
internalName : Name
|
|
/-- The user-facing name to display (typically the first keyword token) -/
|
|
userName : String
|
|
/-- The tags that have been applied to the tactic -/
|
|
tags : NameSet
|
|
/-- The docstring for the tactic -/
|
|
docString : Option String
|
|
/-- Any docstring extensions that have been specified -/
|
|
extensionDocs : Array String
|
|
|
|
def allTacticDocs : MetaM (Array TacticDoc) := do
|
|
let env ← getEnv
|
|
let all :=
|
|
tacticTagExt.toEnvExtension.getState (← getEnv)
|
|
|>.importedEntries |>.push (tacticTagExt.exportEntriesFn (tacticTagExt.getState (← getEnv)))
|
|
let mut tacTags : NameMap NameSet := {}
|
|
for arr in all do
|
|
for (tac, tag) in arr do
|
|
tacTags := tacTags.insert tac (tacTags.findD tac {} |>.insert tag)
|
|
|
|
let mut docs := #[]
|
|
|
|
let some tactics := (Lean.Parser.parserExtension.getState env).categories.find? `tactic
|
|
| return #[]
|
|
for (tac, _) in tactics.kinds do
|
|
-- Skip noncanonical tactics
|
|
if let some _ := alternativeOfTactic env tac then continue
|
|
let userName : String ←
|
|
if let some descr := env.find? tac |>.bind (·.value?) then
|
|
if let some tk ← getFirstTk descr then
|
|
pure tk.trim
|
|
else pure tac.toString
|
|
else pure tac.toString
|
|
|
|
docs := docs.push {
|
|
internalName := tac,
|
|
userName := userName,
|
|
tags := tacTags.findD tac {},
|
|
docString := ← findDocString? env tac,
|
|
extensionDocs := getTacticExtensions env tac
|
|
}
|
|
return docs
|