Skip to content

Commit 3d1277b

Browse files
committed
feat: reusable rpc refs
1 parent 8195f70 commit 3d1277b

File tree

6 files changed

+142
-58
lines changed

6 files changed

+142
-58
lines changed

src/Lean/Server/FileWorker/RequestHandling.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def getInteractiveTermGoal (p : Lsp.PlainTermGoalParams)
345345
let goal ← ci.runMetaM lctx' do
346346
Widget.goalToInteractive (← Meta.mkFreshExprMVar ty).mvarId!
347347
let range := if let some r := i.range? then r.toLspRange text else ⟨p.position, p.position⟩
348-
return some { goal with range, term := ⟨ti⟩ }
348+
return some { goal with range, term := ← WithRpcRef.mkReusable ti }
349349

350350
def handlePlainTermGoal (p : PlainTermGoalParams)
351351
: RequestM (RequestTask (Option PlainTermGoal)) := do

src/Lean/Server/FileWorker/WidgetRequests.lean

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ builtin_initialize
2828
`Lean.Widget.InteractiveDiagnostics.msgToInteractive
2929
MsgToInteractive
3030
(TaggedText MsgEmbed)
31-
fun⟨m⟩, i⟩ => RequestM.pureTask do msgToInteractive m i (hasWidgets := true)
31+
funm, i⟩ => RequestM.pureTask do msgToInteractive m.val i (hasWidgets := true)
3232

3333
/-- The information that the infoview uses to render a popup
3434
for when the user hovers over an expression.
@@ -48,7 +48,8 @@ The intended usage of this is for the infoview to pass the `InfoWithCtx` which
4848
was stored for a particular `SubexprInfo` tag in a `TaggedText` generated with `ppExprTagged`.
4949
-/
5050
def makePopup : WithRpcRef InfoWithCtx → RequestM (RequestTask InfoPopup)
51-
| ⟨i⟩ => RequestM.pureTask do
51+
| i => RequestM.pureTask do
52+
let i := i.val
5253
i.ctx.runMetaM i.info.lctx do
5354
let type? ← match (← i.info.type?) with
5455
| some type => some <$> ppExprTagged type
@@ -124,7 +125,8 @@ builtin_initialize
124125
`Lean.Widget.getGoToLocation
125126
GetGoToLocationParams
126127
(Array Lsp.LocationLink)
127-
fun ⟨kind, ⟨i⟩⟩ => RequestM.pureTask do
128+
fun ⟨kind, i⟩ => RequestM.pureTask do
129+
let i := i.val
128130
let rc ← read
129131
let ls ← FileWorker.locationLinksOfInfo kind i
130132
if !ls.isEmpty then return ls
@@ -138,9 +140,9 @@ builtin_initialize
138140
def lazyTraceChildrenToInteractive (children : WithRpcRef LazyTraceChildren) :
139141
RequestM (RequestTask (Array (TaggedText MsgEmbed))) :=
140142
RequestM.pureTask do
141-
let ⟨indent, children⟩ := children
142-
children.mapM fun child =>
143-
msgToInteractive child (hasWidgets := true) (indent := indent)
143+
let ⟨indent, children⟩ := children.val
144+
children.mapM fun child =>
145+
msgToInteractive child.val (hasWidgets := true) (indent := indent)
144146

145147
builtin_initialize registerBuiltinRpcProcedure ``lazyTraceChildrenToInteractive _ _ lazyTraceChildrenToInteractive
146148

src/Lean/Server/Rpc/Basic.lean

Lines changed: 120 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@ first connect to the session using `$/lean/rpc/connect`. -/
1919

2020
namespace Lean.Lsp
2121

22-
/-- An object which RPC clients can refer to without marshalling. -/
22+
/--
23+
An object which RPC clients can refer to without marshalling.
24+
25+
The language server may serve the same `RpcRef` multiple times and maintains a reference count
26+
to track how often it has served the reference.
27+
If clients want to release the object associated with an `RpcRef`,
28+
they must release the reference as often as they have received it from the server.
29+
-/
2330
structure RpcRef where
2431
/- NOTE(WN): It is important for this to be a single-field structure
2532
in order to deserialize as an `Object` on the JS side. -/
2633
p : USize
27-
deriving BEq, Hashable, FromJson, ToJson
34+
deriving Inhabited, BEq, Hashable, FromJson, ToJson
2835

