1 # ezmcp/src/line: the stdio transport's decisions, with no IO 2 # (basic/transports#stdio). Bytes read from stdin are cut at each newline, 3 # each whole line is decoded from UTF-8 and is one message; a reply is its 4 # JSON text and one newline. 5 import Base 6 import 0xa81e161008ecc2f297c0a3065cb9b188/main.bend as Json 7 import 0xa81e161008ecc2f297c0a3065cb9b188/src/value.bend as Value 8 import ./server.bend as Server 9 10 # whole lines, each with its newline, and the bytes after the last newline 11 type Cut is Data: 12 Cut{lines: List<&2, List<&2, U32>>, rest: List<&2, U32>} 13 14 # a byte is a newline 15 def nl(+byte: U32) -> Bool: 16 U32.is_eq(byte, 10) 17 18 # put a byte at the front of the first piece 19 def split.put(byte: U32, ps: List<&2, List<&2, U32>>) -> List<&2, List<&2, U32>>: 20 match ps: 21 case []: 22 [[byte]] 23 case p <> rest: 24 (byte <> p) <> rest 25 26 # a newline ends a piece of its own, which the bytes before it join; any 27 # other byte joins the first piece 28 def split.at( 29 byte: U32, 30 ps: List<&2, List<&2, U32>>, 31 cut: Bool 32 ) -> List<&2, List<&2, U32>>: 33 match cut: 34 case True{}: 35 [byte] <> ps 36 case False{}: 37 split.put(byte, ps) 38 39 # bytes split after every newline: each piece but the last ends with its 40 # newline, and the last has none 41 def split(bs: List<&2, U32>) -> List<&2, List<&2, U32>>: 42 match bs: 43 case []: 44 [[]] 45 case +b <> t: 46 split.at(b, split(t), nl(b)) 47 48 def cut.cons(piece: List<&2, U32>, cb: Cut) -> Cut: 49 match cb: 50 case Cut{lines, rest}: 51 Cut{piece <> lines, rest} 52 53 def cut.go(rest: List<&2, List<&2, U32>>, piece: List<&2, U32>) -> Cut: 54 match rest: 55 case []: 56 Cut{[], piece} 57 case q <> more: 58 cut.cons(piece, cut.go(more, q)) 59 60 def cut.of(ps: List<&2, List<&2, U32>>) -> Cut: 61 match ps: 62 case []: 63 Cut{[], []} 64 case p <> rest: 65 cut.go(rest, p) 66 67 # the whole lines in a buffer, and what follows the last newline 68 def cut(bs: List<&2, U32>) -> Cut: 69 cut.of(split(bs)) 70 71 # --- UTF-8 --- 72 73 # a byte is a continuation byte, 0x80 to 0xBF 74 def cont(+byte: U32) -> Bool: 75 Bool.and(U32.is_ge(byte, 128), U32.is_le(byte, 191)) 76 77 # U+FFFD, for a byte that starts no valid sequence 78 def bad() -> Char: 79 Chr{65533} 80 81 # a continuation byte's six payload bits 82 def low6(+byte: U32) -> U32: 83 U32.sub(byte, 128) 84 85 # the code point of a two-byte sequence 86 def two(+lead: U32, +byte: U32) -> U32: 87 U32.add(U32.mul(U32.sub(lead, 192), 64), low6(byte)) 88 89 # the code point of a three-byte sequence 90 def three(+lead: U32, +byte: U32, +cb: U32) -> U32: 91 U32.add(U32.mul(U32.add(U32.mul(U32.sub(lead, 224), 64), low6(byte)), 64), low6(cb)) 92 93 # the code point of a four-byte sequence 94 def four(+lead: U32, +byte: U32, +cb: U32, +db: U32) -> U32: 95 U32.add(U32.mul(U32.add(U32.mul(U32.add(U32.mul(U32.sub(lead, 240), 64), 96 low6(byte)), 64), low6(cb)), 64), low6(db)) 97 98 # how many bytes a lead byte starts 99 type Width is Data: 100 W0{} 101 W1{} 102 W2{} 103 W3{} 104 W4{} 105 106 # 1 for ASCII, 2 to 4 for a lead byte, 0 for a byte that starts nothing 107 def width(+byte: U32) -> Width: 108 Bool.pick(Width, U32.is_lt(byte, 128), W1{}, 109 Bool.pick(Width, Bool.and(U32.is_ge(byte, 194), U32.is_le(byte, 223)), W2{}, 110 Bool.pick(Width, Bool.and(U32.is_ge(byte, 224), U32.is_le(byte, 239)), W3{}, 111 Bool.pick(Width, Bool.and(U32.is_ge(byte, 240), U32.is_le(byte, 244)), W4{}, 112 W0{})))) 113 114 # a sequence whose continuation bytes are all valid is its char, and the 115 # decoding goes on after it; else the lead byte is U+FFFD and the decoding 116 # goes on at the byte after it 117 def dec.take( 118 ch: Char, 119 ok: Bool, 120 after_lead: List<&2, U32>, 121 after_seq: List<&2, U32>, 122 go: List<&2, U32> -> String 123 ) -> String: 124 match ok: 125 case True{}: 126 SCon{ch, go(after_seq)} 127 case False{}: 128 SCon{bad(), go(after_lead)} 129 130 def dec.two(+lead: U32, bs: List<&2, U32>, go: List<&2, U32> -> String) -> String: 131 match bs: 132 case +b <> +t: 133 dec.take(Chr{two(lead, b)}, cont(b), b <> t, t, go) 134 case []: 135 SCon{bad(), SNil{}} 136 137 def dec.three( 138 +lead: U32, 139 bs: List<&2, U32>, 140 go: List<&2, U32> -> String 141 ) -> String: 142 match bs: 143 case +b <> +c <> +t: 144 dec.take(Chr{three(lead, b, c)}, Bool.and(cont(b), cont(c)), b <> c <> t, 145 t, go) 146 case other: 147 SCon{bad(), go(other)} 148 149 def dec.four(+lead: U32, bs: List<&2, U32>, go: List<&2, U32> -> String) -> String: 150 match bs: 151 case +b <> +c <> +d <> +t: 152 dec.take(Chr{four(lead, b, c, d)}, 153 Bool.and(cont(b), Bool.and(cont(c), cont(d))), b <> c <> d <> t, t, go) 154 case other: 155 SCon{bad(), go(other)} 156 157 # one sequence at a lead byte of width w, then `go` on what follows 158 def dec.lead( 159 +byte: U32, 160 tail: List<&2, U32>, 161 wid: Width, 162 go: List<&2, U32> -> String 163 ) -> String: 164 match wid: 165 case W1{}: 166 SCon{Chr{byte}, go(tail)} 167 case W2{}: 168 dec.two(byte, tail, go) 169 case W3{}: 170 dec.three(byte, tail, go) 171 case W4{}: 172 dec.four(byte, tail, go) 173 case W0{}: 174 SCon{bad(), go(tail)} 175 176 def decode.step(bs: List<&2, U32>, go: List<&2, U32> -> String) -> String: 177 match bs: 178 case []: 179 SNil{} 180 case +b <> t: 181 dec.lead(b, t, width(b), go) 182 183 # decode with fuel: every step takes at least one byte, so a buffer's 184 # length is fuel enough 185 def decode.go(fuel: Nat, bs: List<&2, U32>) -> String: 186 match fuel: 187 case 0n: 188 SNil{} 189 case 1n+f: 190 decode.step(bs, xs => decode.go(f, xs)) 191 192 # bytes decoded from UTF-8; a byte that starts no valid sequence is U+FFFD 193 def decode(+bs: List<&2, U32>) -> String: 194 decode.go(List.length(&2, U32, bs), bs) 195 196 # --- messages --- 197 198 def line.of( 199 srv: Server.Server, 200 +rev: String, 201 +text: String, 202 blank: Bool 203 ) -> Server.Turn: 204 match blank: 205 case True{}: 206 Server.Turn{Server.Quiet{}, rev} 207 case False{}: 208 Server.turn(srv, rev, text) 209 210 def line.text(srv: Server.Server, +rev: String, +text: String) -> Server.Turn: 211 line.of(srv, rev, text, String.is_empty(String.trim(text))) 212 213 # what a server does with one line read by revision `rev`, and the revision 214 # the next line is read by: nothing for a blank one, and the revision 215 # unchanged; else the server's turn on its text 216 def line(srv: Server.Server, rev: String, bs: List<&2, U32>) -> Server.Turn: 217 line.text(srv, rev, decode(bs)) 218 219 # one message on stdout: its JSON text and a newline 220 def frame(val: Value.Json) -> String: 221 Json.print(val) ++ "\n"