lean4-htt/src/Lean/Widget/UserWidget.lean
Sebastian Ullrich 719765ec5c
feat: overhaul meta system (#10362)
This PR refines and clarifies the `meta` phase distinction in the module
system.

* `meta import A` without `public` now has the clarified meaning of
"enable compile-time evaluation of declarations in or above `A` in the
current module, but not downstream". This is now checked statically by
enforcing that public meta defs, which therefore may be referenced from
outside, can only use public meta imports, and that global evaluating
attributes such as `@[term_parser]` can only be applied to public meta
defs.
* `meta def`s may no longer reference non-meta defs even when in the
same module. This clarifies the meta distinction as well as improves
locality of (new) error messages.
* parser references in `syntax` are now also properly tracked as meta
references.
* A `meta import` of an `import` now properly loads only the `.ir` of
the nested module for the purposes of execution instead of also making
its declarations available for general elaboration.
* `initialize` is now no longer being run on import under the module
system, which is now covered by `meta initialize`.
2025-09-17 21:04:29 +00:00

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

/-
Copyright (c) 2022 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: E.W.Ayers, Wojciech Nawrocki
-/
module
prelude
public import Lean.Elab.Eval
public import Lean.Server.Rpc.RequestHandling
public import Lean.Widget.Types
meta import Lean.Parser.Term
meta import Lean.Elab.Command
public section
namespace Lean.Widget
open Meta Elab
private unsafe def evalModuleUnsafe (e : Expr) : MetaM Module :=
evalExpr' Module ``Module e
@[implemented_by evalModuleUnsafe]
opaque evalModule (e : Expr) : MetaM Module
private unsafe def evalWidgetInstanceUnsafe (e : Expr) : MetaM WidgetInstance :=
evalExpr' WidgetInstance ``WidgetInstance e
@[implemented_by evalWidgetInstanceUnsafe]
opaque evalWidgetInstance (e : Expr) : MetaM WidgetInstance
/-! ## Storage of widget modules -/
class ToModule (α : Type u) where
toModule : α → Module
instance : ToModule Module := ⟨id⟩
private builtin_initialize builtinModulesRef : IO.Ref (Std.TreeMap UInt64 (Name × Module)) ←
IO.mkRef ∅
def addBuiltinModule (id : Name) (m : Module) : IO Unit :=
builtinModulesRef.modify (·.insert m.javascriptHash (id, m))
/-- Every constant `c : α` marked with `@[widget_module]` is registered here.
The registry maps `hash (toModule c).javascript` to ``(`c, `(@toModule α inst c))``
where `inst : ToModule α` is synthesized during registration time
and stored thereafter. -/
private abbrev ModuleRegistry := SimplePersistentEnvExtension
(UInt64 × Name × Expr)
(Std.TreeMap UInt64 (Name × Expr))
private builtin_initialize moduleRegistry : ModuleRegistry ←
registerSimplePersistentEnvExtension {
addImportedFn := fun xss => xss.foldl (Array.foldl (fun s n => s.insert n.1 n.2)) ∅
addEntryFn := fun s n => s.insert n.1 n.2
toArrayFn := fun es => es.toArray
}
/-- Registers a widget module. Its type must implement `Lean.Widget.ToModule`. -/
builtin_initialize widgetModuleAttrImpl : AttributeImpl ←
let mkAttr (builtin : Bool) (name : Name) := do
let impl := {
name
descr := (if builtin then "(builtin) " else "") ++
"Registers a widget module. Its type must implement Lean.Widget.ToModule."
applicationTime := .afterCompilation
add := fun decl stx kind => Prod.fst <$> MetaM.run do
Attribute.Builtin.ensureNoArgs stx
unless kind == AttributeKind.global do throwAttrMustBeGlobal name kind
let e ← mkAppM ``ToModule.toModule #[.const decl []]
let mod ← evalModule e
let env ← getEnv
unless builtin do -- don't warn on collision between previous and current stage
if let some _ := (← builtinModulesRef.get).get? mod.javascriptHash then
logWarning m!"A builtin widget module with the same hash(JS source code) was already registered."
if let some (n, _) := moduleRegistry.getState env |>.get? mod.javascriptHash then
logWarning m!"A widget module with the same hash(JS source code) was already registered at {.ofConstName n true}."
let env ← getEnv
if builtin then
let h := mkConst decl
declareBuiltin decl <| mkApp2 (mkConst ``addBuiltinModule) (toExpr decl) h
else
setEnv <| moduleRegistry.addEntry env (mod.javascriptHash, decl, e)
}
registerBuiltinAttribute impl
return impl
/- We declare the `[builtin_widget_module]` and `[widget_module]` attributes
and bind the latter's implementation
(used for creating the obsolete `[widget]` alias below). -/
let _ ← mkAttr true `builtin_widget_module
mkAttr false `widget_module
/-! ## Retrieval of widget modules -/
structure GetWidgetSourceParams where
/-- Hash of the JS module to retrieve. -/
hash : UInt64
pos : Lean.Lsp.Position
deriving ToJson, FromJson
structure WidgetSource where
/-- Sourcetext of the JS module to run. -/
sourcetext : String
deriving Inhabited, ToJson, FromJson
open Server RequestM in
def getWidgetSource (args : GetWidgetSourceParams) : RequestM (RequestTask WidgetSource) := do
if let some (_, m) := (← builtinModulesRef.get).get? args.hash then
return .pure { sourcetext := m.javascript }
let doc ← readDoc
let pos := doc.meta.text.lspPosToUtf8Pos args.pos
let notFound := throwThe RequestError ⟨.invalidParams, s!"No widget module with hash {args.hash} registered"⟩
withWaitFindSnap doc (notFoundX := notFound)
(fun s => s.endPos >= pos || (moduleRegistry.getState s.env).contains args.hash)
fun snap => do
if let some (_, e) := moduleRegistry.getState snap.env |>.get? args.hash then
runTermElabM snap do
return { sourcetext := (← evalModule e).javascript }
else
notFound
builtin_initialize
Server.registerBuiltinRpcProcedure ``getWidgetSource _ _ getWidgetSource
/-! ## Storage of panel widget instances -/
inductive PanelWidgetsExtEntry where
| «global» (n : Name)
| «local» (wi : WidgetInstance)
/-- Keeps track of panel widget instances that should be displayed.
Instances can be registered for display global
(i.e., persisted in `.olean`s) and locally (not persisted)
For globally displayed widgets
we cannot store a `WidgetInstance` in the persistent state
because it contains a `StateM` closure.
Instead, we add a global constant of type `WidgetInstance`
to the environment, and store its name in the extension.
For locally displayed ones, we just store a `WidgetInstance`
in the extension directly.
This is okay because it is never persisted.
The (persistent) entries are then of the form `(h, n)`
where `h` is a hash stored in the `moduleRegistry`
and `n` is the name of a `WidgetInstance` global constant.
The extension state maps each `h` as above
to a list of entries that can be either global or local ones.
Each element of the state indicates that the widget module `h`
should be displayed with the given `WidgetInstance` as its arguments.
This is similar to a parametric attribute, except that:
- parametric attributes map at most one parameter to one tagged declaration,
whereas we may display multiple instances of a single widget module; and
- parametric attributes use the same type for local and global entries,
which we cannot do owing to the closure. -/
private abbrev PanelWidgetsExt := SimpleScopedEnvExtension
(UInt64 × Name)
(Std.TreeMap UInt64 (List PanelWidgetsExtEntry))
private builtin_initialize panelWidgetsExt : PanelWidgetsExt ←
registerSimpleScopedEnvExtension {
addEntry := fun s (h, n) => s.insert h (.global n :: s.getD h [])
initial := .empty
}
def evalPanelWidgets : MetaM (Array WidgetInstance) := do
let mut ret := #[]
for (_, l) in panelWidgetsExt.getState (← getEnv) do
for e in l do
match e with
| .global n =>
let wi ← evalWidgetInstance (mkConst n)
ret := ret.push wi
| .local wi => ret := ret.push wi
return ret
def addPanelWidgetGlobal [Monad m] [MonadEnv m] [MonadResolveName m] (h : UInt64) (n : Name) : m Unit := do
panelWidgetsExt.add (h, n)
def addPanelWidgetScoped [Monad m] [MonadEnv m] [MonadResolveName m] (h : UInt64) (n : Name) : m Unit := do
panelWidgetsExt.add (h, n) .scoped
def addPanelWidgetLocal [Monad m] [MonadEnv m] (wi : WidgetInstance) : m Unit := do
modifyEnv fun env => panelWidgetsExt.modifyState env fun s =>
s.insert wi.javascriptHash (.local wi :: s.getD wi.javascriptHash [])
def erasePanelWidget [Monad m] [MonadEnv m] (h : UInt64) : m Unit := do
modifyEnv fun env => panelWidgetsExt.modifyState env fun st => st.erase h
/-- Construct a widget instance by finding a widget module
in the current environment.
`hash` must be `hash (toModule c).javascript`
where `c` is some global constant annotated with `@[widget_module]`,
or the name of a builtin widget module. -/
def WidgetInstance.ofHash (hash : UInt64) (props : StateM Server.RpcObjectStore Json) :
CoreM WidgetInstance := do
let env ← getEnv
let builtins ← builtinModulesRef.get
let some id :=
(builtins.get? hash |>.map (·.1)) <|> (moduleRegistry.getState env |>.get? hash |>.map (·.1))
| throwError s!"No widget module with hash {hash} registered"
return { id, javascriptHash := hash, props }
/-- Save the data of a panel widget which will be displayed whenever the text cursor is on `stx`.
`hash` must be as in `WidgetInstance.ofHash`.
For panel widgets, the Lean infoview appends additional fields to the `props` object:
see https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/userWidget.tsx#L145. -/
def savePanelWidgetInfo (hash : UInt64) (props : StateM Server.RpcObjectStore Json) (stx : Syntax) :
CoreM Unit := do
let wi ← WidgetInstance.ofHash hash props
pushInfoLeaf <| .ofUserWidgetInfo { wi with stx }
/-! ## Deprecated definitions -/
/-- Use this structure and the `@[widget]` attribute to define your own widgets.
```lean
@[widget]
def rubiks : UserWidgetDefinition :=
{ name := "Rubiks cube app"
javascript := include_str ...
}
```
-/
structure UserWidgetDefinition where
/-- Pretty name of user widget to display to the user. -/
name : String
/-- An ESmodule that exports a react component to render. -/
javascript: String
deriving Inhabited, ToJson, FromJson
instance : ToModule UserWidgetDefinition where
toModule uwd := { uwd with }
private unsafe def evalUserWidgetDefinitionUnsafe [Monad m] [MonadEnv m] [MonadOptions m] [MonadError m]
(id : Name) : m UserWidgetDefinition := do
ofExcept <| (← getEnv).evalConstCheck UserWidgetDefinition (← getOptions) ``UserWidgetDefinition id
@[implemented_by evalUserWidgetDefinitionUnsafe]
opaque evalUserWidgetDefinition [Monad m] [MonadEnv m] [MonadOptions m] [MonadError m]
(id : Name) : m UserWidgetDefinition
/-! ## Retrieving panel widget instances -/
/-- Retrieve all the `UserWidgetInfo`s that intersect a given line. -/
def widgetInfosAt? (text : FileMap) (t : InfoTree) (hoverLine : Nat) : List UserWidgetInfo :=
t.deepestNodes fun
| _ctx, i@(Info.ofUserWidgetInfo wi), _cs => do
if let (some pos, some tailPos) := (i.pos?, i.tailPos?) then
-- Does the widget's line range contain `hoverLine`?
guard <| (text.utf8PosToLspPos pos).line ≤ hoverLine ∧ hoverLine ≤ (text.utf8PosToLspPos tailPos).line
return wi
else
failure
| _, _, _ => none
structure PanelWidgetInstance extends WidgetInstance where
/-- The syntactic span in the Lean file at which the panel widget is displayed. -/
range? : Option Lsp.Range := none
/-- When present, the infoview will wrap the widget
in `<details><summary>{name}</summary>...</details>`.
This functionality is deprecated
but retained for backwards compatibility
with `UserWidgetDefinition`. -/
name? : Option String := none
deriving Server.RpcEncodable
/-- Output of `getWidgets` RPC.-/
structure GetWidgetsResponse where
widgets : Array PanelWidgetInstance
deriving Server.RpcEncodable
open Lean Server RequestM in
/-- Get the panel widgets present around a particular position. -/
def getWidgets (pos : Lean.Lsp.Position) : RequestM (RequestTask GetWidgetsResponse) := do
let doc ← readDoc
let filemap := doc.meta.text
mapTaskCostly (findInfoTreeAtPos doc (filemap.lspPosToUtf8Pos pos) (includeStop := true)) fun
| some infoTree@(.context (.commandCtx cc) _) =>
ContextInfo.runMetaM { cc with } {} do
let env ← getEnv
/- Panels from the environment. -/
let ws' ← evalPanelWidgets
let ws' : Array PanelWidgetInstance ← ws'.mapM fun wi => do
-- Check if the definition uses the deprecated `UserWidgetDefinition`
-- on a best-effort basis.
-- If it does, also send the `name` field.
let name? ← env.find? wi.id
|>.filter (·.type.isConstOf ``UserWidgetDefinition)
|>.mapM fun _ => do
let uwd ← evalUserWidgetDefinition wi.id
return uwd.name
return { wi with name? }
/- Panels from the infotree. -/
let ws := widgetInfosAt? filemap infoTree pos.line
let ws : Array PanelWidgetInstance ← ws.toArray.mapM fun (wi : UserWidgetInfo) => do
let name? ← env.find? wi.id
|>.filter (·.type.isConstOf ``UserWidgetDefinition)
|>.mapM fun _ => do
let uwd ← evalUserWidgetDefinition wi.id
return uwd.name
return { wi with range? := String.Range.toLspRange filemap <$> Syntax.getRange? wi.stx, name? }
return { widgets := ws' ++ ws }
| _ => return ⟨∅⟩
builtin_initialize
Server.registerBuiltinRpcProcedure ``getWidgets _ _ getWidgets
end Lean.Widget