2936
instance : ToString RpcRef where
3037
toString r := toString r.p
@@ -33,36 +40,123 @@ end Lean.Lsp
3340

3441
namespace Lean.Server
3542

36-
structure RpcObjectStore : Type where
37-
/-- Objects that are being kept alive for the RPC client, together with their type names,
38-
mapped to by their RPC reference.
43+
/--
44+
Marks values to be encoded as opaque references in RPC packets.
45+
Two identical `WithRpcRef`s with `id? = some id` and the same `id` will yield the same `RpcRef`.
46+
47+
See also the docstring for `RpcEncodable`.
48+
-/
49+
structure WithRpcRef (α : Type u) where
50+
private mk' ::
51+
val : α
52+
id? : Option USize
53+
deriving Inhabited
54+
55+
/--
56+
Creates a non-reusable `WithRpcRef`.
57+
Every time `val` is served to the client, it generates a new `RpcRef`.
58+
-/
59+
def WithRpcRef.mkNonReusable (val : α) : WithRpcRef α where
60+
val
61+
id? := none
3962

40-
Note that we may currently have multiple references to the same object. It is only disposed
41-
of once all of those are gone. This simplifies the client a bit as it can drop every reference
42-
received separately. -/
43-
aliveRefs : PersistentHashMap Lsp.RpcRef Dynamic := {}
44-
/-- Value to use for the next `RpcRef`. It is monotonically increasing to avoid any possible
45-
bugs resulting from its reuse. -/
46-
nextRef : USize := 0
63+
/--
64+
Alias for `WithRpcRef.mkNonReusable`.
65+
-/
66+
def WithRpcRef.mk (val : α) : WithRpcRef α := .mkNonReusable val
4767

