This PR extends `Std.Channel` to provide a full sync and async API, as well as unbounded, zero sized and bounded channels. A few notes on the implementation: - the bounded channel is inspired by [Go channels on steroids](https://docs.google.com/document/d/1yIAYmbvL3JxOKOjuCyon7JhW4cSv1wy5hC0ApeGMV9s/pub) though currently doesn't do any of the lock-free optimizations - @mhuisi convinced me that having a non-closable channel may be a good idea as this alleviates the need for error handling which is very annoying when working with `Task`. This does complicate the API a little bit and I'm not quite sure whether this is a choice we want users to give. An alternative to this would be to just write `send!` that panics on sending to a closed channel (receiving from a closed channel is not an error), this is for example the behavior that golang goes with.
347 lines
13 KiB
Text
347 lines
13 KiB
Text
/-
|
||
Copyright (c) 2020 Marc Huisinga. All rights reserved.
|
||
Released under Apache 2.0 license as described in the file LICENSE.
|
||
|
||
Authors: Marc Huisinga, Wojciech Nawrocki
|
||
-/
|
||
prelude
|
||
import Init.System.IO
|
||
import Lean.Data.RBTree
|
||
import Lean.Data.Json
|
||
|
||
/-! Implementation of JSON-RPC 2.0 (https://www.jsonrpc.org/specification)
|
||
for use in the LSP server. -/
|
||
|
||
namespace Lean.JsonRpc
|
||
|
||
open Json
|
||
|
||
/-- In JSON-RPC, each request from the client editor to the language server comes with a
|
||
request id so that the corresponding response can be identified or cancelled. -/
|
||
inductive RequestID where
|
||
| str (s : String)
|
||
| num (n : JsonNumber)
|
||
| null
|
||
deriving Inhabited, BEq, Ord
|
||
|
||
instance : OfNat RequestID n := ⟨RequestID.num n⟩
|
||
|
||
instance : ToString RequestID where
|
||
toString
|
||
| RequestID.str s => s!"\"{s}\""
|
||
| RequestID.num n => toString n
|
||
| RequestID.null => "null"
|
||
|
||
/-- Error codes defined by
|
||
[JSON-RPC](https://www.jsonrpc.org/specification#error_object) and
|
||
[LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#errorCodes). -/
|
||
inductive ErrorCode where
|
||
/-- Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. -/
|
||
| parseError
|
||
/-- The JSON sent is not a valid Request object. -/
|
||
| invalidRequest
|
||
/-- The method does not exist / is not available. -/
|
||
| methodNotFound
|
||
/-- Invalid method parameter(s). -/
|
||
| invalidParams
|
||
/-- Internal JSON-RPC error. -/
|
||
| internalError
|
||
/-- Error code indicating that a server received a notification or
|
||
request before the server has received the `initialize` request. -/
|
||
| serverNotInitialized
|
||
| unknownErrorCode
|
||
-- LSP-specific codes below.
|
||
/-- The server detected that the content of a document got
|
||
modified outside normal conditions. A server should
|
||
NOT send this error code if it detects a content change
|
||
in it unprocessed messages. The result even computed
|
||
on an older state might still be useful for the client.
|
||
|
||
If a client decides that a result is not of any use anymore
|
||
the client should cancel the request. -/
|
||
| contentModified
|
||
/-- The client has canceled a request and a server as detected the cancel. -/
|
||
| requestCancelled
|
||
-- Lean-specific codes below.
|
||
| rpcNeedsReconnect
|
||
| workerExited
|
||
| workerCrashed
|
||
deriving Inhabited, BEq
|
||
|
||
instance : FromJson ErrorCode := ⟨fun
|
||
| num (-32700 : Int) => return ErrorCode.parseError
|
||
| num (-32600 : Int) => return ErrorCode.invalidRequest
|
||
| num (-32601 : Int) => return ErrorCode.methodNotFound
|
||
| num (-32602 : Int) => return ErrorCode.invalidParams
|
||
| num (-32603 : Int) => return ErrorCode.internalError
|
||
| num (-32002 : Int) => return ErrorCode.serverNotInitialized
|
||
| num (-32001 : Int) => return ErrorCode.unknownErrorCode
|
||
| num (-32801 : Int) => return ErrorCode.contentModified
|
||
| num (-32800 : Int) => return ErrorCode.requestCancelled
|
||
| num (-32900 : Int) => return ErrorCode.rpcNeedsReconnect
|
||
| num (-32901 : Int) => return ErrorCode.workerExited
|
||
| num (-32902 : Int) => return ErrorCode.workerCrashed
|
||
| _ => throw "expected error code"⟩
|
||
|
||
instance : ToJson ErrorCode := ⟨fun
|
||
| ErrorCode.parseError => (-32700 : Int)
|
||
| ErrorCode.invalidRequest => (-32600 : Int)
|
||
| ErrorCode.methodNotFound => (-32601 : Int)
|
||
| ErrorCode.invalidParams => (-32602 : Int)
|
||
| ErrorCode.internalError => (-32603 : Int)
|
||
| ErrorCode.serverNotInitialized => (-32002 : Int)
|
||
| ErrorCode.unknownErrorCode => (-32001 : Int)
|
||
| ErrorCode.contentModified => (-32801 : Int)
|
||
| ErrorCode.requestCancelled => (-32800 : Int)
|
||
| ErrorCode.rpcNeedsReconnect => (-32900 : Int)
|
||
| ErrorCode.workerExited => (-32901 : Int)
|
||
| ErrorCode.workerCrashed => (-32902 : Int)⟩
|
||
|
||
/-- A JSON-RPC message.
|
||
|
||
Uses separate constructors for notifications and errors because client and server
|
||
behavior is expected to be wildly different for both.
|
||
-/
|
||
inductive Message where
|
||
/-- A request message to describe a request between the client and the server. Every processed request must send a response back to the sender of the request. -/
|
||
| request (id : RequestID) (method : String) (params? : Option Structured)
|
||
/-- A notification message. A processed notification message must not send a response back. They work like events. -/
|
||
| notification (method : String) (params? : Option Structured)
|
||
/-- A Response Message sent as a result of a request. -/
|
||
| response (id : RequestID) (result : Json)
|
||
/-- A non-successful response. -/
|
||
| responseError (id : RequestID) (code : ErrorCode) (message : String) (data? : Option Json)
|
||
deriving Inhabited
|
||
|
||
def Batch := Array Message
|
||
|
||
/-- Generic version of `Message.request`.
|
||
|
||
A request message to describe a request between the client and the server. Every processed request must send a response back to the sender of the request.
|
||
|
||
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
|
||
- [JSON-RPC](https://www.jsonrpc.org/specification#request_object)
|
||
-/
|
||
structure Request (α : Type u) where
|
||
id : RequestID
|
||
method : String
|
||
param : α
|
||
deriving Inhabited, BEq
|
||
|
||
instance [ToJson α] : CoeOut (Request α) Message :=
|
||
⟨fun r => Message.request r.id r.method (toStructured? r.param).toOption⟩
|
||
|
||
/-- Generic version of `Message.notification`.
|
||
|
||
A notification message. A processed notification message must not send a response back. They work like events.
|
||
|
||
- [JSON-RPC](https://www.jsonrpc.org/specification#notification)
|
||
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage).
|
||
-/
|
||
structure Notification (α : Type u) where
|
||
method : String
|
||
param : α
|
||
deriving Inhabited, BEq
|
||
|
||
instance [ToJson α] : CoeOut (Notification α) Message :=
|
||
⟨fun r => Message.notification r.method (toStructured? r.param).toOption⟩
|
||
|
||
/-- Generic version of `Message.response`.
|
||
|
||
A Response Message sent as a result of a request. If a request doesn’t provide a
|
||
result value the receiver of a request still needs to return a response message
|
||
to conform to the JSON-RPC specification. The result property of the ResponseMessage
|
||
should be set to null in this case to signal a successful request.
|
||
|
||
References:
|
||
- [JSON-RPC](https://www.jsonrpc.org/specification#response_object)
|
||
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage)
|
||
-/
|
||
structure Response (α : Type u) where
|
||
id : RequestID
|
||
result : α
|
||
deriving Inhabited, BEq
|
||
|
||
instance [ToJson α] : CoeOut (Response α) Message :=
|
||
⟨fun r => Message.response r.id (toJson r.result)⟩
|
||
|
||
/-- Generic version of `Message.responseError`.
|
||
|
||
References:
|
||
- [JSON-RPC](https://www.jsonrpc.org/specification#error_object)
|
||
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseError).
|
||
-/
|
||
structure ResponseError (α : Type u) where
|
||
id : RequestID
|
||
code : ErrorCode
|
||
/-- A string providing a short description of the error. -/
|
||
message : String
|
||
/-- A primitive or structured value that contains additional
|
||
information about the error. Can be omitted. -/
|
||
data? : Option α := none
|
||
deriving Inhabited, BEq
|
||
|
||
instance [ToJson α] : CoeOut (ResponseError α) Message :=
|
||
⟨fun r => Message.responseError r.id r.code r.message (r.data?.map toJson)⟩
|
||
|
||
instance : CoeOut (ResponseError Unit) Message :=
|
||
⟨fun r => Message.responseError r.id r.code r.message none⟩
|
||
|
||
instance : Coe String RequestID := ⟨RequestID.str⟩
|
||
instance : Coe JsonNumber RequestID := ⟨RequestID.num⟩
|
||
|
||
private def RequestID.lt : RequestID → RequestID → Bool
|
||
| RequestID.str a, RequestID.str b => a < b
|
||
| RequestID.num a, RequestID.num b => a < b
|
||
| RequestID.null, RequestID.num _ => true
|
||
| RequestID.null, RequestID.str _ => true
|
||
| RequestID.num _, RequestID.str _ => true
|
||
| _, _ /- str < *, num < null, null < null -/ => false
|
||
|
||
private def RequestID.ltProp : LT RequestID :=
|
||
⟨fun a b => RequestID.lt a b = true⟩
|
||
|
||
instance : LT RequestID :=
|
||
RequestID.ltProp
|
||
|
||
instance (a b : RequestID) : Decidable (a < b) :=
|
||
inferInstanceAs (Decidable (RequestID.lt a b = true))
|
||
|
||
instance : FromJson RequestID := ⟨fun j =>
|
||
match j with
|
||
| str s => return RequestID.str s
|
||
| num n => return RequestID.num n
|
||
| _ => throw "a request id needs to be a number or a string"⟩
|
||
|
||
instance : ToJson RequestID := ⟨fun rid =>
|
||
match rid with
|
||
| RequestID.str s => s
|
||
| RequestID.num n => num n
|
||
| RequestID.null => null⟩
|
||
|
||
instance : ToJson Message := ⟨fun m =>
|
||
mkObj $ ⟨"jsonrpc", "2.0"⟩ :: match m with
|
||
| Message.request id method params? =>
|
||
[ ⟨"id", toJson id⟩,
|
||
⟨"method", method⟩
|
||
] ++ opt "params" params?
|
||
| Message.notification method params? =>
|
||
⟨"method", method⟩ ::
|
||
opt "params" params?
|
||
| Message.response id result =>
|
||
[ ⟨"id", toJson id⟩,
|
||
⟨"result", result⟩]
|
||
| Message.responseError id code message data? =>
|
||
[ ⟨"id", toJson id⟩,
|
||
⟨"error", mkObj $ [
|
||
⟨"code", toJson code⟩,
|
||
⟨"message", message⟩
|
||
] ++ opt "data" data?⟩
|
||
]⟩
|
||
|
||
instance : FromJson Message where
|
||
fromJson? j := do
|
||
let "2.0" ← j.getObjVal? "jsonrpc" | throw "only version 2.0 of JSON RPC is supported"
|
||
(do let id ← j.getObjValAs? RequestID "id"
|
||
let method ← j.getObjValAs? String "method"
|
||
let params? := j.getObjValAs? Structured "params"
|
||
pure (Message.request id method params?.toOption)) <|>
|
||
(do let method ← j.getObjValAs? String "method"
|
||
let params? := j.getObjValAs? Structured "params"
|
||
pure (Message.notification method params?.toOption)) <|>
|
||
(do let id ← j.getObjValAs? RequestID "id"
|
||
let result ← j.getObjVal? "result"
|
||
pure (Message.response id result)) <|>
|
||
(do let id ← j.getObjValAs? RequestID "id"
|
||
let err ← j.getObjVal? "error"
|
||
let code ← err.getObjValAs? ErrorCode "code"
|
||
let message ← err.getObjValAs? String "message"
|
||
let data? := err.getObjVal? "data"
|
||
pure (Message.responseError id code message data?.toOption))
|
||
|
||
-- TODO(WN): temporary until we have deriving FromJson
|
||
instance [FromJson α] : FromJson (Notification α) where
|
||
fromJson? j := do
|
||
let msg : Message ← fromJson? j
|
||
if let Message.notification method params? := msg then
|
||
let params := params?
|
||
let param : α ← fromJson? (toJson params)
|
||
pure $ ⟨method, param⟩
|
||
else throw "not a notification"
|
||
|
||
end Lean.JsonRpc
|
||
|
||
namespace IO.FS.Stream
|
||
|
||
open Lean
|
||
open Lean.JsonRpc
|
||
|
||
section
|
||
def readMessage (h : FS.Stream) (nBytes : Nat) : IO Message := do
|
||
let j ← h.readJson nBytes
|
||
match fromJson? j with
|
||
| Except.ok m => pure m
|
||
| Except.error inner => throw $ userError s!"JSON '{j.compress}' did not have the format of a JSON-RPC message.\n{inner}"
|
||
|
||
def readRequestAs (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α) [FromJson α] : IO (Request α) := do
|
||
let m ← h.readMessage nBytes
|
||
match m with
|
||
| Message.request id method params? =>
|
||
if method = expectedMethod then
|
||
let j := toJson params?
|
||
match fromJson? j with
|
||
| Except.ok v => pure ⟨id, expectedMethod, v⟩
|
||
| Except.error inner => throw $ userError s!"Unexpected param '{j.compress}' for method '{expectedMethod}'\n{inner}"
|
||
else
|
||
throw $ userError s!"Expected method '{expectedMethod}', got method '{method}'"
|
||
| _ => throw $ userError s!"Expected JSON-RPC request, got: '{(toJson m).compress}'"
|
||
|
||
def readNotificationAs (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α) [FromJson α] : IO (Notification α) := do
|
||
let m ← h.readMessage nBytes
|
||
match m with
|
||
| Message.notification method params? =>
|
||
if method = expectedMethod then
|
||
let j := toJson params?
|
||
match fromJson? j with
|
||
| Except.ok v => pure ⟨expectedMethod, v⟩
|
||
| Except.error inner => throw $ userError s!"Unexpected param '{j.compress}' for method '{expectedMethod}'\n{inner}"
|
||
else
|
||
throw $ userError s!"Expected method '{expectedMethod}', got method '{method}'"
|
||
| _ => throw $ userError s!"Expected JSON-RPC notification, got: '{(toJson m).compress}'"
|
||
|
||
def readResponseAs (h : FS.Stream) (nBytes : Nat) (expectedID : RequestID) (α) [FromJson α] : IO (Response α) := do
|
||
let m ← h.readMessage nBytes
|
||
match m with
|
||
| Message.response id result =>
|
||
if id == expectedID then
|
||
match fromJson? result with
|
||
| Except.ok v => pure ⟨expectedID, v⟩
|
||
| Except.error inner => throw $ userError s!"Unexpected result '{result.compress}'\n{inner}"
|
||
else
|
||
throw $ userError s!"Expected id {expectedID}, got id {id}"
|
||
| _ => throw $ userError s!"Expected JSON-RPC response, got: '{(toJson m).compress}'"
|
||
end
|
||
|
||
section
|
||
variable [ToJson α]
|
||
|
||
def writeMessage (h : FS.Stream) (m : Message) : IO Unit :=
|
||
h.writeJson (toJson m)
|
||
|
||
def writeRequest (h : FS.Stream) (r : Request α) : IO Unit :=
|
||
h.writeMessage r
|
||
|
||
def writeNotification (h : FS.Stream) (n : Notification α) : IO Unit :=
|
||
h.writeMessage n
|
||
|
||
def writeResponse (h : FS.Stream) (r : Response α) : IO Unit :=
|
||
h.writeMessage r
|
||
|
||
def writeResponseError (h : FS.Stream) (e : ResponseError Unit) : IO Unit :=
|
||
h.writeMessage (Message.responseError e.id e.code e.message none)
|
||
|
||
def writeResponseErrorWithData (h : FS.Stream) (e : ResponseError α) : IO Unit :=
|
||
h.writeMessage e
|
||
end
|
||
|
||
end IO.FS.Stream
|