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. 7 import Base 8 import 0x81c67699424929b5c44cd8577e18117f/main.bend as Ezjson 9 import 0x81c67699424929b5c44cd8577e18117f/src/value.bend as J 10 import ./proto.bend as Proto 11 import ./docs.bend as Docs 12 import ./nav.bend as Nav 13 import ./semantic.bend as Semantic 14 import ../rules.bend as Lint 15 import ../finding.bend as Finding 16 import ../config.bend as Config 17 import ../lint/plan.bend as Plan 18 import ../noqa.bend as Noqa 19 import ./transport/service.bend as T 20 import ./checker/answer.bend as C 21 import ./files/service.bend as F 22 import ./report.bend as Rep 23 import ./enc.bend as Enc 24 import ../syntax/outline.bend as Outline 25 import ../syntax/lex.bend as Lex 26 import ../syntax/word.bend as Word 27 28 # the methods the server answers; MOther is anything else 29 type 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 48 type 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 54 type 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 60 type 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 64 type 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 69 type Role is Data: 70 RRequest{} 71 RNotice{} 72 RExit{} 73 RUnknown{} 74 75 # a method by its name 76 def 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 94 def 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 128 def 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 132 def id_of(msg: J.Json) -> J.Json: 133 Ezjson.get(msg, "id") 134 135 # the document a request is about 136 def 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 140 def 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 144 def 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 148 def 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 158 def 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 166 def 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 174 def 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 182 def 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 186 def noted(notes: List<&2, String>, docs: List<&2, Docs.Doc>) -> Work: 187 Work{notes, Answer{J.JNull{}}, docs} 188 189 def 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 207 def 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 218 def 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 223 def 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 231 def 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 239 def 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 250 def 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 261 def 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 272 def 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 283 def 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 287 def pub.configs(~fs: F.Files, +uri: String) -> IO(List<&2, Maybe<&2, String>>): 288 do IO>>: 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 293 def 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: 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 307 def pub.saved(~fs: F.Files, ~ck: C.Checker, ee: Enc.Enc, +uri: String, docs: List<&2, Docs.Doc>) -> IO(Work): 308 do IO: 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 314 def pub.changed(~fs: F.Files, ee: Enc.Enc, +uri: String, text: String, docs: List<&2, Docs.Doc>) -> IO(Work): 315 do IO: 316 rs : List<&2, Maybe<&2, String>> <- pub.configs(~fs, uri) 317 return pub.change(ee, uri, text, rs, docs) 318 319 def 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 328 def 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 339 def 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 343 def 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: 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 356 def 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: 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 369 def 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: 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 383 def 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: 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 397 def 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 402 def 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 411 def 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: 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 427 def 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: 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 446 def semantic(~fs: F.Files, ee: Enc.Enc, +docs: List<&2, Docs.Doc>, uri: String) -> IO(Work): # noqa: L001 LSP IO glue 447 do IO: 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 452 def symbols(~fs: F.Files, ee: Enc.Enc, +docs: List<&2, Docs.Doc>, +uri: String) -> IO(Work): # noqa: L001 LSP IO glue 453 do IO: 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 460 def 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 504 def 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 512 def 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: 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 525 def decoded(mm: Maybe<&2, J.Json>) -> J.Json: 526 Maybe.default(&2, J.Json, mm, J.JNull{}) 527 528 # every reply sent, in order 529 def send_all(~H: Type, ~tr: T.Transport, 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: 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 539 def 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 554 def answer( # noqa: L001 serve loop IO 555 ~H: Type, 556 ~tr: T.Transport, 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: 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 568 def turn.msg( 569 ~H: Type, 570 ~tr: T.Transport, 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: 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 585 def turn( # noqa: L001 serve loop IO 586 ~H: Type, 587 ~tr: T.Transport, 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 603 def got( # noqa: L001 serve loop IO 604 ~H: Type, 605 ~tr: T.Transport, 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 616 def serve.go( 617 ~H: Type, 618 ~tr: T.Transport, 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: 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 636 def serve( # noqa: L001 serve loop IO 637 ~H: Type, 638 ~tr: T.Transport, 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)