48-
def rpcStoreRef (any : Dynamic) : StateM RpcObjectStore Lsp.RpcRef := do
49-
let st ← get
50-
set { st with
51-
aliveRefs := st.aliveRefs.insert ⟨st.nextRef⟩ any
52-
nextRef := st.nextRef + 1
68+
builtin_initialize freshWithRpcRefId : IO.Ref USize ← IO.mkRef 0
69+
70+
/--
71+
Creates an `WithRpcRef` instance with a unique `id?`.
72+
Two identical `WithRpcRef`s with `id? = some id` and the same `id` will yield the same `RpcRef`.
73+
Hence, storing a `WithRpcRef` produced by `mkReusable` and serving it to the client twice will also
74+
yield the same `RpcRef` twice, allowing clients to reuse their associated UI state across
75+
RPC requests.
76+
-/
77+
def WithRpcRef.mkReusable (val : α) : BaseIO (WithRpcRef α) := do
78+
let id ← freshWithRpcRefId.modifyGet fun id => (id, id + 1)
79+
return {
80+
val,
81+
id? := some id
5382
}
54-
return ⟨st.nextRef⟩
5583

56-
def rpcGetRef (r : Lsp.RpcRef) : ReaderT RpcObjectStore Id (Option Dynamic) :=
57-
return (← read).aliveRefs.find? r
84+
structure ReferencedObject where
85+
obj : Dynamic
86+
id? : Option USize
87+
rc : Nat
88+
89+
structure RpcObjectStore : Type where
90+
/--
91+
Objects that are being kept alive for the RPC client, together with their type names,
92+
mapped to by their RPC reference.
93+
-/
94+
aliveRefs : PersistentHashMap Lsp.RpcRef ReferencedObject := {}
95+
/--
96+
Unique `RpcRef` for the ID of an object that is being referenced through RPC.
97+
We store this mapping so that we can reuse `RpcRef`s for the same object.
98+
Reusing `RpcRef`s is helpful because it enables clients to reuse their UI state.
99+
-/
100+
refsById : PersistentHashMap USize Lsp.RpcRef := {}
101+
/--
102+
Value to use for the next fresh `RpcRef`. It is monotonically increasing to avoid any possible
103+
bugs resulting from its reuse.
104+
-/
105+
nextRef : USize := 0
106+
107+
def rpcStoreRef [TypeName α] (obj : WithRpcRef α) : StateM RpcObjectStore Lsp.RpcRef := do
108+
let st ← get
109+
let reusableRef? : Option Lsp.RpcRef := do st.refsById.find? (← obj.id?)
110+
match reusableRef? with
111+
| some ref =>
112+
-- Reuse `RpcRef` for this `obj` so that clients can reuse their UI state for it.
113+
-- We maintain a reference count so that we only free `obj` when the client has released
114+
-- all of its instances of the `RpcRef` for `obj`.
115+
let some referencedObj := st.aliveRefs.find? ref
116+
| return panic! "Found object ID in `refsById` but not in `aliveRefs`."
117+
let referencedObj := { referencedObj with rc := referencedObj.rc + 1 }
118+
set { st with aliveRefs := st.aliveRefs.insert ref referencedObj }
119+
return ref
120+
| none =>
121+
let ref : Lsp.RpcRef := ⟨st.nextRef⟩
122+
set { st with
123+
aliveRefs :=
124+
st.aliveRefs.insert ref ⟨.mk obj.val, obj.id?, 1
125+
refsById :=
126+
match obj.id? with
127+
| none => st.refsById
128+
| some id => st.refsById.insert id ref
129+
nextRef :=
130+
st.nextRef + 1
131+
}
132+
return ref
133+
134+
def rpcGetRef (α) [TypeName α] (r : Lsp.RpcRef)
135+
: ReaderT RpcObjectStore (ExceptT String Id) (WithRpcRef α) := do
136+
let some referencedObj := (← read).aliveRefs.find? r
137+
| throw s!"RPC reference '{r}' is not valid"
138+
let some val := referencedObj.obj.get? α
139+
| throw <| s!"RPC call type mismatch in reference '{r}'\nexpected '{TypeName.typeName α}', " ++
140+
s!"got '{referencedObj.obj.typeName}'"
141+
return { val, id? := referencedObj.id? }
58142

59143
def rpcReleaseRef (r : Lsp.RpcRef) : StateM RpcObjectStore Bool := do
60144
let st ← get
61-
if st.aliveRefs.contains r then
62-
set { st with aliveRefs := st.aliveRefs.erase r }
63-
return true
145+
let some referencedObj := st.aliveRefs.find? r
146+
| return false
147+
let referencedObj := { referencedObj with rc := referencedObj.rc - 1 }
148+
if referencedObj.rc == 0 then
149+
set { st with
150+
aliveRefs :=
151+
st.aliveRefs.erase r
152+
refsById :=
153+
match referencedObj.id? with
154+
| none => st.refsById
155+
| some id => st.refsById.erase id
156+
}
64157
else
65-
return false
158+
set { st with aliveRefs := st.aliveRefs.insert r referencedObj }
159+
return true
66160

67161
/-- `RpcEncodable α` means that `α` can be deserialized from and serialized into JSON
68162
for the purpose of receiving arguments to and sending return values from
@@ -121,26 +215,11 @@ instance [RpcEncodable α] : RpcEncodable (StateM RpcObjectStore α) where
121215
let a : α ← rpcDecode j
122216
return return a
123217

124-
/-- Marks values to be encoded as opaque references in RPC packets.
125-
126-
See the docstring for `RpcEncodable`. -/
127-
structure WithRpcRef (α : Type u) where
128-
val : α
129-
deriving Inhabited
130-
131218
instance [TypeName α] : RpcEncodable (WithRpcRef α) :=
132219
{ rpcEncode, rpcDecode }
133220
where
134221
-- separate definitions to prevent inlining
135-
rpcEncode r := toJson <$> rpcStoreRef (.mk r.val)
136-
rpcDecode j := do
137-
let r ← fromJson? j
138-
match (← rpcGetRef r) with
139-
| none => throw s!"RPC reference '{r}' is not valid"
140-
| some any =>
141-
if let some obj := any.get? α then
142-
return ⟨obj⟩
143-
else
144-
throw s!"RPC call type mismatch in reference '{r}'\nexpected '{TypeName.typeName α}', got '{any.typeName}'"
222+
rpcEncode r := toJson <$> rpcStoreRef r
223+
rpcDecode j := do rpcGetRef α (← fromJson? j)
145224

146225
end Lean.Server

src/Lean/Widget/InteractiveCode.lean

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,19 @@ def SubexprInfo.withDiffTag (tag : DiffTag) (c : SubexprInfo) : SubexprInfo :=
5959

6060
/-- Tags pretty-printed code with infos from the delaborator. -/
6161
partial def tagCodeInfos (ctx : Elab.ContextInfo) (infos : SubExpr.PosMap Elab.Info) (tt : TaggedText (Nat × Nat))
62-
: CodeWithInfos :=
62+
: BaseIO CodeWithInfos :=
6363
go tt
6464
where
65-
go (tt : TaggedText (Nat × Nat)) :=
66-
tt.rewrite fun (n, _) subTt =>
65+
go (tt : TaggedText (Nat × Nat)) : BaseIO (TaggedText SubexprInfo) :=
66+
tt.rewriteM fun (n, _) subTt => do
6767
match infos.find? n with
6868
| none => go subTt
6969
| some i =>
7070
let t : SubexprInfo := {
71-
info := WithRpcRef.mk { ctx, info := i, children := .empty }
71+
info := WithRpcRef.mkReusable { ctx, info := i, children := .empty }
7272
subexprPos := n
7373
}
74-
TaggedText.tag t (go subTt)
74+
return TaggedText.tag t (go subTt)
7575

7676
open PrettyPrinter Delaborator in
7777
/--
@@ -93,6 +93,6 @@ def ppExprTagged (e : Expr) (delab : Delab := Delaborator.delab) : MetaM CodeWit
9393
fileMap := default
9494
ngen := (← getNGen)
9595
}
96-
return tagCodeInfos ctx infos tt
96+
tagCodeInfos ctx infos tt
9797

