src/lsp/server.bend

bolt@1.18.0.0 · 645 lines · 0 definitions

1# lsp/server: the loop. The transport the messages ride on, the files
2# behind imports and Base, and the checker behind the diagnostics are passed
3# in (transport/, files/, checker/answer.bend's Checker; checker/bend.bend's
4# service is the real checker). The open documents are the
5# loop's state. The checker runs on open and save, against the file on disk;
6# the linter on every open, change and save, on the text the editor shows.
7import Base
8import emerging-ezjson@1.2.0.0/main.bend as Ezjson
9import emerging-ezjson@1.2.0.0/src/value.bend as J
10import bolt@1.18.0.0/src/lsp/proto.bend as Proto
11import bolt@1.18.0.0/src/lsp/docs.bend as Docs
12import bolt@1.18.0.0/src/lsp/nav.bend as Nav
13import bolt@1.18.0.0/src/lsp/semantic.bend as Semantic
14import bolt@1.18.0.0/src/rules.bend as Lint
15import bolt@1.18.0.0/src/finding.bend as Finding
16import bolt@1.18.0.0/src/config.bend as Config
17import bolt@1.18.0.0/src/lint/plan.bend as Plan
18import bolt@1.18.0.0/src/noqa.bend as Noqa
19import bolt@1.18.0.0/src/lsp/transport/service.bend as T
20import bolt@1.18.0.0/src/lsp/checker/answer.bend as C
21import bolt@1.18.0.0/src/lsp/files/service.bend as F
22import bolt@1.18.0.0/src/lsp/report.bend as Rep
23import bolt@1.18.0.0/src/lsp/enc.bend as Enc
24import bolt@1.18.0.0/src/syntax/outline.bend as Outline
25import bolt@1.18.0.0/src/syntax/lex.bend as Lex
26import bolt@1.18.0.0/src/syntax/word.bend as Word
27
28# the methods the server answers; MOther is anything else
29type Method is Data:
30  MInitialize{}
31  MShutdown{}
32  MExit{}
33  MOpen{}
34  MChange{}
35  MSave{}
36  MClose{}
37  MHover{}
38  MDefinition{}
39  MSymbols{}
40  MCompletion{}
41  MReferences{}
42  MRename{}
43  MSemantic{}
44  MOther{}
45
46# a message the server sends: a notification, already printed, or the
47# response to a request, its result or its error
48type Reply is Data:
49  RNote{body: String}
50  RResult{id: J.Json, result: J.Json}
51  RError{id: J.Json, code: String, message: String}
52
53# what a request is answered with: a result, or an error's code and message
54type Answer is Data:
55  Answer{result: J.Json}
56  Refusal{code: String, message: String}
57
58# what the work behind a method gave: the notifications it sends, the answer
59# (read for a request only), the documents after it. It never sees the id
60type Work is Data:
61  Work{notes: List<&2, String>, answer: Answer, docs: List<&2, Docs.Doc>}
62
63# what one message leads to: the replies to send, whether to stop, the documents
64type Out is Data:
65  Out{replies: List<&2, Reply>, quit: Bool, docs: List<&2, Docs.Doc>}
66
67# how the server treats a method: a request owed one response, a
68# notification, exit, or a method it does not know
69type Role is Data:
70  RRequest{}
71  RNotice{}
72  RExit{}
73  RUnknown{}
74
75# a method by its name
76def method(+name: String) -> Method:
77  Bool.pick(Method, String.eq(name, "initialize"), MInitialize{},
78    Bool.pick(Method, String.eq(name, "shutdown"), MShutdown{},
79      Bool.pick(Method, String.eq(name, "exit"), MExit{},
80        Bool.pick(Method, String.eq(name, "textDocument/didOpen"), MOpen{},
81          Bool.pick(Method, String.eq(name, "textDocument/didChange"), MChange{},
82            Bool.pick(Method, String.eq(name, "textDocument/didSave"), MSave{},
83              Bool.pick(Method, String.eq(name, "textDocument/didClose"), MClose{},
84                Bool.pick(Method, String.eq(name, "textDocument/hover"), MHover{},
85                  Bool.pick(Method, String.eq(name, "textDocument/definition"), MDefinition{},
86                    Bool.pick(Method, String.eq(name, "textDocument/documentSymbol"), MSymbols{},
87                      Bool.pick(Method, String.eq(name, "textDocument/completion"), MCompletion{},
88                        Bool.pick(Method, String.eq(name, "textDocument/references"), MReferences{},
89                          Bool.pick(Method, String.eq(name, "textDocument/rename"), MRename{},
90                            Bool.pick(Method, String.eq(name, "textDocument/semanticTokens/full"), MSemantic{},
91                              MOther{}))))))))))))))
92
93# a method's role
94def role(mm: Method) -> Role:
95  match mm:
96    case MInitialize{}:
97      RRequest{}
98    case MShutdown{}:
99      RRequest{}
100    case MExit{}:
101      RExit{}
102    case MOpen{}:
103      RNotice{}
104    case MChange{}:
105      RNotice{}
106    case MSave{}:
107      RNotice{}
108    case MClose{}:
109      RNotice{}
110    case MHover{}:
111      RRequest{}
112    case MDefinition{}:
113      RRequest{}
114    case MSymbols{}:
115      RRequest{}
116    case MCompletion{}:
117      RRequest{}
118    case MReferences{}:
119      RRequest{}
120    case MRename{}:
121      RRequest{}
122    case MSemantic{}:
123      RRequest{}
124    case MOther{}:
125      RUnknown{}
126
127# a message's method name, empty when it has none
128def name_of(msg: J.Json) -> String:
129  Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(msg, "method")), "")
130
131# a message's id: null for a notification
132def id_of(msg: J.Json) -> J.Json:
133  Ezjson.get(msg, "id")
134
135# the document a request is about
136def uri_of(params: J.Json) -> String:
137  Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(Ezjson.get(params, "textDocument"), "uri")), "")
138
139# the line of a request's position
140def line_of(params: J.Json) -> U32:
141  Maybe.default(&2, U32, Ezjson.as_u32(Ezjson.get(Ezjson.get(params, "position"), "line")), 0)
142
143# the character of a request's position
144def col_of(params: J.Json) -> U32:
145  Maybe.default(&2, U32, Ezjson.as_u32(Ezjson.get(Ezjson.get(params, "position"), "character")), 0)
146
147# a reply as the text sent
148def text(rr: Reply) -> String:
149  match rr:
150    case RNote{body}:
151      body
152    case RResult{id, result}:
153      Ezjson.print(Proto.response(id, result))
154    case RError{id, code, message}:
155      Ezjson.print(Proto.failure(id, code, message))
156
157# notifications as replies
158def notices(ns: List<&2, String>) -> List<&2, Reply>:
159  match ns:
160    case Nil{}:
161      Nil{}
162    case Con{n, t}:
163      RNote{n} <> notices(t)
164
165# the response to the request with this id
166def respond(id: J.Json, aa: Answer) -> Reply:
167  match aa:
168    case Answer{result}:
169      RResult{id, result}
170    case Refusal{code, message}:
171      RError{id, code, message}
172
173# a request the server does not know is refused; a notification is ignored
174def unknown(id: J.Json) -> List<&2, Reply>:
175  match id:
176    case J.JNull{}:
177      []
178    case other:
179      [RError{other, "-32601", "method not found"}]
180
181# the work of a request: its result, no notifications
182def answered(result: J.Json, docs: List<&2, Docs.Doc>) -> Work:
183  Work{[], Answer{result}, docs}
184
185# the work of a notification: what it sends, and no answer
186def noted(notes: List<&2, String>, docs: List<&2, Docs.Doc>) -> Work:
187  Work{notes, Answer{J.JNull{}}, docs}
188
189def step.go(rr: Role, id: J.Json, ww: Work) -> Out:
190  match rr:
191    case RRequest{}:
192      Work{notes, answer, docs} = ww
193      Out{[respond(id, answer)], False{}, docs}
194    case RNotice{}:
195      Work{notes, answer, docs} = ww
196      Out{notices(notes), False{}, docs}
197    case RExit{}:
198      Work{notes, answer, docs} = ww
199      Out{[], True{}, docs}
200    case RUnknown{}:
201      Work{notes, answer, docs} = ww
202      Out{unknown(id), False{}, docs}
203
204# the dispatcher's decision for one message, whatever its method's work gave:
205# a request gets one response with its id, a notification its notices, exit
206# stops, and a method it does not know is refused or ignored
207def step(mm: Method, id: J.Json, ww: Work) -> Out:
208  step.go(role(mm), id, ww)
209
210# publishing
211# ----------
212# What open, change, save and close publish is decided by the pure pub.*
213# defs below, over what the IO gave them: the checker's errors (ds) and the
214# texts of the document's bolt.bend candidates (rs). work only gathers those.
215
216# a document's diagnostics, as the notification sent: the checker's errors,
217# then the linter's findings
218def pub.note(cc: Enc.Cols, uri: String, ds: List<&2, Rep.Diag>, gs: List<&2, Finding.Graded>) -> String:
219  Ezjson.print(Proto.publish(cc, uri, ds, gs))
220
221# the bolt.bend candidates of a document's path, nearest first: the ones the
222# CLI reads for a file at that path, run from the same directory
223def pub.cands(cwd: String, path: String) -> List<&2, String>:
224  Config.chain(Config.home(cwd, Config.dir_of(path)))
225
226# the linter's findings on the text the editor shows, at the levels the
227# nearest bolt.bend read gives them, less what the text's noqa comments
228# silence, then the `noqa` rule's, which judges no project rule's code (the
229# editor runs none); none for a bolt.bend, which is bolt's configuration,
230# not source
231def pub.lint(+path: String, +text: String, rs: List<&2, Maybe<&2, String>>) -> List<&2, Finding.Graded>:
232  +cfg = Config.nearest(rs)
233  Bool.pick(List<&2, Finding.Graded>, Plan.is_config(path), [],
234    Plan.after([Noqa.file(path, Lex.tokens(text))], False{}, Lint.graded(cfg, Lint.all(path, text)),
235      fs => Lint.graded(cfg, fs)))
236
237# open or save: the checker's errors with the lint of the text, the errors
238# kept for the changes after
239def pub.checked(
240  ee: Enc.Enc,
241  +uri: String,
242  +text: String,
243  +ds: List<&2, Rep.Diag>,
244  rs: List<&2, Maybe<&2, String>>,
245  +docs: List<&2, Docs.Doc>
246) -> Work:
247  noted([pub.note(Enc.cols(ee, text), uri, ds, pub.lint(Proto.path(uri), text, rs))], Docs.set_errors(docs, uri, ds))
248
249# open: the document's text kept, then published with the checker's errors
250def pub.open(
251  ee: Enc.Enc,
252  +uri: String,
253  +text: String,
254  ds: List<&2, Rep.Diag>,
255  rs: List<&2, Maybe<&2, String>>,
256  +docs: List<&2, Docs.Doc>
257) -> Work:
258  pub.checked(ee, uri, text, ds, rs, Docs.set(docs, uri, text))
259
260# save: the text the editor shows published with the checker's new errors
261def pub.save(
262  ee: Enc.Enc,
263  +uri: String,
264  ds: List<&2, Rep.Diag>,
265  rs: List<&2, Maybe<&2, String>>,
266  +docs: List<&2, Docs.Doc>
267) -> Work:
268  pub.checked(ee, uri, Maybe.default(&2, String, Docs.get(docs, uri), ""), ds, rs, docs)
269
270# change: the new text kept, and its lint published with the checker's last
271# errors
272def pub.change(
273  ee: Enc.Enc,
274  +uri: String,
275  +text: String,
276  rs: List<&2, Maybe<&2, String>>,
277  +docs: List<&2, Docs.Doc>
278) -> Work:
279  noted([pub.note(Enc.cols(ee, text), uri, Docs.errors(docs, uri), pub.lint(Proto.path(uri), text, rs))],
280    Docs.set(docs, uri, text))
281
282# close: the document forgotten, and an empty list published
283def pub.close(ee: Enc.Enc, +uri: String, docs: List<&2, Docs.Doc>) -> Work:
284  noted([pub.note(Enc.cols(ee, ""), uri, [], [])], Docs.del(docs, uri))
285
286# the texts of a document's bolt.bend candidates, as the files hold them
287def pub.configs(~fs: F.Files, +uri: String) -> IO(List<&2, Maybe<&2, String>>):
288  do IO<List<&2, Maybe<&2, String>>>:
289    cwd : String <- F.cwd(fs)
290    F.read_all(~fs, pub.cands(cwd, Proto.path(uri)))
291
292# open, on the checker's run on the file on disk
293def pub.opened(
294  ~fs: F.Files,
295  ~ck: C.Checker,
296  ee: Enc.Enc,
297  +uri: String,
298  text: String,
299  docs: List<&2, Docs.Doc>
300) -> IO(Work):
301  do IO<Work>:
302    aa : C.Answer <- C.check(ck, Proto.path(uri))
303    rs : List<&2, Maybe<&2, String>> <- pub.configs(~fs, uri)
304    return pub.open(ee, uri, text, C.errors(aa), rs, docs)
305
306# save, on the checker's run on the file on disk
307def pub.saved(~fs: F.Files, ~ck: C.Checker, ee: Enc.Enc, +uri: String, docs: List<&2, Docs.Doc>) -> IO(Work):
308  do IO<Work>:
309    aa : C.Answer <- C.check(ck, Proto.path(uri))
310    rs : List<&2, Maybe<&2, String>> <- pub.configs(~fs, uri)
311    return pub.save(ee, uri, C.errors(aa), rs, docs)
312
313# change: the linter only
314def pub.changed(~fs: F.Files, ee: Enc.Enc, +uri: String, text: String, docs: List<&2, Docs.Doc>) -> IO(Work):
315  do IO<Work>:
316    rs : List<&2, Maybe<&2, String>> <- pub.configs(~fs, uri)
317    return pub.change(ee, uri, text, rs, docs)
318
319def hover.of(mm: Maybe<&2, Nav.Found>) -> J.Json:
320  match mm:
321    case None{}:
322      J.JNull{}
323    case Some{Nav.Found{uri, item}}:
324      Proto.hover(item)
325    case Some{Nav.Here{uri, b}}:
326      Proto.hover.local(b)
327
328def definition.of(cc: Enc.Cols, mm: Maybe<&2, Nav.Found>) -> J.Json:
329  match mm:
330    case None{}:
331      J.JNull{}
332    case Some{Nav.Found{uri, item}}:
333      Proto.location(cc, uri, item)
334    case Some{Nav.Here{uri, b}}:
335      Proto.location.local(cc, uri, b)
336
337# the column a request's position names, read in the negotiated encoding of
338# the document's text
339def col_in(cc: Enc.Cols, +params: J.Json) -> U32:
340  Enc.into(cc, line_of(params), col_of(params))
341
342# hover at a request's position
343def hovered(  # noqa: L001 LSP IO glue
344  ~fs: F.Files,
345  ee: Enc.Enc,
346  +docs: List<&2, Docs.Doc>,
347  +uri: String,
348  +params: J.Json
349) -> IO(Work):
350  do IO<Work>:
351    text : String <- Nav.text_of(~fs, docs, uri)
352    f : Maybe<&2, Nav.Found> <- Nav.hover(~fs, docs, uri, line_of(params), col_in(Enc.cols(ee, text), params))
353    return answered(hover.of(f), docs)
354
355# definition at a request's position
356def defined(  # noqa: L001 LSP IO glue
357  ~fs: F.Files,
358  ee: Enc.Enc,
359  +docs: List<&2, Docs.Doc>,
360  +uri: String,
361  +params: J.Json
362) -> IO(Work):
363  do IO<Work>:
364    +text : String <- Nav.text_of(~fs, docs, uri)
365    +cc : Enc.Cols = Enc.cols(ee, text)
366    f : Maybe<&2, Nav.Found> <- Nav.at(~fs, docs, uri, line_of(params), col_in(cc, params))
367    return answered(definition.of(cc, f), docs)
368
369def complete.with(
370  ~fs: F.Files,
371  cc: Enc.Cols,
372  +prefix: String,
373  +docs: List<&2, Docs.Doc>,
374  uri: String,
375  +line: U32,
376  +col: U32
377) -> IO(Work):
378  do IO<Work>:
379    items : List<&2, Outline.Item> <- Nav.candidates(~fs, docs, uri, prefix, line)
380    return answered(Proto.completions(cc, items, line, (col - U32.from_nat(String.length(prefix)) : U32), col), docs)
381
382# the candidates for the name being typed at a position
383def complete(  # noqa: L001 LSP IO glue
384  ~fs: F.Files,
385  ee: Enc.Enc,
386  +docs: List<&2, Docs.Doc>,
387  +uri: String,
388  +params: J.Json
389) -> IO(Work):
390  do IO<Work>:
391    +text : String <- Nav.text_of(~fs, docs, uri)
392    +cc : Enc.Cols = Enc.cols(ee, text)
393    +line : U32 = line_of(params)
394    +col : U32 = col_in(cc, params)
395    complete.with(~fs, cc, Word.before(text, line, col), docs, uri, line, col)
396
397def references.of(cc: Enc.Cols, ss: Nav.Sites, uri: String, name: String) -> J.Json:
398  Nav.Sites{mine, at} = ss
399  Proto.locations(cc, uri, name, at)
400
401# only a name bound in the document renames; the editor shows the refusal
402def rename.of(cc: Enc.Cols, ss: Nav.Sites, uri: String, name: String, new: String) -> Answer:
403  Nav.Sites{mine, at} = ss
404  match mine:
405    case True{}:
406      Answer{Proto.rename(cc, uri, name, new, at)}
407    case False{}:
408      Refusal{"-32803", "only a name bound in this file can be renamed"}
409
410# every position of the name at a position
411def references(  # noqa: L001 LSP IO glue
412  ~fs: F.Files,
413  ee: Enc.Enc,
414  +docs: List<&2, Docs.Doc>,
415  +uri: String,
416  +params: J.Json
417) -> IO(Work):
418  do IO<Work>:
419    +text : String <- Nav.text_of(~fs, docs, uri)
420    +cc : Enc.Cols = Enc.cols(ee, text)
421    +line : U32 = line_of(params)
422    +col : U32 = col_in(cc, params)
423    s : Nav.Sites <- Nav.sites(~fs, docs, uri, line, col)
424    return answered(references.of(cc, s, uri, Word.at(text, line, col)), docs)
425
426# the edit renaming the name at a position, or a refusal
427def renamed(  # noqa: L001 LSP IO glue
428  ~fs: F.Files,
429  ee: Enc.Enc,
430  +docs: List<&2, Docs.Doc>,
431  +uri: String,
432  +params: J.Json
433) -> IO(Work):
434  do IO<Work>:
435    +text : String <- Nav.text_of(~fs, docs, uri)
436    +cc : Enc.Cols = Enc.cols(ee, text)
437    +line : U32 = line_of(params)
438    +col : U32 = col_in(cc, params)
439    s : Nav.Sites <- Nav.sites(~fs, docs, uri, line, col)
440    return Work{[],
441      rename.of(cc, s, uri, Word.at(text, line, col),
442        Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(params, "newName")), "")),
443      docs}
444
445# the document's semantic tokens
446def semantic(~fs: F.Files, ee: Enc.Enc, +docs: List<&2, Docs.Doc>, uri: String) -> IO(Work):  # noqa: L001 LSP IO glue
447  do IO<Work>:
448    text : String <- Nav.text_of(~fs, docs, uri)
449    return answered(Proto.semantic(Semantic.data(ee, text)), docs)
450
451# the document's symbols
452def symbols(~fs: F.Files, ee: Enc.Enc, +docs: List<&2, Docs.Doc>, +uri: String) -> IO(Work):  # noqa: L001 LSP IO glue
453  do IO<Work>:
454    text : String <- Nav.text_of(~fs, docs, uri)
455    items : List<&2, Outline.Item> <- Nav.outline(~fs, docs, uri)
456    return answered(Proto.symbols(Enc.cols(ee, text), items), docs)
457
458# the work behind a method: what the checker, the files and the documents
459# give it. It does not see the message's id: step alone puts that on a reply
460def work(  # noqa: L001 LSP IO glue
461  ~fs: F.Files,
462  ~ck: C.Checker,
463  ee: Enc.Enc,
464  mm: Method,
465  +params: J.Json,
466  +docs: List<&2, Docs.Doc>
467) -> IO(Work):
468  match mm:
469    case MInitialize{}:
470      IO.pure(Work, answered(Proto.capabilities(ee), docs))
471    case MShutdown{}:
472      IO.pure(Work, answered(J.JNull{}, docs))
473    case MExit{}:
474      IO.pure(Work, noted([], docs))
475    case MOpen{}:
476      pub.opened(~fs, ~ck, ee, uri_of(params),
477        Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(Ezjson.get(params, "textDocument"), "text")), ""), docs)
478    case MChange{}:
479      pub.changed(~fs, ee, uri_of(params),
480        Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(Ezjson.at(Ezjson.get(params, "contentChanges"), 0), "text")), ""), docs)
481    case MSave{}:
482      pub.saved(~fs, ~ck, ee, uri_of(params), docs)
483    case MClose{}:
484      IO.pure(Work, pub.close(ee, uri_of(params), docs))
485    case MHover{}:
486      hovered(~fs, ee, docs, uri_of(params), params)
487    case MDefinition{}:
488      defined(~fs, ee, docs, uri_of(params), params)
489    case MSymbols{}:
490      symbols(~fs, ee, docs, uri_of(params))
491    case MCompletion{}:
492      complete(~fs, ee, docs, uri_of(params), params)
493    case MReferences{}:
494      references(~fs, ee, docs, uri_of(params), params)
495    case MRename{}:
496      renamed(~fs, ee, docs, uri_of(params), params)
497    case MSemantic{}:
498      semantic(~fs, ee, docs, uri_of(params))
499    case MOther{}:
500      IO.pure(Work, noted([], docs))
501
502# the encoding after a message: initialize settles it (enc.bend's
503# negotiate); every other message keeps it
504def encoding(mm: Method, params: J.Json, ee: Enc.Enc) -> Enc.Enc:
505  match mm:
506    case MInitialize{}:
507      Enc.negotiate(params)
508    case _other:
509      ee
510
511# one message: its method's work, then step's decision on it
512def handle(  # noqa: L001 LSP IO glue
513  ~fs: F.Files,
514  ~ck: C.Checker,
515  ee: Enc.Enc,
516  +msg: J.Json,
517  docs: List<&2, Docs.Doc>
518) -> IO(Out):
519  +mm = method(name_of(msg))
520  do IO<Out>:
521    w : Work <- work(~fs, ~ck, ee, mm, Ezjson.get(msg, "params"), docs)
522    return step(mm, id_of(msg), w)
523
524# a parsed message, or null when it did not parse
525def decoded(mm: Maybe<&2, J.Json>) -> J.Json:
526  Maybe.default(&2, J.Json, mm, J.JNull{})
527
528# every reply sent, in order
529def send_all(~H: Type, ~tr: T.Transport<H>, replies: List<&2, Reply>, h3: H) -> IO(H):  # noqa: L001 serve loop IO
530  match replies:
531    case Nil{}:
532      IO.pure(H, h3)
533    case Con{body, t}:
534      do IO<H>:
535        h2 : H <- T.send(H, tr, h3, text(body))
536        send_all(~H, ~tr, t, h2)
537
538# the loop goes on with the documents, unless the message said to quit
539def next(  # noqa: L001 serve loop IO
540  -H: Type,
541  quit: Bool,
542  h3: H,
543  ee: Enc.Enc,
544  docs: List<&2, Docs.Doc>,
545  rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit)
546) -> IO(Unit):
547  match quit:
548    case True{}:
549      IO.pure(Unit, Unit{})
550    case False{}:
551      rest(h3, ee, docs)
552
553# the replies sent, then the loop goes on
554def answer(  # noqa: L001 serve loop IO
555  ~H: Type,
556  ~tr: T.Transport<H>,
557  out: Out,
558  h3: H,
559  ee: Enc.Enc,
560  rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit)
561) -> IO(Unit):
562  Out{replies, quit, docs} = out
563  do IO<Unit>:
564    h2 : H <- send_all(~H, ~tr, replies, h3)
565    next(H, quit, h2, ee, docs, rest)
566
567# a message handled under the encoding it leaves, and answered
568def turn.msg(
569  ~H: Type,
570  ~tr: T.Transport<H>,
571  ~fs: F.Files,
572  ~ck: C.Checker,
573  +msg: J.Json,
574  h3: H,
575  ee: Enc.Enc,
576  docs: List<&2, Docs.Doc>,
577  rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit)
578) -> IO(Unit):
579  +e2 = encoding(method(name_of(msg)), Ezjson.get(msg, "params"), ee)
580  do IO<Unit>:
581    out : Out <- handle(~fs, ~ck, e2, msg, docs)
582    answer(~H, ~tr, out, h3, e2, rest)
583
584# one message body handled and answered; none means the peer is gone
585def turn(  # noqa: L001 serve loop IO
586  ~H: Type,
587  ~tr: T.Transport<H>,
588  ~fs: F.Files,
589  ~ck: C.Checker,
590  mm: Maybe<&2, String>,
591  h3: H,
592  ee: Enc.Enc,
593  docs: List<&2, Docs.Doc>,
594  rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit)
595) -> IO(Unit):
596  match mm:
597    case None{}:
598      IO.pure(Unit, Unit{})
599    case Some{body}:
600      turn.msg(~H, ~tr, ~fs, ~ck, decoded(Ezjson.parse(body)), h3, ee, docs, rest)
601
602# what recv gave: the handle and maybe a body
603def got(  # noqa: L001 serve loop IO
604  ~H: Type,
605  ~tr: T.Transport<H>,
606  ~fs: F.Files,
607  ~ck: C.Checker,
608  rr: H & Maybe<&2, String>,
609  ee: Enc.Enc,
610  docs: List<&2, Docs.Doc>,
611  rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit)
612) -> IO(Unit):
613  (h, m) = rr
614  turn(~H, ~tr, ~fs, ~ck, m, h, ee, docs, rest)
615
616def serve.go(
617  ~H: Type,
618  ~tr: T.Transport<H>,
619  ~fs: F.Files,
620  ~ck: C.Checker,
621  fuel: Nat,
622  h3: H,
623  ee: Enc.Enc,
624  docs: List<&2, Docs.Doc>
625) -> IO(Unit):
626  match fuel:
627    case 0n:
628      IO.pure(Unit, Unit{})
629    case 1n+f:
630      do IO<Unit>:
631        r : H & Maybe<&2, String> <- T.recv(H, tr, h3)
632        got(~H, ~tr, ~fs, ~ck, r, ee, docs, hh => e2 => dd => serve.go(~H, ~tr, ~fs, ~ck, f, hh, e2, dd))
633
634# serves until exit, the peer's end, or the fuel's (a message each), in
635# UTF-16 until initialize negotiates
636def serve(  # noqa: L001 serve loop IO
637  ~H: Type,
638  ~tr: T.Transport<H>,
639  ~fs: F.Files,
640  ~ck: C.Checker,
641  fuel: Nat,
642  h3: H,
643  docs: List<&2, Docs.Doc>
644) -> IO(Unit):
645  serve.go(~H, ~tr, ~fs, ~ck, fuel, h3, Enc.Utf16{}, docs)