9898
end Lean.Widget

src/Lean/Widget/InteractiveDiagnostic.lean

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ partial def msgToInteractive (msgData : MessageData) (hasWidgets : Bool) (indent
192192
let rec fmtToTT (fmt : Format) (indent : Nat) : IO (TaggedText MsgEmbed) :=
193193
(TaggedText.prettyTagged fmt indent).rewriteM fun (n, col) tt =>
194194
match embeds[n]! with
195-
| .code ctx infos =>
196-
return .tag (.expr (tagCodeInfos ctx infos tt)) default
195+
| .code ctx infos => do
196+
return .tag (.expr (tagCodeInfos ctx infos tt)) default
197197
| .goal ctx lctx g =>
198198
ctx.runMetaM lctx do
199199
return .tag (.goal (← goalToInteractive g)) default
@@ -205,7 +205,10 @@ partial def msgToInteractive (msgData : MessageData) (hasWidgets : Bool) (indent
205205
let col := indent + col
206206
let children ←
207207
match children with
208-
| .lazy children => pure <| .lazy ⟨{indent := col+2, children := children.map .mk}⟩
208+
| .lazy children => pure <| .lazy <| ← WithRpcRef.mkReusable {
209+
indent := col+2
210+
children := ← children.mapM (WithRpcRef.mkReusable ·)
211+
}
209212
| .strict children => pure <| .strict (← children.mapM (fmtToTT · (col+2)))
210213
return .tag (.trace indent cls (← fmtToTT msg col) collapsed children) default
211214
| .ignoreTags => return .text tt.stripTags

src/Lean/Widget/InteractiveGoal.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def goalToInteractive (mvarId : MVarId) : MetaM InteractiveGoal := do
192192
return {
193193
hyps
194194
type := goalFmt
195-
ctx := {← Elab.CommandContextInfo.save with }
195+
ctx := ← WithRpcRef.mkReusable {← Elab.CommandContextInfo.save with }
196196
userName?
197197
goalPrefix := getGoalPrefix mvarDecl
198198
mvarId

0 commit comments

Comments
 (0)